exchangealgebra-0.6.0.0: test/Transfer/BalanceSpec.hs
{-# LANGUAGE ScopedTypeVariables #-}
-- | Exact balance laws for Accounting transfers and their Journal lifting.
-- This Accounting test suite uses independent Rational observations and a
-- sequential posting rewrite oracle. Read the fixtures before 'runTests'.
module Transfer.BalanceSpec (runTests) where
import ExchangeAlgebra.Accounting.Account
import ExchangeAlgebra.Accounting.Exchange
import Control.Monad (forM_, unless)
import Data.List (sort)
import qualified Data.HashMap.Strict as HashMap
import qualified Data.Map.Strict as Map
import Data.Maybe (isJust)
import Data.Proxy (Proxy(..))
import System.Exit (exitFailure)
import Test.QuickCheck hiding (collect, label)
import ExchangeAlgebra.Algebra hiding (base, filter, map)
import qualified ExchangeAlgebra.Algebra as Algebra
import qualified ExchangeAlgebra.Algebra.Internal as Internal
import ExchangeAlgebra.Algebra.Value (MoneyDecimal)
import ExchangeAlgebra.Accounting.Closing (ClosingSide(..), closingEntries, closingSide)
import qualified ExchangeAlgebra.Accounting.Transfer as Transfer
import qualified ExchangeAlgebra.Journal.Core as Journal
import ExchangeAlgebra.Journal.Core ((.|))
-- | Two-coordinate base using the library's existing account instance.
type TestBase = HatBase (CountUnit, AccountTitles)
-- | A posting retains its value and complete base for raw pair observations.
type Posting v = (v, TestBase)
-- | A row fixture has independent source and destination account titles.
type AccountMove = (AccountTitles, AccountTitles)
-- | Stop the suite immediately on a failed law or fixture.
assertLaw :: String -> Bool -> IO ()
assertLaw label passed = unless passed $ do
putStrLn ("[FAIL] " ++ label)
exitFailure
-- | Require successful validation without using partial extraction.
requireRight :: Show e => String -> Either e a -> IO a
requireRight _ (Right value) = pure value
requireRight label (Left failure) = do
putStrLn ("[FAIL] " ++ label ++ ": " ++ show failure)
exitFailure
-- | Run properties with their law identifiers in test output.
checkLaw :: Testable property => String -> property -> IO ()
checkLaw label lawProperty = do
putStrLn ("[LAW] " ++ label)
result <- quickCheckWithResult stdArgs { maxSuccess = 100 } lawProperty
assertLaw label (isSuccess result)
-- | Observe every raw posting, preserving repetitions and traversal order.
postings :: forall v. HatVal v => Alg v TestBase -> [Posting v]
postings = foldEntries append []
where
append :: [Posting v] -> v -> TestBase -> [Posting v]
append entries value base = entries ++ [(value, base)]
-- | Read one finite value as an exact Rational through its accumulator.
exactValue :: HatVal v => v -> Maybe Rational
exactValue value = exactAccum (addAccum value emptyAccum)
-- | Observe exact Not-minus-Hat balances, omitting zero coordinates.
exactBalances :: forall v. HatVal v => Alg v TestBase -> Maybe (Map.Map TestBase Rational)
exactBalances ledger = fmap (Map.filter (/= 0)) $ foldl' collect (Just Map.empty)
(postings ledger)
where
collect :: Maybe (Map.Map TestBase Rational)
-> Posting v
-> Maybe (Map.Map TestBase Rational)
collect previous (value, side :< coordinates) = do
balances <- previous
amount <- exactValue value
let signed :: Rational
signed = case side of
Not -> amount
Hat -> negate amount
HatNot -> 0
pure (Map.insertWith (+) (Not :< coordinates) signed balances)
-- | Sum selected debit or credit values without any floating-point addition.
exactTotal :: HatVal v => Alg v TestBase -> Maybe Rational
exactTotal ledger = fmap sum $ traverse (exactValue . fst) (postings ledger)
-- | Construct a wildcard-unit row without Hat/Not in its endpoints.
moveRow :: AccountMove -> Transfer.TransferRow TestBase
moveRow (source, target) = (wildcard, source) Transfer..-> (wildcard, target)
Transfer.|% Transfer.Relabel
-- | Build the ordered two-stage chain used by the rewrite oracle.
chainMoves :: [AccountMove]
chainMoves = [(Cash, Deposits), (Deposits, Sales)]
-- | Generate valid redundant ledgers with positive and negative net balances.
genLedger :: HatVal v => Gen (Alg v TestBase)
genLedger = do
count <- chooseInt (0, 35)
entries <- vectorOf count $ do
amount <- elements [1, 2, 3, 1 / 10, 1 / 5, 9007199254740992]
side <- elements [Hat, Not]
unit <- elements [Yen, Dollar, wildcard]
title <- elements [Cash, Deposits, Sales, Products]
pure (fromRational amount .@ side :< (unit, title))
pure (Algebra.fromList entries)
-- | Rewrite each posting row by row, independently of the transfer machinery.
rewriteLedger :: forall v. HatVal v => [AccountMove] -> Alg v TestBase -> Alg v TestBase
rewriteLedger moves ledger = Algebra.fromList
[value .@ foldl' rewrite base moves | (value, base) <- postings ledger]
where
rewrite :: TestBase -> AccountMove -> TestBase
rewrite base@(side :< (unit, title)) (source, target)
| title /= source = base
| whichSide (Not :< (unit, source)) == whichSide (Not :< (unit, target)) =
side :< (unit, target)
| otherwise = revHat (side :< (unit, target))
-- | Check the pair structure without comparing rounded totals.
validPairs :: forall v. HatVal v => [Posting v] -> Bool
validPairs entries = sideValues (whichSide (Not :< (Yen, Cash) :: TestBase))
== sideValues (whichSide (Hat :< (Yen, Cash) :: TestBase))
where
sideValues :: Side -> [Maybe Rational]
sideValues side = sort [exactValue value | (value, base) <- entries, whichSide base == side]
-- | T1-T5 use exact Rational observations, including nonrepresentable Double nets.
propRelabel :: forall v. HatVal v => Proxy v -> Property
propRelabel _ = forAll (genLedger :: Gen (Alg v TestBase)) $ \ledger ->
case Transfer.mkTransferTable (map moveRow chainMoves) of
Left failure -> counterexample (show failure) False
Right table -> case Transfer.transferEntries table ledger of
Left failure -> counterexample (show failure) False
Right entries ->
let combined :: Alg v TestBase
combined = ledger .+ entries
sourceBalances :: Maybe (Map.Map TestBase Rational)
sourceBalances = fmap (Map.filterWithKey isSource) (exactBalances combined)
isSource :: TestBase -> Rational -> Bool
isSource (_ :< (_, title)) _ = title `elem` [Cash, Deposits]
positive :: Posting v -> Bool
positive (value, _) = value > 0 && not (isErrorValue value)
in conjoin
[ counterexample "T1 exact debit/credit and equal pair values" $
exactTotal (decL entries) == exactTotal (decR entries)
&& validPairs (postings entries)
, counterexample "T2 finite positive complete pairs" $
all positive (postings entries)
, counterexample "T3 exact source closure" $
sourceBalances == Just Map.empty
, counterexample "T4 exact empty second result" $
Transfer.transferEntries table combined == Right Zero
, counterexample "T5 sequential independent rewrite" $
exactBalances combined == exactBalances (rewriteLedger chainMoves ledger)
]
-- | T6 covers every ordered distinct pair of registry accounts and both sides.
testAllAccountPairs :: forall v. HatVal v => Proxy v -> IO ()
testAllAccountPairs _ = forM_ concreteAccountTitles $ \source ->
forM_ concreteAccountTitles $ \target -> unless (source == target) $
forM_ [Hat, Not] $ \side -> do
table <- requireRight "T6 table" $ Transfer.mkTransferTable [moveRow (source, target)]
entries <- requireRight "T6 entries" $ Transfer.transferEntries table
(3 .@ side :< (Yen, source) :: Alg v TestBase)
let expectedSide :: Hat
expectedSide
| whichSide (Not :< (Yen, source)) == whichSide (Not :< (Yen, target)) =
side
| otherwise = hat (revHat (side :< (Yen, target)))
expected :: Alg v TestBase
expected = 3 .@ revHat (side :< (Yen, source))
.+ 3 .@ expectedSide :< (Yen, target)
assertLaw "T6 registry pair Hat/Not and T2 one representable pair" $
sort (postings entries) == sort (postings expected)
-- | T7 first checks the whole closing registry before testing any transfers.
testClosingHomeSides :: IO ()
testClosingHomeSides = forM_ concreteAccountTitles $ \title -> case closingSide title of
Nothing -> pure ()
Just decision -> do
let sameHome :: Bool
sameHome = whichSide (Not :< (Yen, title) :: TestBase)
== whichSide (Not :< (Yen, RetainedEarnings) :: TestBase)
assertLaw ("T7 STOP: closing/home mismatch " ++ show title) $
(decision == ClosingKeep) == sameHome
-- | T7 compares raw posting multisets over all eligible closing accounts.
testClosingMultiset :: forall v. HatVal v => Proxy v -> IO ()
testClosingMultiset _ = do
let titles :: [AccountTitles]
titles = filter (isJust . closingSide) concreteAccountTitles
ledger :: Alg v TestBase
ledger = Algebra.fromList
[ amount .@ side :< (unit, title)
| title <- titles
, unit <- [Yen, Dollar, wildcard]
, (amount, side) <- [(0.1, Not), (0.2, Not), (0.05, Hat)]
]
table <- requireRight "T7 table" $ Transfer.mkTransferTable
[moveRow (title, RetainedEarnings) | title <- titles]
closing <- requireRight "T7 closing" (closingEntries ledger)
entries <- requireRight "T7 transfer" (Transfer.transferEntries table ledger)
assertLaw "T7 closing raw multiset" $ sort (postings closing) == sort (postings entries)
-- | T2 and T3 pin the Double balance that cannot fit in a single posting.
testDoubleSplit :: IO ()
testDoubleSplit = do
table <- requireRight "T2 split table" $ Transfer.mkTransferTable [moveRow (Cash, Sales)]
let ledger :: Alg Double TestBase
ledger = 9007199254740992 .@ Not :< (Yen, Cash) .+ 1 .@ Not :< (Yen, Cash)
entries <- requireRight "T2 split entries" (Transfer.transferEntries table ledger)
assertLaw "T2 split balance has two exact pairs" $
length (postings entries) == 4 && validPairs (postings entries)
assertLaw "T3 split residual is exactly zero" $
exactBalances (ledger .+ entries) == Just (Map.singleton (Not :< (Yen, Sales))
(-9007199254740993))
-- | T8 fixes once-rounded shares, zero results, and non-idempotent portions.
testPortions :: IO ()
testPortions = do
let row :: Transfer.TransferRow TestBase
row = (wildcard, Cash) Transfer..-> (wildcard, Deposits) Transfer.|% Transfer.Portion 1 3
one :: Alg MoneyDecimal TestBase
one = 1 .@ Not :< (Yen, Cash)
table <- requireRight "T8 thirds" (Transfer.mkTransferTable [row])
assertLaw "T8 decimal third is unroundable" $
Transfer.transferEntries table one
== Left (Transfer.UnroundableAmount row (Not :< (Yen, Cash)))
rounded <- requireRight "T8 cents" $
Transfer.transferEntriesWith (ToMultipleOf 0.01) table one
assertLaw "T8 cents leave exactly 0.67" $
exactBalances (one .+ rounded) == Just (Map.fromList
[(Not :< (Yen, Cash), 67 / 100), (Not :< (Yen, Deposits), 33 / 100)])
thirds <- requireRight "T8 representable third" $
Transfer.transferEntries table (3 .@ Not :< (Yen, Cash) :: Alg MoneyDecimal TestBase)
assertLaw "T8 three divided by three moves one" $
exactBalances thirds == Just (Map.fromList
[(Not :< (Yen, Cash), -1), (Not :< (Yen, Deposits), 1)])
half <- requireRight "T8 half table" $ Transfer.mkTransferTable
[(wildcard, Cash) Transfer..-> (wildcard, Deposits) Transfer.|% Transfer.Portion 1 2]
first <- requireRight "T8 first half" (Transfer.transferEntries half one)
second <- requireRight "T8 second half" (Transfer.transferEntries half (one .+ first))
assertLaw "T8 portion is not idempotent" (second /= Zero)
zero <- requireRight "T2 rounded zero" $
Transfer.transferEntriesWith (ToMultipleOf 1) table one
assertLaw "T2 rounded zero emits no pair" (zero == Zero)
-- | T8 checks exact half shares on both supported value instances.
propHalfPortion :: forall v. HatVal v => Proxy v -> Property
propHalfPortion _ = forAll (chooseInteger (1, 1000000)) $ \amount ->
let row :: Transfer.TransferRow TestBase
row = (wildcard, Cash) Transfer..-> (wildcard, Sales) Transfer.|% Transfer.Portion 1 2
ledger :: Alg v TestBase
ledger = fromInteger amount .@ Hat :< (Dollar, Cash)
halfAmount :: Rational
halfAmount = fromInteger amount / 2
in case Transfer.mkTransferTable [row] of
Left failure -> counterexample (show failure) False
Right table -> case Transfer.transferEntries table ledger of
Left failure -> counterexample (show failure) False
Right entries -> counterexample "T8 exact remainder and side-preserving target" $
exactBalances (ledger .+ entries) == Just (Map.fromList
[(Not :< (Dollar, Cash), negate halfAmount),
(Not :< (Dollar, Sales), halfAmount)])
&& length (postings entries) == 2
-- | Wildcards in source patterns match, while concrete queries miss ledger wildcards.
testWildcardMatching :: forall v. HatVal v => Proxy v -> IO ()
testWildcardMatching _ = do
let ledger :: Alg v TestBase
ledger = 7 .@ Not :< (wildcard, Cash)
concreteRow :: Transfer.TransferRow TestBase
concreteRow = (Yen, Cash) Transfer..-> (wildcard, Deposits) Transfer.|% Transfer.Relabel
concrete <- requireRight "T9 concrete pattern" $ Transfer.mkTransferTable [concreteRow]
assertLaw "T9 concrete source misses wildcard ledger coordinate" $
Transfer.transferEntries concrete ledger == Right Zero
broad <- requireRight "T9 wildcard pattern" $
Transfer.mkTransferTable [moveRow (Cash, Deposits)]
entries <- requireRight "T9 wildcard ledger selection" $ Transfer.transferEntries broad ledger
assertLaw "T9 wildcard destination keeps literal ledger wildcard" $
exactBalances (ledger .+ entries)
== Just (Map.singleton (Not :< (wildcard, Deposits)) 7)
-- | T9 rejects backward edges, cycles, self edges, and wildcard backflow.
testOrder :: IO ()
testOrder = do
let forward :: [Transfer.TransferRow TestBase]
forward = map moveRow [(Cash, Deposits), (Deposits, Securities)]
backward :: [Transfer.TransferRow TestBase]
backward = reverse forward
rejectsBackward :: [Transfer.TransferRow TestBase] -> Bool
rejectsBackward rows = case Transfer.mkTransferTable rows of
Left (Transfer.BackwardFlow _ _) -> True
_ -> False
wildcardBackflow :: [Transfer.TransferRow TestBase]
wildcardBackflow =
[ (Yen, Deposits) Transfer..-> (wildcard, Securities) Transfer.|% Transfer.Relabel
, (wildcard, Cash) Transfer..-> (wildcard, Deposits) Transfer.|% Transfer.Relabel
]
table <- requireRight "T9 forward" (Transfer.mkTransferTable forward)
assertLaw "T9 table preserves written order" (Transfer.tableRows table == forward)
assertLaw "T9 backward" (rejectsBackward backward)
assertLaw "T9 self" (rejectsBackward [moveRow (Cash, Cash)])
assertLaw "T9 cycle" (rejectsBackward (map moveRow [(Cash, Deposits), (Deposits, Cash)]))
assertLaw "T9 wildcard backward regression" (rejectsBackward wildcardBackflow)
wildcardForward <- requireRight "T9 wildcard forward regression" $
Transfer.mkTransferTable (reverse wildcardBackflow)
let wildcardLedger :: Alg Double TestBase
wildcardLedger = 0.1 .@ Not :< (Yen, Cash)
.+ 0.2 .@ Not :< (Yen, Cash)
.+ 3 .@ Hat :< (Dollar, Cash)
.+ 2 .@ Not :< (Yen, Deposits)
wildcardEntries <- requireRight "T9 wildcard forward entries" $
Transfer.transferEntries wildcardForward wildcardLedger
assertLaw "T4 wildcard forward second result is exactly empty" $
Transfer.transferEntries wildcardForward (wildcardLedger .+ wildcardEntries) == Right Zero
entries <- requireRight "T9 chain entries" $ Transfer.transferEntries table
(7 .@ Not :< (Yen, Cash) :: Alg MoneyDecimal TestBase)
assertLaw "T9 forward chain reaches final account" $
exactBalances (7 .@ Not :< (Yen, Cash) .+ entries)
== Just (Map.singleton (Not :< (Yen, Securities)) 7)
-- | T9 fixes validation priority and the written j-then-i candidate order.
testTableFailureOrder :: IO ()
testTableFailureOrder = do
let invalidFirst :: Transfer.TransferRow TestBase
invalidFirst = (Yen, Cash) Transfer..-> (Yen, Cash) Transfer.|% Transfer.Portion 0 1
invalidLater :: Transfer.TransferRow TestBase
invalidLater = (Yen, Cash) Transfer..-> (Yen, Cash) Transfer.|% Transfer.Portion 2 1
overlapFirst :: Transfer.TransferRow TestBase
overlapFirst = (Yen, Cash) Transfer..-> (Dollar, Cash) Transfer.|% Transfer.Relabel
overlapSecond :: Transfer.TransferRow TestBase
overlapSecond = (Dollar, Cash) Transfer..-> (Yen, Cash) Transfer.|% Transfer.Relabel
overlapThird :: Transfer.TransferRow TestBase
overlapThird = (wildcard, Cash) Transfer..-> (wildcard, Sales) Transfer.|% Transfer.Relabel
overlapFourth :: Transfer.TransferRow TestBase
overlapFourth = (wildcard, Cash) Transfer..-> (wildcard, Securities)
Transfer.|% Transfer.Relabel
backwardFirst :: Transfer.TransferRow TestBase
backwardFirst = (Yen, Cash) Transfer..-> (Yen, Deposits) Transfer.|% Transfer.Relabel
backwardSecond :: Transfer.TransferRow TestBase
backwardSecond = (Dollar, Cash) Transfer..-> (Dollar, Deposits) Transfer.|% Transfer.Relabel
backwardThird :: Transfer.TransferRow TestBase
backwardThird = (wildcard, Deposits) Transfer..-> (wildcard, Cash)
Transfer.|% Transfer.Relabel
backwardFourth :: Transfer.TransferRow TestBase
backwardFourth = (Yen, Securities) Transfer..-> (Yen, Cash) Transfer.|% Transfer.Relabel
assertLaw "T9 invalid portion precedes overlaps and backflow, first invalid row wins" $
Transfer.mkTransferTable [overlapFirst, overlapSecond, invalidFirst, invalidLater]
== Left (Transfer.InvalidPortion invalidFirst)
assertLaw "T9 overlap precedes backflow, first j then first i wins" $
Transfer.mkTransferTable [overlapFirst, overlapSecond, overlapThird, overlapFourth]
== Left (Transfer.OverlappingRows overlapFirst overlapThird)
assertLaw "T9 backflow first j then first i wins among multiple candidates" $
Transfer.mkTransferTable [backwardFirst, backwardSecond, backwardThird, backwardFourth]
== Left (Transfer.BackwardFlow backwardFirst backwardThird)
-- | T10 supplies several simultaneous failures and shuffles their raw postings.
propFailureOrder :: Property
propFailureOrder = forAll (shuffle fixtures) $ \permuted ->
case Transfer.mkTransferTable [row] of
Left failure -> counterexample (show failure) False
Right table -> conjoin
[ counterexample "T10 nonfinite precedes negative on the same base" $
apply table permuted
== Left (Transfer.InvalidPosting invalidBase NonFiniteValue)
, counterexample "T10 unclassified precedes unroundable" $
apply table (filter isValidSideValue permuted)
== Left (Transfer.UnclassifiedAccount unclassifiedBase)
, counterexample "T10 final failure is unroundable" $
apply table (filter isClassifiedValid permuted)
== Left (Transfer.UnroundableAmount row validBase)
]
where
row :: Transfer.TransferRow TestBase
row = (Yen, wildcard) Transfer..-> (Dollar, Deposits) Transfer.|% Transfer.Portion 1 3
invalidBase :: TestBase
invalidBase = Not :< (Yen, Cash)
unclassifiedBase :: TestBase
unclassifiedBase = Not :< (Yen, wildcard)
validBase :: TestBase
validBase = Not :< (Yen, Products)
fixtures :: [Posting Double]
fixtures =
[ (-2, invalidBase)
, (0 / 0, invalidBase)
, (1 / 0, Not :< (Yen, Sales))
, (1, unclassifiedBase)
, (1, validBase)
]
isValidSideValue :: Posting Double -> Bool
isValidSideValue (value, _) = not (isErrorValue value)
isClassifiedValid :: Posting Double -> Bool
isClassifiedValid posting@(_, base) = isValidSideValue posting
&& getAccountTitle base /= wildcard
apply :: Transfer.TransferTable TestBase
-> [Posting Double]
-> Either (Transfer.TransferError TestBase) (Alg Double TestBase)
apply table entries = Transfer.transferEntriesWith (ToMultipleOf 0.01) table
(Algebra.fromList [value `Internal.Single` base | (value, base) <- entries])
-- | T10 rejects raw singleton wildcard sides before checking their scalar.
testWildcardFailure :: IO ()
testWildcardFailure = do
table <- requireRight "T10 wildcard table" $
Transfer.mkTransferTable [moveRow (Cash, Deposits)]
let base :: TestBase
base = HatNot :< (Yen, Cash)
forM_ [1, -1, 0 / 0] $ \value ->
assertLaw "T10 wildcard precedes invalid scalar on the same posting" $
Transfer.transferEntries table (Internal.Single value base :: Alg Double TestBase)
== Left (Transfer.WildcardPosting (Not :< (Yen, Cash)))
assertLaw "T10 zero wildcard contributes no failure" $
Transfer.transferEntries table (Internal.Single 0 base :: Alg Double TestBase) == Right Zero
-- | A stable target groups notes 1 and 2, while note 4 forms a second group.
groupNote :: Int -> Int
groupNote note
| note <= 3 = 3
| otherwise = 5
-- | J1 combines target groups before rounding and preserves other target groups.
testJournalGroups :: forall v. HatVal v => Proxy v -> IO ()
testJournalGroups _ = do
table <- requireRight "J1 table" $ Transfer.mkTransferTable
[(wildcard, Cash) Transfer..-> (wildcard, Deposits) Transfer.|% Transfer.Portion 1 3]
let ledger :: Journal.Journal Int v TestBase
ledger = (1 .@ Not :< (Yen, Cash) .| 1)
.+ (2 .@ Not :< (Yen, Cash) .| 2)
.+ (6 .@ Not :< (Dollar, Cash) .| 4)
entriesAt :: Int -> Journal.Journal Int v TestBase -> Alg v TestBase
entriesAt note = Journal.toAlg . Journal.filterWithNote (\actual _ -> actual == note)
result <- requireRight "J1 grouped exact thirds" $
Transfer.transferEntriesBy groupNote table ledger
forM_ [3, 5] $ \target -> do
expected <- requireRight "J1 independent group" $ Transfer.transferEntries table
(Journal.toAlg (Journal.filterWithNote (\note _ -> groupNote note == target) ledger))
assertLaw "J1 target entries equal independently grouped transfer" $
sort (postings (entriesAt target result)) == sort (postings expected)
let expectedThree :: Alg v TestBase
expectedThree = 1 .@ Hat :< (Yen, Cash) .+ 1 .@ Not :< (Yen, Deposits)
expectedFive :: Alg v TestBase
expectedFive = 2 .@ Hat :< (Dollar, Cash) .+ 2 .@ Not :< (Dollar, Deposits)
assertLaw "J1 target three has the manually specified one-unit pair" $
sort (postings (entriesAt 3 result)) == sort (postings expectedThree)
assertLaw "J1 target five has the manually specified two-unit pair" $
sort (postings (entriesAt 5 result)) == sort (postings expectedFive)
assertLaw "J1 original notes absent from added entries" $
all (Algebra.isZero . (`entriesAt` result)) [1, 2, 4]
-- | J2 rejects moving input targets before evaluating invalid posting values.
testJournalMoves :: forall v. HatVal v => Proxy v -> IO ()
testJournalMoves _ = do
table <- requireRight "J2 table" $ Transfer.mkTransferTable [moveRow (Cash, Deposits)]
let ledger :: Journal.Journal Int v TestBase
ledger = (1 .@ Not :< (Yen, Cash) .| 1)
.+ (Internal.Single (-1) (Not :< (Yen, Cash)) .| 2)
next :: Int -> Int
next = (+ 1)
assertLaw "J2 note movement checked before invalid values" $
case Transfer.transferEntriesBy next table ledger of
Left (Transfer.TransferNoteMoves 2 3) -> True
_ -> False
accepted <- requireRight "J2 absent next-period target" $ Transfer.transferEntriesBy next table
(1 .@ Not :< (Yen, Cash) .| 1 :: Journal.Journal Int v TestBase)
assertLaw "J2 absent target accepted" $
exactBalances (Journal.toAlg accepted) == Just (Map.fromList
[(Not :< (Yen, Cash), -1), (Not :< (Yen, Deposits), 1)])
-- | J3 repeats a Relabel transfer after its target notes have been added.
propJournalIdempotence :: forall v. HatVal v => Proxy v -> Property
propJournalIdempotence _ = forAll (genLedger :: Gen (Alg v TestBase)) $ \ledger ->
case Transfer.mkTransferTable (map moveRow chainMoves) of
Left failure -> counterexample (show failure) False
Right table ->
let journal :: Journal.Journal Int v TestBase
journal = ledger .| 1
stableTarget :: Int -> Int
stableTarget _ = 3
in case Transfer.transferEntriesBy stableTarget table journal of
Left failure -> counterexample (show failure) False
Right result -> counterexample "J3 second result is exactly empty" $
case Transfer.transferEntriesBy stableTarget table (journal .+ result) of
Right repeated -> HashMap.null (Journal.toMap repeated)
Left _ -> False
-- | Register the gate, both value instances, and all balance and Journal laws.
runTests :: IO ()
runTests = do
testClosingHomeSides
checkLaw "T1-T5 Double" (propRelabel (Proxy :: Proxy Double))
checkLaw "T1-T5 MoneyDecimal" (propRelabel (Proxy :: Proxy MoneyDecimal))
testAllAccountPairs (Proxy :: Proxy Double)
testAllAccountPairs (Proxy :: Proxy MoneyDecimal)
testClosingMultiset (Proxy :: Proxy Double)
testClosingMultiset (Proxy :: Proxy MoneyDecimal)
testDoubleSplit
testPortions
checkLaw "T8 Double half share" (propHalfPortion (Proxy :: Proxy Double))
checkLaw "T8 MoneyDecimal half share" (propHalfPortion (Proxy :: Proxy MoneyDecimal))
testWildcardMatching (Proxy :: Proxy Double)
testWildcardMatching (Proxy :: Proxy MoneyDecimal)
testOrder
testTableFailureOrder
checkLaw "T10 deterministic failure precedence" propFailureOrder
testWildcardFailure
testJournalGroups (Proxy :: Proxy Double)
testJournalGroups (Proxy :: Proxy MoneyDecimal)
testJournalMoves (Proxy :: Proxy Double)
testJournalMoves (Proxy :: Proxy MoneyDecimal)
checkLaw "J3 Double" (propJournalIdempotence (Proxy :: Proxy Double))
checkLaw "J3 MoneyDecimal" (propJournalIdempotence (Proxy :: Proxy MoneyDecimal))
putStrLn "[PASS] T1-T10 and J1-J3 exact transfer laws"