mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-11-04 17:17:39 +00:00
757 lines
41 KiB
Haskell
757 lines
41 KiB
Haskell
{-# LANGUAGE CPP #-}
|
|
{-# LANGUAGE LambdaCase #-}
|
|
{-# LANGUAGE NamedFieldPuns #-}
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
{-# LANGUAGE TupleSections #-}
|
|
{-# LANGUAGE TypeOperators #-}
|
|
|
|
module Bots.StoreService.NamesBalancesTests (namesBalancesTests, cardPaid, coveredSpend, credit, creditLate, figures, holder, lateCredit, payment, placeOrderRow, settleHolder, source) where
|
|
|
|
import Bots.StoreService.NamesOrdersTests (Ctx (..), withOrders)
|
|
import Bots.StoreService.NamesStoreTests (at, btc, invoiceStatus, newReg, ownerA, ownerB, paidOrder, reg, signature, t0, write)
|
|
import Bots.StoreService.WebTests (capturingStderr, rcv, withServiceStore, withServiceStores)
|
|
import Control.Concurrent.Async (async, wait)
|
|
import Control.Concurrent.MVar (newEmptyMVar, putMVar, takeMVar)
|
|
import Control.Concurrent.STM (atomically, check)
|
|
import Control.Monad (forM_)
|
|
import Data.Bifunctor (first)
|
|
import Data.Int (Int64)
|
|
import Data.Text (Text)
|
|
import qualified Data.Text as T
|
|
import Data.Text.Encoding (encodeUtf8)
|
|
import Data.Time.Clock (UTCTime, addUTCTime)
|
|
import Simplex.Chat.PaymentService.Types (CardProvider (..), InvoiceId (..), InvoiceStatus (..), ServicePaymentMethod (..))
|
|
import Simplex.Messaging.Agent.Store.Common (DBStore, withTransaction)
|
|
import qualified Simplex.Messaging.Agent.Store.DB as DB
|
|
import qualified Simplex.Messaging.Crypto as C
|
|
import Simplex.Messaging.SimplexName (labelHash)
|
|
import StoreService.Config (ServiceConfig (..))
|
|
import StoreService.Names.Balances
|
|
import StoreService.Names.Orders (ClosedOrder (..), OrderRefusal (..), OrderRequest (..), OrdersEnv (..))
|
|
import qualified StoreService.Names.Orders as Orders
|
|
import StoreService.Names.Phases (RegistrationId (..))
|
|
import StoreService.Names.Signatures (OrderFields (..))
|
|
import StoreService.Names.Store
|
|
import StoreService.Names.Turns (balanceTurnCount, newTurns, turnsCount, withBalanceTurn, withLabelTurn)
|
|
import StoreService.Orders (cancelOpenInvoice, expireOverdue, settleOrder)
|
|
import StoreService.Poller (cleanupUnpaid, newPollerEnv, unpaidRetention)
|
|
import StoreService.Providers (PaymentSignal (..))
|
|
import StoreService.Waiters (newWaiters)
|
|
import StoreService.Web.Server (WebEnv (..))
|
|
import System.Timeout (timeout)
|
|
import Test.Hspec
|
|
|
|
#if defined(dbPostgres)
|
|
import Control.Concurrent.Async (concurrently)
|
|
import Control.Concurrent.STM (modifyTVar', newTVarIO, readTVar)
|
|
import Data.Either (isRight)
|
|
import Database.PostgreSQL.Simple (Only (..), (:.) (..))
|
|
#else
|
|
import Database.SQLite.Simple (Only (..), (:.) (..))
|
|
#endif
|
|
|
|
namesBalancesTests :: Spec
|
|
namesBalancesTests = describe "StoreService names: balances" $ do
|
|
describe "store" $ do
|
|
it "credits a balance, making it on the first credit" testCredit
|
|
it "spends a covered price at once from the amount seen, refusing a stale amount, a dearer price or a held balance, changing nothing" testSpendAtOnce
|
|
it "lets one of two difference orders hold a balance, the losing order not written" testTwoHolds
|
|
it "spends a hold only for its holder and while the amount covers it, clearing the hold" testSpendHeld
|
|
it "releases a hold only for its holder" testRelease
|
|
it "refuses spends, holds and held spends of 0 or fewer cents, and negative credits, refunds and late credits" testNoNegativeCents
|
|
it "refunds a balance only at its amount, above 0 and not held, refused while held and accepted once released" testRefund
|
|
it "keeps amount = credited - spent - refunded over spends at once, a held spend, a released hold and a refund, spent counting orders past unpaid" testInvariant
|
|
it "keeps the invariant over credit, refund, credit, refund, refunded the sum" testCreditRefundTwice
|
|
it "reads a hold and a spend back through a new store connection" testRestart
|
|
it "finds an order and its balance by the order's registration id or its invoice id" testByReference
|
|
it "chains an order back along its balance sources over three orders, with each payment and its cents" testChain
|
|
it "stops the chain of a closed difference order at itself, with its late credits" testChainOfClosed
|
|
it "hashes a balance order over its fields, the paying order, price, balance and method, and stores it for a difference spend" testBalanceOrderHash
|
|
it "returns the 50 newest orders and then every older one holding a balance above 0" testFindBalances
|
|
describe "closes" $ do
|
|
it "releases a hold at a cancel, the sweep's expiry, settlement's expiry and the next order's close" testClosesRelease
|
|
it "reads an overdue holder's hold as none, and closes it in the transaction of the order placed" testOverdueHolderClosed
|
|
it "skips a named holder no longer live, and refuses with the balance when a live one cannot be closed" testNamedHolders
|
|
it "refuses with the balance a named holder whose invoice has not expired, leaving it open and holding" testNamedHolderNotDue
|
|
it "keeps at the cleanup, and logs, an unpaid order still holding a balance" testCleanupKeepsHolder
|
|
describe "refunds" $ do
|
|
it "refuses no order, no balance, a balance held by an open or overdue holder, and another amount, then records one with its chain" testRecordRefund
|
|
describe "late credits" $ do
|
|
it "credits a late payment once per credit number, a taken number changing neither the rows nor the balance" testCreditLatePayment
|
|
it "tells each case of pending money and selects confirmed money not all credited, by the order whatever the invoice's status" testLateScans
|
|
it "leaves 0.1 and 0.2 BTC credited against 0.3 confirmed neither pending nor selected" testExactUnits
|
|
it "tells confirmed money not credited as such while its cents still read 0, as Monero's can" testConfirmedBeforeCents
|
|
it "reads crypto received with no confirmed figure as unconfirmed" testNoConfirmedFigure
|
|
it "alerts on pending money past the cutoff once, again only when more arrives, claimed by one of two instances" testLateAlerts
|
|
it "alerts again on money that could not be counted in units when more of it arrives" testUncountedAlertRepeats
|
|
describe "turns" $ do
|
|
it "runs two holders of one balance one after the other, and nests in a label turn" testBalanceTurn
|
|
#if defined(dbPostgres)
|
|
describe "two connections" $ do
|
|
it "applies one of a spend and a refund, or of a held spend and a refund, and keeps the invariant for a credit and a refund" testConcurrentWrites
|
|
#endif
|
|
|
|
-- fixtures
|
|
|
|
-- | Every name and commitment its own, so any number of live orders fit.
|
|
fieldsN :: Text -> OrderFields
|
|
fieldsN label =
|
|
OrderFields
|
|
{ ofLabelHash = labelHash label,
|
|
ofLength = T.length label,
|
|
ofDuration = 69292800,
|
|
ofCommitment = C.sha256Hash $ encodeUtf8 label,
|
|
ofSealed = "sealed-" <> label
|
|
}
|
|
|
|
-- | A crypto invoice row, or a card one with its client secret in place of an address.
|
|
invoiceRow :: DB.Connection -> Text -> Bool -> Int -> UTCTime -> IO ()
|
|
invoiceRow db invId card cents expiresAt =
|
|
DB.execute
|
|
db
|
|
"INSERT INTO sx_badge_service_invoices (invoice_id, provider, price, amount, currency, payment_url, payment_address, payment_crypto_currency, payment_crypto_amount, expires_at, status, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)"
|
|
((invId, provider, cents, cents, "usd" :: Text, url, address, currency, cryptoAmount, expiresAt) :. ("open" :: Text, t0, t0))
|
|
where
|
|
destination :: (Text, Maybe Text, Maybe Text, Maybe Text, Maybe Text)
|
|
destination
|
|
| card = ("stripe", Just "pi_secret", Nothing, Nothing, Nothing)
|
|
| otherwise = ("crypto", Nothing, Just "bc1qexampleaddress", Just "btc", Just "0.003")
|
|
(provider, url, address, currency, cryptoAmount) = destination
|
|
|
|
-- | A paid order, unpaid and live, whose invoice of the cents expires at the given time.
|
|
placeOrderRow :: DBStore -> Text -> Bool -> Int -> UTCTime -> IO ()
|
|
placeOrderRow st r card cents expiresAt = withTransaction st $ \db -> do
|
|
invoiceRow db ("inv-" <> r) card cents expiresAt
|
|
createPaidRegistration db (newReg r ownerA (fieldsN r)) (OTInvoice method cents) (paidOrder ("inv-" <> r)) {poMethod = method, poPrice = cents} t0 `shouldReturn` Right ()
|
|
where
|
|
method = if card then SPMCard CPStripe else btc
|
|
|
|
-- | A taken paid order with a balance of its price.
|
|
source :: DBStore -> Text -> Int -> IO ()
|
|
source st r cents = do
|
|
placeOrderRow st r False cents (at 60)
|
|
taken <- reg st r
|
|
write st taken {nrPhase = PTaken} `shouldReturn` Written 2
|
|
credit st r cents
|
|
|
|
credit :: DBStore -> Text -> Int -> IO ()
|
|
credit st r cents = withTransaction st $ \db -> creditBalance db (RegistrationId r) cents t0
|
|
|
|
holdPrice :: Int
|
|
holdPrice = 40000
|
|
|
|
holdTerms :: Text -> Int -> OrderTerms
|
|
holdTerms src seen = OTBalance (RegistrationId src) holdPrice seen (Just btc)
|
|
|
|
-- | A difference order holding all of the source's balance seen, its invoice of the difference expiring at the given time.
|
|
holder :: DBStore -> Text -> Text -> Int -> UTCTime -> IO (Either RegistrationRefused ())
|
|
holder st r src seen expiresAt = withTransaction st $ \db -> do
|
|
invoiceRow db ("inv-" <> r) False (holdPrice - seen) expiresAt
|
|
createPaidRegistration db (newReg r ownerA (fieldsN r)) (holdTerms src seen) (paidOrder ("inv-" <> r)) {poPrice = holdPrice} t0
|
|
|
|
hold :: Text -> UTCTime -> Bool -> BalanceHold
|
|
hold h bhUntil bhOverdue = BalanceHold {bhHolder = RegistrationId h, bhUntil, bhOverdue}
|
|
|
|
coveredSpend :: DBStore -> Text -> Text -> Int -> Int -> IO (Either RegistrationRefused ())
|
|
coveredSpend st r src price seen = withTransaction st $ \db -> createBalanceRegistration db (newReg r ownerA (fieldsN r)) (RegistrationId src) price seen t0
|
|
|
|
-- | The holder queued and its hold spent in one transaction, as settlement does.
|
|
settleHolder :: DB.Connection -> NameRegistration -> IO Bool
|
|
settleHolder db r = do
|
|
updateRegistration db r {nrPhase = PQueued} t0 `shouldReturn` Written (nrVersion r + 1)
|
|
spendHeldBalance db (nrId r) (nrBalanceUsed r) t0
|
|
|
|
release :: DBStore -> Text -> IO Bool
|
|
release st h = withTransaction st $ \db -> releaseHeldBalance db (RegistrationId h) t0
|
|
|
|
refund :: DBStore -> Text -> Int -> IO Bool
|
|
refund st r cents = withTransaction st $ \db -> refundBalance db (RegistrationId r) cents (at 5)
|
|
|
|
-- | Amount, credited, refunded and the hold, as they read at the given time.
|
|
figures :: DBStore -> Text -> UTCTime -> IO (Maybe (Int, Int, Int, Maybe BalanceHold))
|
|
figures st r now = withTransaction st $ \db -> fmap (\Balance {blAmount, blCredited, blRefunded, blHold} -> (blAmount, blCredited, blRefunded, blHold)) <$> getBalance db (RegistrationId r) now
|
|
|
|
-- | amount = credited - spent - refunded, spent being the balance_used of the orders past unpaid it paid for.
|
|
invariantHolds :: DBStore -> Text -> IO ()
|
|
invariantHolds st r = do
|
|
Just (amount, credited, refunded, _) <- figures st r t0
|
|
used <- withTransaction st $ \db -> DB.query db "SELECT balance_used FROM sx_badge_service_name_registrations WHERE balance_source = ? AND phase <> ?" (r, PUnpaid)
|
|
(r, amount) `shouldBe` (r, credited - sum (map fromOnly (used :: [Only Int])) - refunded)
|
|
|
|
absent :: DBStore -> Text -> IO ()
|
|
absent st r = withTransaction st (\db -> getRegistration db (RegistrationId r)) `shouldReturn` Nothing
|
|
|
|
-- | A payment row with the figures the provider's reads left: cents received and confirmed, the crypto text, and units received and confirmed.
|
|
data Paid = Paid
|
|
{ pCents :: Int,
|
|
pConfirmedCents :: Int,
|
|
pCrypto :: Maybe Text,
|
|
pUnits :: Maybe Int64,
|
|
pConfirmedUnits :: Maybe Int64,
|
|
pInFull :: Bool
|
|
}
|
|
|
|
cryptoPaid :: Int64 -> Int64 -> Paid
|
|
cryptoPaid units confirmed = Paid {pCents = 20000, pConfirmedCents = 0, pCrypto = Just "0.003", pUnits = Just units, pConfirmedUnits = Just confirmed, pInFull = False}
|
|
|
|
cardPaid :: Int -> Int -> Paid
|
|
cardPaid cents confirmed = Paid {pCents = cents, pConfirmedCents = confirmed, pCrypto = Nothing, pUnits = Nothing, pConfirmedUnits = Nothing, pInFull = False}
|
|
|
|
payment :: DBStore -> Text -> Paid -> UTCTime -> IO ()
|
|
payment st invId Paid {pCents, pConfirmedCents, pCrypto, pUnits, pConfirmedUnits, pInFull} createdAt =
|
|
withTransaction st $ \db ->
|
|
DB.execute
|
|
db
|
|
"INSERT INTO sx_badge_service_payments (payment_id, invoice_id, provider, amount, currency, crypto_paid, paid_in_full, status, confirmed_amount, received_units, confirmed_units, invoice_rate, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)"
|
|
((invId, invId, "crypto" :: Text, pCents, "usd" :: Text, pCrypto, if pInFull then 1 else 0 :: Int, "pending" :: Text, pConfirmedCents, pUnits) :. (pConfirmedUnits, Just ("62500" :: Text), createdAt, createdAt))
|
|
|
|
-- | An order closed unpaid, by crypto unless card, with a late payment row created at the given time.
|
|
closedWith :: DBStore -> Text -> Bool -> Paid -> UTCTime -> IO ()
|
|
closedWith st r card paid createdAt = do
|
|
placeOrderRow st r card 20000 (at 60)
|
|
open <- reg st r
|
|
write st open {nrLive = False} `shouldReturn` Written 2
|
|
payment st ("inv-" <> r) paid createdAt
|
|
|
|
lateCredit :: Text -> Int -> Maybe Int64 -> Int -> LateCredit
|
|
lateCredit r n units cents =
|
|
LateCredit
|
|
{ lcInvoice = InvoiceId ("inv-" <> r),
|
|
lcCreditNo = n,
|
|
lcRegistration = RegistrationId r,
|
|
lcUnits = units,
|
|
lcInvoiceRate = Just "62500",
|
|
lcCurrentRate = Just "60000.5",
|
|
lcRateUsed = Just "60000.5",
|
|
lcCents = cents
|
|
}
|
|
|
|
creditLate :: DBStore -> LateCredit -> IO Bool
|
|
creditLate st c = withTransaction st $ \db -> creditLatePayment db c (at 3)
|
|
|
|
orderRequest :: Text -> OrderRequest
|
|
orderRequest r = OrderRequest {orOwner = ownerA, orFields = fieldsN r, orTimestamp = 0, orSignature = signature}
|
|
|
|
spendInsert :: Text -> Text -> Int -> Int -> DB.Connection -> UTCTime -> IO (Either OrderRefusal ())
|
|
spendInsert r src price seen db now = first (const $ ORBalanceChanged Nothing) <$> createBalanceRegistration db (newReg r ownerA (fieldsN r)) (RegistrationId src) price seen now
|
|
|
|
closedIds :: Either OrderRefusal [ClosedOrder] -> Either OrderRefusal [(RegistrationId, Int64)]
|
|
closedIds = fmap $ map $ \ClosedOrder {coRegistration, coVersion} -> (nrId coRegistration, coVersion)
|
|
|
|
-- store
|
|
|
|
testCredit :: IO ()
|
|
testCredit = withServiceStore $ \st -> do
|
|
placeOrderRow st "s" False 20000 (at 60)
|
|
figures st "s" t0 `shouldReturn` Nothing
|
|
credit st "s" 20000
|
|
credit st "s" 2500
|
|
figures st "s" t0 `shouldReturn` Just (22500, 22500, 0, Nothing)
|
|
|
|
testSpendAtOnce :: IO ()
|
|
testSpendAtOnce = withServiceStore $ \st -> do
|
|
source st "s" 10000
|
|
coveredSpend st "c1" "s" 6000 10000 `shouldReturn` Right ()
|
|
c1 <- reg st "c1"
|
|
(nrPhase c1, nrLive c1, nrPrice c1, nrPayment c1, nrBalanceSource c1, nrBalanceUsed c1) `shouldBe` (PQueued, True, 6000, RPBalance, Just (RegistrationId "s"), 6000)
|
|
nrOrderHash c1 `shouldBe` orderHash ownerA (fieldsN "c1") (OTBalance (RegistrationId "s") 6000 10000 Nothing)
|
|
figures st "s" t0 `shouldReturn` Just (4000, 10000, 0, Nothing)
|
|
coveredSpend st "c2" "s" 3000 10000 `shouldReturn` Left RRBalanceChanged
|
|
coveredSpend st "c3" "s" 5000 4000 `shouldReturn` Left RRBalanceChanged
|
|
mapM_ (absent st) ["c2", "c3"]
|
|
figures st "s" t0 `shouldReturn` Just (4000, 10000, 0, Nothing)
|
|
holder st "h" "s" 4000 (at 60) `shouldReturn` Right ()
|
|
coveredSpend st "c4" "s" 1000 4000 `shouldReturn` Left RRBalanceChanged
|
|
absent st "c4"
|
|
release st "h" `shouldReturn` True
|
|
coveredSpend st "c5" "s" 4000 4000 `shouldReturn` Right ()
|
|
figures st "s" t0 `shouldReturn` Just (0, 10000, 0, Nothing)
|
|
|
|
testTwoHolds :: IO ()
|
|
testTwoHolds = withServiceStore $ \st -> do
|
|
source st "s" 10000
|
|
holder st "h1" "s" 10000 (at 60) `shouldReturn` Right ()
|
|
holder st "h2" "s" 10000 (at 60) `shouldReturn` Left RRBalanceChanged
|
|
absent st "h2"
|
|
h1 <- reg st "h1"
|
|
(nrPhase h1, nrPayment h1, nrPrice h1, nrBalanceSource h1, nrBalanceUsed h1) `shouldBe` (PUnpaid, RPInvoice btc (InvoiceId "inv-h1"), holdPrice, Just (RegistrationId "s"), 10000)
|
|
figures st "s" t0 `shouldReturn` Just (10000, 10000, 0, Just $ hold "h1" (at 60) False)
|
|
release st "h1" `shouldReturn` True
|
|
holder st "h3" "s" 9000 (at 60) `shouldReturn` Left RRBalanceChanged
|
|
absent st "h3"
|
|
figures st "s" t0 `shouldReturn` Just (10000, 10000, 0, Nothing)
|
|
|
|
testSpendHeld :: IO ()
|
|
testSpendHeld = withServiceStore $ \st -> do
|
|
source st "s" 10000
|
|
source st "other" 5000
|
|
holder st "h" "s" 10000 (at 60) `shouldReturn` Right ()
|
|
holder st "g" "other" 5000 (at 60) `shouldReturn` Right ()
|
|
let spendBy :: Text -> Int -> IO Bool
|
|
spendBy h used = withTransaction st $ \db -> spendHeldBalance db (RegistrationId h) used t0
|
|
spendBy "nobody" 10000 `shouldReturn` False
|
|
spendBy "h" 10001 `shouldReturn` False
|
|
figures st "s" t0 `shouldReturn` Just (10000, 10000, 0, Just $ hold "h" (at 60) False)
|
|
spendBy "g" 5000 `shouldReturn` True
|
|
figures st "s" t0 `shouldReturn` Just (10000, 10000, 0, Just $ hold "h" (at 60) False)
|
|
figures st "other" t0 `shouldReturn` Just (0, 5000, 0, Nothing)
|
|
spendBy "h" 10000 `shouldReturn` True
|
|
figures st "s" t0 `shouldReturn` Just (0, 10000, 0, Nothing)
|
|
spendBy "h" 0 `shouldReturn` False
|
|
|
|
testRelease :: IO ()
|
|
testRelease = withServiceStore $ \st -> do
|
|
source st "s" 10000
|
|
holder st "h" "s" 10000 (at 60) `shouldReturn` Right ()
|
|
release st "g" `shouldReturn` False
|
|
figures st "s" t0 `shouldReturn` Just (10000, 10000, 0, Just $ hold "h" (at 60) False)
|
|
release st "h" `shouldReturn` True
|
|
figures st "s" t0 `shouldReturn` Just (10000, 10000, 0, Nothing)
|
|
release st "h" `shouldReturn` False
|
|
|
|
testNoNegativeCents :: IO ()
|
|
testNoNegativeCents = withServiceStore $ \st -> do
|
|
source st "s" 10000
|
|
mapM (\(r, cents) -> coveredSpend st r "s" cents 10000) [("c0", 0), ("c1", -1)] `shouldReturn` replicate 2 (Left RRBalanceChanged)
|
|
mapM_ (absent st) ["c0", "c1"]
|
|
figures st "s" t0 `shouldReturn` Just (10000, 10000, 0, Nothing)
|
|
holder st "h" "s" 10000 (at 60) `shouldReturn` Right ()
|
|
mapM (\used -> withTransaction st $ \db -> spendHeldBalance db (RegistrationId "h") used t0) [0, -1] `shouldReturn` [False, False]
|
|
figures st "s" t0 `shouldReturn` Just (10000, 10000, 0, Just $ hold "h" (at 60) False)
|
|
source st "z" 0
|
|
holder st "hz" "z" 0 (at 60) `shouldReturn` Left RRBalanceChanged
|
|
figures st "z" t0 `shouldReturn` Just (0, 0, 0, Nothing)
|
|
placeOrderRow st "x" False 20000 (at 60)
|
|
credit st "x" 5000
|
|
creditLate st (lateCredit "x" 1 Nothing (-1)) `shouldThrow` anyException
|
|
figures st "x" t0 `shouldReturn` Just (5000, 5000, 0, Nothing)
|
|
forM_ ["credited", "refunded"] $ \column ->
|
|
withTransaction st (\db -> DB.execute_ db $ "UPDATE sx_badge_service_name_balances SET " <> column <> " = -1 WHERE registration_id = 's'") `shouldThrow` anyException
|
|
figures st "s" t0 `shouldReturn` Just (10000, 10000, 0, Just $ hold "h" (at 60) False)
|
|
|
|
testRefund :: IO ()
|
|
testRefund = withServiceStore $ \st -> do
|
|
source st "s" 10000
|
|
holder st "h" "s" 10000 (at 60) `shouldReturn` Right ()
|
|
refund st "s" 10000 `shouldReturn` False
|
|
figures st "s" t0 `shouldReturn` Just (10000, 10000, 0, Just $ hold "h" (at 60) False)
|
|
release st "h" `shouldReturn` True
|
|
mapM (refund st "s") [9999, 10001] `shouldReturn` [False, False]
|
|
figures st "s" t0 `shouldReturn` Just (10000, 10000, 0, Nothing)
|
|
refund st "s" 10000 `shouldReturn` True
|
|
Just Balance {blAmount, blCredited, blRefunded, blRefundedAt} <- withTransaction st $ \db -> getBalance db (RegistrationId "s") t0
|
|
(blAmount, blCredited, blRefunded, blRefundedAt) `shouldBe` (0, 10000, 10000, Just (at 5))
|
|
mapM (refund st "s") [0, 10000] `shouldReturn` [False, False]
|
|
|
|
testInvariant :: IO ()
|
|
testInvariant = withServiceStore $ \st -> do
|
|
source st "s" 30000
|
|
invariantHolds st "s"
|
|
coveredSpend st "c1" "s" 5000 30000 `shouldReturn` Right ()
|
|
invariantHolds st "s"
|
|
holder st "h1" "s" 25000 (at 60) `shouldReturn` Right ()
|
|
invariantHolds st "s"
|
|
h1 <- reg st "h1"
|
|
withTransaction st (`settleHolder` h1) `shouldReturn` True
|
|
figures st "s" t0 `shouldReturn` Just (0, 30000, 0, Nothing)
|
|
invariantHolds st "s"
|
|
credit st "s" 8000
|
|
holder st "h2" "s" 8000 (at 60) `shouldReturn` Right ()
|
|
h2 <- reg st "h2"
|
|
withTransaction st (\db -> closeOverdue db h2 (at 61)) `shouldReturn` Just (nrVersion h2 + 1)
|
|
figures st "s" t0 `shouldReturn` Just (8000, 38000, 0, Nothing)
|
|
invariantHolds st "s"
|
|
refund st "s" 8000 `shouldReturn` True
|
|
figures st "s" t0 `shouldReturn` Just (0, 38000, 8000, Nothing)
|
|
invariantHolds st "s"
|
|
|
|
testCreditRefundTwice :: IO ()
|
|
testCreditRefundTwice = withServiceStore $ \st -> do
|
|
source st "s" 10000
|
|
refund st "s" 10000 `shouldReturn` True
|
|
credit st "s" 4000
|
|
refund st "s" 4000 `shouldReturn` True
|
|
figures st "s" t0 `shouldReturn` Just (0, 14000, 14000, Nothing)
|
|
invariantHolds st "s"
|
|
|
|
testRestart :: IO ()
|
|
testRestart = withServiceStores $ \open -> do
|
|
st <- open
|
|
source st "s" 10000
|
|
source st "t" 8000
|
|
holder st "h" "s" 10000 (at 60) `shouldReturn` Right ()
|
|
coveredSpend st "c" "t" 3000 8000 `shouldReturn` Right ()
|
|
let readBack :: DBStore -> IO (Maybe (Int, Int, Int, Maybe BalanceHold), Maybe (Int, Int, Int, Maybe BalanceHold), NameRegistration, NameRegistration)
|
|
readBack store = (,,,) <$> figures store "s" t0 <*> figures store "t" t0 <*> reg store "h" <*> reg store "c"
|
|
stored@(held, spent, _, c) <- readBack st
|
|
(held, spent, nrBalanceUsed c) `shouldBe` (Just (10000, 10000, 0, Just $ hold "h" (at 60) False), Just (5000, 8000, 0, Nothing), 3000)
|
|
restarted <- open
|
|
readBack restarted `shouldReturn` stored
|
|
|
|
testByReference :: IO ()
|
|
testByReference = withServiceStore $ \st -> do
|
|
source st "s" 10000
|
|
placeOrderRow st "plain" False 20000 (at 60)
|
|
let byRef :: Text -> IO (Maybe (RegistrationId, Maybe Int))
|
|
byRef ref = withTransaction st $ \db -> fmap (fmap (fmap blAmount)) <$> balanceByReference db ref t0
|
|
mapM byRef ["s", "inv-s"] `shouldReturn` replicate 2 (Just (RegistrationId "s", Just 10000))
|
|
mapM byRef ["plain", "inv-plain"] `shouldReturn` replicate 2 (Just (RegistrationId "plain", Nothing))
|
|
byRef "nothing" `shouldReturn` Nothing
|
|
|
|
testChain :: IO ()
|
|
testChain = withServiceStore $ \st -> do
|
|
source st "a" 20000
|
|
holder st "b" "a" 20000 (at 60) `shouldReturn` Right ()
|
|
b <- reg st "b"
|
|
withTransaction st (`settleHolder` b) `shouldReturn` True
|
|
queued <- reg st "b"
|
|
write st queued {nrPhase = PTaken} `shouldReturn` Written (nrVersion queued + 1)
|
|
credit st "b" holdPrice
|
|
coveredSpend st "c" "b" 10000 holdPrice `shouldReturn` Right ()
|
|
withTransaction st (\db -> balanceChain db (RegistrationId "c"))
|
|
`shouldReturn` [ BalanceLink {lkRegistration = RegistrationId "c", lkPayment = RPBalance, lkPaid = 0, lkFromBalance = 10000, lkLateCredits = []},
|
|
BalanceLink {lkRegistration = RegistrationId "b", lkPayment = RPInvoice btc (InvoiceId "inv-b"), lkPaid = holdPrice - 20000, lkFromBalance = 20000, lkLateCredits = []},
|
|
BalanceLink {lkRegistration = RegistrationId "a", lkPayment = RPInvoice btc (InvoiceId "inv-a"), lkPaid = 20000, lkFromBalance = 0, lkLateCredits = []}
|
|
]
|
|
|
|
testChainOfClosed :: IO ()
|
|
testChainOfClosed = withServiceStore $ \st -> do
|
|
source st "s" 15000
|
|
holder st "d" "s" 15000 (at 60) `shouldReturn` Right ()
|
|
waiters <- newWaiters
|
|
cancelOpenInvoice st waiters Nothing (InvoiceId "inv-d") (at 2) `shouldReturn` True
|
|
figures st "s" t0 `shouldReturn` Just (15000, 15000, 0, Nothing)
|
|
let late = lateCredit "d" 1 (Just 40000) 2400
|
|
creditLate st late `shouldReturn` True
|
|
withTransaction st (\db -> balanceChain db (RegistrationId "d"))
|
|
`shouldReturn` [BalanceLink {lkRegistration = RegistrationId "d", lkPayment = RPInvoice btc (InvoiceId "inv-d"), lkPaid = 0, lkFromBalance = 0, lkLateCredits = [late]}]
|
|
|
|
testBalanceOrderHash :: IO ()
|
|
testBalanceOrderHash = withServiceStore $ \st -> do
|
|
let o = fieldsN "h"
|
|
terms = holdTerms "s" 15000
|
|
base = orderHash ownerA o terms
|
|
src = RegistrationId "s"
|
|
variants =
|
|
[ ("labelHash" :: Text, orderHash ownerA o {ofLabelHash = labelHash "hh"} terms),
|
|
("length", orderHash ownerA o {ofLength = 2} terms),
|
|
("duration", orderHash ownerA o {ofDuration = 69292801} terms),
|
|
("owner", orderHash ownerB o terms),
|
|
("commitment", orderHash ownerA o {ofCommitment = C.sha256Hash "another"} terms),
|
|
("sealed", orderHash ownerA o {ofSealed = ofSealed o <> "x"} terms),
|
|
("paying order", orderHash ownerA o (OTBalance (RegistrationId "t") holdPrice 15000 (Just btc))),
|
|
("price", orderHash ownerA o (OTBalance src (holdPrice + 1) 15000 (Just btc))),
|
|
("balance", orderHash ownerA o (OTBalance src holdPrice 15001 (Just btc))),
|
|
("method", orderHash ownerA o (OTBalance src holdPrice 15000 (Just $ SPMCard CPStripe))),
|
|
("no method", orderHash ownerA o (OTBalance src holdPrice 15000 Nothing)),
|
|
("an invoice order", orderHash ownerA o (OTInvoice btc holdPrice))
|
|
]
|
|
orderHash ownerA (fieldsN "h") (holdTerms "s" 15000) `shouldBe` base
|
|
forM_ variants $ \(field, h) -> (field, h == base) `shouldBe` (field, False)
|
|
source st "s" 15000
|
|
holder st "h" "s" 15000 (at 60) `shouldReturn` Right ()
|
|
fmap nrId <$> withTransaction st (`getRegistrationByOrderHash` base) `shouldReturn` Just (RegistrationId "h")
|
|
|
|
testFindBalances :: IO ()
|
|
testFindBalances = withServiceStore $ \st -> do
|
|
let rid :: Int -> Text
|
|
rid i = "r" <> T.pack (show i)
|
|
forM_ [1 .. 52 :: Int] $ \i -> withTransaction st $ \db -> do
|
|
invoiceRow db ("inv-" <> rid i) False 20000 (at 60)
|
|
createPaidRegistration db (newReg (rid i) ownerA (fieldsN $ rid i)) (OTInvoice btc 20000) (paidOrder ("inv-" <> rid i)) (at i) `shouldReturn` Right ()
|
|
credit st "r1" 5000
|
|
credit st "r2" 3000
|
|
refund st "r2" 3000 `shouldReturn` True
|
|
map nrId <$> withTransaction st (`findRegistrations` ownerA) `shouldReturn` map (RegistrationId . rid) ([52, 51 .. 3] <> [1])
|
|
|
|
-- closes
|
|
|
|
testClosesRelease :: IO ()
|
|
testClosesRelease = withServiceStore $ \st -> do
|
|
waiters <- newWaiters
|
|
let closes = ["cancel", "sweep", "settled", "next"]
|
|
forM_ closes $ \c -> do
|
|
source st ("s-" <> c) 10000
|
|
holder st ("h-" <> c) ("s-" <> c) 10000 (at 60) `shouldReturn` Right ()
|
|
cancelOpenInvoice st waiters Nothing (InvoiceId "inv-h-cancel") (at 2) `shouldReturn` True
|
|
expireOverdue st waiters Nothing (at 61) (at 61) [InvoiceId "inv-h-sweep"] `shouldReturn` [InvoiceId "inv-h-sweep"]
|
|
settleOrder st waiters Nothing (InvoiceId "inv-h-settled") (SigClosed (rcv 0 Nothing)) (at 2) `shouldReturn` Right ISExpired
|
|
next <- reg st "h-next"
|
|
withTransaction st (\db -> closeOverdue db next (at 61)) `shouldReturn` Just (nrVersion next + 1)
|
|
forM_ closes $ \c -> do
|
|
(c,) <$> figures st ("s-" <> c) t0 `shouldReturn` (c, Just (10000, 10000, 0, Nothing))
|
|
(c,) . nrLive <$> reg st ("h-" <> c) `shouldReturn` (c, False)
|
|
(c,) <$> invoiceStatus st ("inv-h-" <> c) `shouldReturn` (c, "expired")
|
|
|
|
testOverdueHolderClosed :: IO ()
|
|
testOverdueHolderClosed = withOrders $ \Ctx {cStore = st, cOrders} -> do
|
|
now <- oeNow cOrders
|
|
source st "s" 10000
|
|
holder st "h" "s" 10000 (at 60) `shouldReturn` Right ()
|
|
Just balance <- withTransaction st $ \db -> getBalance db (RegistrationId "s") now
|
|
(blHold balance, heldBy balance) `shouldBe` (Just $ hold "h" (at 60) True, Nothing)
|
|
Just h <- withTransaction st $ \db -> overdueHolder db (RegistrationId "s") now
|
|
nrId h `shouldBe` RegistrationId "h"
|
|
withTransaction st (\db -> overdueHolder db (RegistrationId "s") t0) `shouldReturn` Nothing
|
|
closedIds <$> Orders.placeOrder cOrders (orderRequest "c") [h] (\_ _ -> pure $ Left ORBadRequest) `shouldReturn` Left ORBadRequest
|
|
invoiceStatus st "inv-h" `shouldReturn` "open"
|
|
nrLive <$> reg st "h" `shouldReturn` True
|
|
figures st "s" now `shouldReturn` Just (10000, 10000, 0, Just $ hold "h" (at 60) True)
|
|
closedIds <$> Orders.placeOrder cOrders (orderRequest "c") [h] (spendInsert "c" "s" 6000 10000) `shouldReturn` Right [(RegistrationId "h", nrVersion h + 1)]
|
|
invoiceStatus st "inv-h" `shouldReturn` "expired"
|
|
nrLive <$> reg st "h" `shouldReturn` False
|
|
figures st "s" now `shouldReturn` Just (4000, 10000, 0, Nothing)
|
|
nrPhase <$> reg st "c" `shouldReturn` PQueued
|
|
|
|
testNamedHolders :: IO ()
|
|
testNamedHolders = withOrders $ \Ctx {cStore = st, cOrders, cWeb} -> do
|
|
now <- oeNow cOrders
|
|
source st "s1" 10000
|
|
holder st "h1" "s1" 10000 (at 60) `shouldReturn` Right ()
|
|
h1 <- reg st "h1"
|
|
cancelOpenInvoice st (weWaiters cWeb) Nothing (InvoiceId "inv-h1") now `shouldReturn` True
|
|
closedIds <$> Orders.placeOrder cOrders (orderRequest "c1") [h1] (spendInsert "c1" "s1" 6000 10000) `shouldReturn` Right []
|
|
figures st "s1" now `shouldReturn` Just (4000, 10000, 0, Nothing)
|
|
source st "s2" 10000
|
|
holder st "h2" "s2" 10000 (at 60) `shouldReturn` Right ()
|
|
h2 <- reg st "h2"
|
|
payment st "inv-h2" (cardPaid 100 0) (at 61)
|
|
Just funded <- withTransaction st $ \db -> getBalance db (RegistrationId "s2") now
|
|
blHold funded `shouldBe` Just (hold "h2" (at 60) False)
|
|
closedIds <$> Orders.placeOrder cOrders (orderRequest "c2") [h2] (spendInsert "c2" "s2" 6000 10000) `shouldReturn` Left (ORBalanceChanged $ Just funded)
|
|
absent st "c2"
|
|
nrLive <$> reg st "h2" `shouldReturn` True
|
|
invoiceStatus st "inv-h2" `shouldReturn` "open"
|
|
|
|
testNamedHolderNotDue :: IO ()
|
|
testNamedHolderNotDue = withOrders $ \Ctx {cStore = st, cOrders} -> do
|
|
now <- oeNow cOrders
|
|
let expiry = addUTCTime 600 now
|
|
source st "s" 10000
|
|
holder st "h" "s" 10000 expiry `shouldReturn` Right ()
|
|
h <- reg st "h"
|
|
Just held <- withTransaction st $ \db -> getBalance db (RegistrationId "s") now
|
|
blHold held `shouldBe` Just (hold "h" expiry False)
|
|
closedIds <$> Orders.placeOrder cOrders (orderRequest "c") [h] (spendInsert "c" "s" 6000 10000) `shouldReturn` Left (ORBalanceChanged $ Just held)
|
|
absent st "c"
|
|
invoiceStatus st "inv-h" `shouldReturn` "open"
|
|
nrLive <$> reg st "h" `shouldReturn` True
|
|
figures st "s" now `shouldReturn` Just (10000, 10000, 0, Just $ hold "h" expiry False)
|
|
|
|
testCleanupKeepsHolder :: IO ()
|
|
testCleanupKeepsHolder = withOrders $ \Ctx {cStore = st, cWeb} -> do
|
|
source st "s" 10000
|
|
holder st "h" "s" 10000 t0 `shouldReturn` Right ()
|
|
let due = addUTCTime (unpaidRetention + 1) t0
|
|
poller <- newPollerEnv st (weWaiters cWeb) Nothing (weHints cWeb) (weProviders cWeb) (poll $ weConfig cWeb)
|
|
logs <- capturingStderr $ cleanupUnpaid poller due
|
|
logs `shouldSatisfy` T.isInfixOf "unpaid registration h is kept because it still holds the balance of registration s"
|
|
nrLive <$> reg st "h" `shouldReturn` True
|
|
figures st "s" due `shouldReturn` Just (10000, 10000, 0, Just $ hold "h" t0 True)
|
|
|
|
-- refunds
|
|
|
|
testRecordRefund :: IO ()
|
|
testRecordRefund = withServiceStore $ \st -> do
|
|
turns <- newTurns
|
|
waiters <- newWaiters
|
|
source st "s" 10000
|
|
placeOrderRow st "plain" False 20000 (at 60)
|
|
holder st "h" "s" 10000 (at 60) `shouldReturn` Right ()
|
|
let refundOf :: Text -> Int -> UTCTime -> IO (Either RefundRefusal RefundRecord)
|
|
refundOf ref cents now = recordRefund turns st ref cents now
|
|
refundOf "nothing" 10000 t0 `shouldReturn` Left RefundNoOrder
|
|
refundOf "plain" 20000 t0 `shouldReturn` Left RefundNoBalance
|
|
refundOf "s" 10000 t0 `shouldReturn` Left (RefundHeld $ hold "h" (at 60) False)
|
|
refundOf "inv-s" 10000 (at 61) `shouldReturn` Left (RefundHeld $ hold "h" (at 60) True)
|
|
figures st "s" t0 `shouldReturn` Just (10000, 10000, 0, Just $ hold "h" (at 60) False)
|
|
cancelOpenInvoice st waiters Nothing (InvoiceId "inv-h") (at 2) `shouldReturn` True
|
|
refundOf "s" 9000 t0 `shouldReturn` Left (RefundAmount 10000)
|
|
chain <- withTransaction st $ \db -> balanceChain db (RegistrationId "s")
|
|
chain `shouldBe` [BalanceLink {lkRegistration = RegistrationId "s", lkPayment = RPInvoice btc (InvoiceId "inv-s"), lkPaid = 10000, lkFromBalance = 0, lkLateCredits = []}]
|
|
refundOf "inv-s" 10000 t0 `shouldReturn` Right RefundRecord {rdRegistration = RegistrationId "s", rdCents = 10000, rdChain = chain}
|
|
figures st "s" t0 `shouldReturn` Just (0, 10000, 10000, Nothing)
|
|
refundOf "s" 10000 t0 `shouldReturn` Left (RefundAmount 0)
|
|
turnsCount turns `shouldReturn` 0
|
|
|
|
-- late credits
|
|
|
|
testCreditLatePayment :: IO ()
|
|
testCreditLatePayment = withServiceStore $ \st -> do
|
|
closedWith st "x" False (cryptoPaid 400000 400000) t0
|
|
let state :: IO (Maybe (Int64, Int, Int))
|
|
state = withTransaction st $ \db -> fmap (\LateCreditState {lsCreditedUnits, lsCreditedCents, lsCredits} -> (lsCreditedUnits, lsCreditedCents, lsCredits)) <$> lateCreditState db (InvoiceId "inv-x")
|
|
creditLate st (lateCredit "x" 1 (Just 300000) 3000) `shouldReturn` True
|
|
creditLate st (lateCredit "x" 1 (Just 100000) 1000) `shouldReturn` False
|
|
figures st "x" t0 `shouldReturn` Just (3000, 3000, 0, Nothing)
|
|
state `shouldReturn` Just (300000, 3000, 1)
|
|
creditLate st (lateCredit "x" 2 (Just 100000) 1000) `shouldReturn` True
|
|
figures st "x" t0 `shouldReturn` Just (4000, 4000, 0, Nothing)
|
|
state `shouldReturn` Just (400000, 4000, 2)
|
|
|
|
testLateScans :: IO ()
|
|
testLateScans = withServiceStore $ \st -> do
|
|
let cases =
|
|
[ ("uncredited", False, cryptoPaid 300000 300000, Just LPUncredited),
|
|
("unconfirmed", False, cryptoPaid 300000 0, Just LPUnconfirmed),
|
|
("monero", False, Paid {pCents = 0, pConfirmedCents = 0, pCrypto = Just "0", pUnits = Just 0, pConfirmedUnits = Just 0, pInFull = True}, Just LPConfirming),
|
|
("part", False, cryptoPaid 300000 100000, Just LPUnconfirmed),
|
|
("card", True, cardPaid 5000 5000, Just LPUncredited),
|
|
("cardpending", True, cardPaid 5000 0, Just LPUnconfirmed),
|
|
("settledlate", False, cryptoPaid 200000 200000, Just LPUncredited),
|
|
("uncounted", False, (cryptoPaid 0 0) {pCents = 5000, pUnits = Nothing}, Just LPUncounted),
|
|
("credited", True, cardPaid 5000 5000, Nothing)
|
|
]
|
|
forM_ (zip [1 ..] cases) $ \(i, (r, card, paid, _)) -> closedWith st r card paid (at i)
|
|
creditLate st (lateCredit "part" 1 (Just 100000) 1500) `shouldReturn` True
|
|
creditLate st (lateCredit "credited" 1 Nothing 5000) `shouldReturn` True
|
|
withTransaction st $ \db -> DB.execute db "UPDATE sx_badge_service_invoices SET status = ? WHERE invoice_id = ?" ("paid" :: Text, "inv-settledlate" :: Text)
|
|
placeOrderRow st "live" False 20000 (at 60)
|
|
payment st "inv-live" (cryptoPaid 300000 300000) (at 20)
|
|
let pendingOf :: Text -> IO (Text, Maybe LatePending)
|
|
pendingOf r = withTransaction st $ \db -> (r,) <$> latePending db (InvoiceId $ "inv-" <> r)
|
|
forM_ cases $ \(r, _, _, expected) -> pendingOf r `shouldReturn` (r, expected)
|
|
pendingOf "live" `shouldReturn` ("live", Nothing)
|
|
withTransaction st uncreditedLateInvoices `shouldReturn` map (InvoiceId . ("inv-" <>)) ["uncredited", "card", "settledlate"]
|
|
withTransaction st (\db -> lateCreditState db (InvoiceId "inv-part"))
|
|
`shouldReturn` Just
|
|
LateCreditState
|
|
{ lsInvoice = InvoiceId "inv-part",
|
|
lsRegistration = RegistrationId "part",
|
|
lsPhase = PUnpaid,
|
|
lsLive = False,
|
|
lsCard = False,
|
|
lsReceivedCents = 20000,
|
|
lsConfirmedCents = 0,
|
|
lsReceivedUnits = Just 300000,
|
|
lsConfirmedUnits = Just 100000,
|
|
lsInvoiceRate = Just "62500",
|
|
lsCreditedUnits = 100000,
|
|
lsCreditedCents = 1500,
|
|
lsCredits = 1,
|
|
lsPaymentCreatedAt = at 4
|
|
}
|
|
withTransaction st (\db -> lateCreditState db (InvoiceId "inv-plain")) `shouldReturn` Nothing
|
|
|
|
testExactUnits :: IO ()
|
|
testExactUnits = withServiceStore $ \st -> do
|
|
closedWith st "x" False (cryptoPaid 30000000 30000000) t0
|
|
creditLate st (lateCredit "x" 1 (Just 10000000) 6000) `shouldReturn` True
|
|
creditLate st (lateCredit "x" 2 (Just 20000000) 12000) `shouldReturn` True
|
|
withTransaction st (\db -> latePending db (InvoiceId "inv-x")) `shouldReturn` Nothing
|
|
withTransaction st uncreditedLateInvoices `shouldReturn` []
|
|
fmap lsCreditedUnits <$> withTransaction st (\db -> lateCreditState db (InvoiceId "inv-x")) `shouldReturn` Just 30000000
|
|
|
|
testConfirmedBeforeCents :: IO ()
|
|
testConfirmedBeforeCents = withServiceStore $ \st -> do
|
|
closedWith st "x" False Paid {pCents = 0, pConfirmedCents = 0, pCrypto = Just "0", pUnits = Just 1000000000000, pConfirmedUnits = Just 1000000000000, pInFull = True} t0
|
|
withTransaction st (\db -> latePending db (InvoiceId "inv-x")) `shouldReturn` Just LPUncredited
|
|
withTransaction st uncreditedLateInvoices `shouldReturn` [InvoiceId "inv-x"]
|
|
|
|
testNoConfirmedFigure :: IO ()
|
|
testNoConfirmedFigure = withServiceStore $ \st -> do
|
|
closedWith st "x" False (cryptoPaid 300000 0) {pConfirmedUnits = Nothing} t0
|
|
withTransaction st (\db -> latePending db (InvoiceId "inv-x")) `shouldReturn` Just LPUnconfirmed
|
|
|
|
testLateAlerts :: IO ()
|
|
testLateAlerts = withServiceStore $ \st -> do
|
|
closedWith st "old" False (cryptoPaid 300000 0) t0
|
|
closedWith st "card" True (cardPaid 5000 0) (at 1)
|
|
closedWith st "done" True (cardPaid 5000 5000) (at 2)
|
|
creditLate st (lateCredit "done" 1 Nothing 5000) `shouldReturn` True
|
|
closedWith st "new" False (cryptoPaid 300000 0) (at 120)
|
|
let due :: IO [LateAlert]
|
|
due = withTransaction st $ \db -> lateAlertDue db (at 60)
|
|
marks :: LateAlert -> (InvoiceId, LatePending, Maybe Int64, Maybe UTCTime, Maybe Int64)
|
|
marks LateAlert {laState = LateCreditState {lsInvoice}, laPending, laReceived, laAlertedAt, laAlertedReceived} = (lsInvoice, laPending, laReceived, laAlertedAt, laAlertedReceived)
|
|
claim :: LateAlert -> UTCTime -> IO Bool
|
|
claim alert t = withTransaction st $ \db -> claimLateAlert db alert t
|
|
[old, card] <- due
|
|
map marks [old, card]
|
|
`shouldBe` [(InvoiceId "inv-old", LPUnconfirmed, Just 300000, Nothing, Nothing), (InvoiceId "inv-card", LPUnconfirmed, Just 5000, Nothing, Nothing)]
|
|
claim old (at 61) `shouldReturn` True
|
|
claim old (at 61) `shouldReturn` False
|
|
claim card (at 61) `shouldReturn` True
|
|
due `shouldReturn` []
|
|
withTransaction st $ \db -> DB.execute db "UPDATE sx_badge_service_payments SET received_units = ? WHERE invoice_id = ?" (400000 :: Int64, "inv-old" :: Text)
|
|
[more] <- due
|
|
marks more `shouldBe` (InvoiceId "inv-old", LPUnconfirmed, Just 400000, Just (at 61), Just 300000)
|
|
claim more (at 62) `shouldReturn` True
|
|
claim more (at 63) `shouldReturn` False
|
|
due `shouldReturn` []
|
|
map marks <$> withTransaction st (\db -> lateAlertDue db (at 180)) `shouldReturn` [(InvoiceId "inv-new", LPUnconfirmed, Just 300000, Nothing, Nothing)]
|
|
|
|
testUncountedAlertRepeats :: IO ()
|
|
testUncountedAlertRepeats = withServiceStore $ \st -> do
|
|
closedWith st "x" False (cryptoPaid 0 0) {pCents = 5000, pUnits = Nothing} t0
|
|
let due :: IO [LateAlert]
|
|
due = withTransaction st $ \db -> lateAlertDue db (at 60)
|
|
[alert] <- due
|
|
(laPending alert, laReceived alert) `shouldBe` (LPUncounted, Just 5000)
|
|
withTransaction st (\db -> claimLateAlert db alert (at 61)) `shouldReturn` True
|
|
due `shouldReturn` []
|
|
withTransaction st $ \db -> DB.execute db "UPDATE sx_badge_service_payments SET amount = ? WHERE invoice_id = ?" (6000 :: Int, "inv-x" :: Text)
|
|
map (\LateAlert {laPending, laReceived, laAlertedReceived} -> (laPending, laReceived, laAlertedReceived)) <$> due `shouldReturn` [(LPUncounted, Just 6000, Just 5000)]
|
|
|
|
-- turns
|
|
|
|
testBalanceTurn :: IO ()
|
|
testBalanceTurn = withServiceStore $ \st -> do
|
|
turns <- newTurns
|
|
source st "s" 10000
|
|
inside <- newEmptyMVar
|
|
proceed <- newEmptyMVar
|
|
let src = RegistrationId "s"
|
|
firstHolder <- async $ withBalanceTurn turns src $ do
|
|
putMVar inside ()
|
|
takeMVar proceed
|
|
holder st "h1" "s" 10000 (at 60)
|
|
takeMVar inside
|
|
secondHolder <- async $ withBalanceTurn turns src $ holder st "h2" "s" 10000 (at 60)
|
|
timeout 5000000 (atomically $ balanceTurnCount turns src >>= check . (== 2)) `shouldReturn` Just ()
|
|
putMVar proceed ()
|
|
wait firstHolder `shouldReturn` Right ()
|
|
wait secondHolder `shouldReturn` Left RRBalanceChanged
|
|
timeout 5000000 (withLabelTurn turns (labelHash "alicebakery") $ withBalanceTurn turns src $ pure ()) `shouldReturn` Just ()
|
|
turnsCount turns `shouldReturn` 0
|
|
|
|
#if defined(dbPostgres)
|
|
-- | Both transactions begun before either writes, so the second writer waits for the first's row lock and re-reads its condition.
|
|
together :: DBStore -> DBStore -> (DB.Connection -> IO a) -> (DB.Connection -> IO b) -> IO (a, b)
|
|
together st1 st2 w1 w2 = do
|
|
begun <- newTVarIO (0 :: Int)
|
|
let run :: DBStore -> (DB.Connection -> IO c) -> IO c
|
|
run st w = withTransaction st $ \db -> do
|
|
atomically $ modifyTVar' begun (+ 1)
|
|
atomically $ readTVar begun >>= check . (== 2)
|
|
w db
|
|
concurrently (run st1 w1) (run st2 w2)
|
|
|
|
testConcurrentWrites :: IO ()
|
|
testConcurrentWrites = withServiceStores $ \open -> do
|
|
st <- open
|
|
other <- open
|
|
forM_ [1 .. 25 :: Int] $ \n -> do
|
|
let tag = T.pack (show n)
|
|
spendSrc = "spend" <> tag
|
|
heldSrc = "held" <> tag
|
|
lateSrc = "late" <> tag
|
|
source st spendSrc 10000
|
|
(spent, refundedSpend) <-
|
|
together st other (\db -> createBalanceRegistration db (newReg ("c" <> tag) ownerA (fieldsN $ "c" <> tag)) (RegistrationId spendSrc) 6000 10000 t0) (\db -> refundBalance db (RegistrationId spendSrc) 10000 t0)
|
|
(n, isRight spent, refundedSpend) `shouldSatisfy` (\(_, a, b) -> a /= b)
|
|
fmap (\(amount, _, _, _) -> amount) <$> figures st spendSrc t0 `shouldReturn` Just (if refundedSpend then 0 else 4000)
|
|
invariantHolds st spendSrc
|
|
source st heldSrc 10000
|
|
holder st ("h" <> tag) heldSrc 10000 (at 60) `shouldReturn` Right ()
|
|
h <- reg st ("h" <> tag)
|
|
(n,) <$> together st other (`settleHolder` h) (\db -> refundBalance db (RegistrationId heldSrc) 10000 t0) `shouldReturn` (n, (True, False))
|
|
figures st heldSrc t0 `shouldReturn` Just (0, 10000, 0, Nothing)
|
|
invariantHolds st heldSrc
|
|
source st lateSrc 10000
|
|
(credited, refundedLate) <- together st other (\db -> creditLatePayment db (lateCredit lateSrc 1 (Just 50000) 3000) t0) (\db -> refundBalance db (RegistrationId lateSrc) 10000 t0)
|
|
credited `shouldBe` True
|
|
(n,) <$> figures st lateSrc t0 `shouldReturn` (n, Just $ if refundedLate then (3000, 13000, 10000, Nothing) else (13000, 13000, 0, Nothing))
|
|
invariantHolds st lateSrc
|
|
#endif
|