diff options
| author | Somhairle H. Marisol <[email protected]> | 2026-09-22 13:03:36 +0800 |
|---|---|---|
| committer | Somhairle H. Marisol <[email protected]> | 2026-09-22 13:03:36 +0800 |
| commit | 2dedc30358d1724f60ab646b7a24e93f3672dff1 (patch) | |
| tree | 515258bf4b3e1546fdf97c481e12740c573ac7c8 /tests/FundLab.Api.Tests/CashReconciliationTests.fs | |
| parent | 39965954968436911b32e774d32d3cca9527e4e2 (diff) | |
| download | fund-lab-2dedc30358d1724f60ab646b7a24e93f3672dff1.tar.gz | |
Debit available cash on manual stock and bond buys with ledger events (3d-34)
Diffstat (limited to 'tests/FundLab.Api.Tests/CashReconciliationTests.fs')
| -rw-r--r-- | tests/FundLab.Api.Tests/CashReconciliationTests.fs | 144 |
1 files changed, 138 insertions, 6 deletions
diff --git a/tests/FundLab.Api.Tests/CashReconciliationTests.fs b/tests/FundLab.Api.Tests/CashReconciliationTests.fs index d8dc7bb..6fb761e 100644 --- a/tests/FundLab.Api.Tests/CashReconciliationTests.fs +++ b/tests/FundLab.Api.Tests/CashReconciliationTests.fs @@ -83,6 +83,18 @@ type CashReconciliationTests(fixture: PostgresFixture) = | CapitalDepositWriteResult.CapitalDepositReplayed _ -> () | other -> failwithf "unexpected deposit result: %A" other + let buyStock fundId code quantity price idempotencyKey = + match + repository().CreateStockTrade( + idempotencyKey, + fundId, + { InstrumentCode = code; StockName = Some "贵州茅台"; Quantity = quantity; Price = price } + ) + with + | StockTradeWriteResult.StockTradeCreated _ + | StockTradeWriteResult.StockTradeReplayed _ -> () + | other -> failwithf "unexpected stock trade result: %A" other + let buyStockDebit fundId code quantity price idempotencyKey = match repository().CreateStockTrade( @@ -96,6 +108,19 @@ type CashReconciliationTests(fixture: PostgresFixture) = | StockTradeWriteResult.StockTradeReplayed _ -> () | other -> failwithf "unexpected stock trade result: %A" other + let buyStockNoDebit fundId code quantity price idempotencyKey = + match + repository().CreateStockTrade( + idempotencyKey, + fundId, + { InstrumentCode = code; StockName = Some "贵州茅台"; Quantity = quantity; Price = price }, + debitAvailableCash = false + ) + with + | StockTradeWriteResult.StockTradeCreated _ + | StockTradeWriteResult.StockTradeReplayed _ -> () + | other -> failwithf "unexpected stock trade result: %A" other + let sellStock fundId code quantity price idempotencyKey = match repository().CreateStockSell( @@ -108,7 +133,7 @@ type CashReconciliationTests(fixture: PostgresFixture) = | StockSellWriteResult.StockSellReplayed _ -> () | other -> failwithf "unexpected stock sell result: %A" other - let buyBond fundId code quantity price = + let buyBondWith fundId code quantity price debitAvailableCash = match repository().CreateBondTrade( fixture.Key(sprintf "cash-recon-bond-%s" (Guid.NewGuid().ToString("N"))), @@ -126,12 +151,19 @@ type CashReconciliationTests(fixture: PostgresFixture) = ValueDate = None MaturityDate = None TradeDate = Some today - } + }, + debitAvailableCash = debitAvailableCash ) with | BondTradeWriteResult.BondTradeCreated _ -> () | other -> failwithf "unexpected bond trade result: %A" other + let buyBond fundId code quantity price = + buyBondWith fundId code quantity price true + + let buyBondNoDebit fundId code quantity price = + buyBondWith fundId code quantity price false + let recordBondCoupon fundId code amount idempotencyKey = match repository().RecordBondCashflow( @@ -229,14 +261,15 @@ type CashReconciliationTests(fixture: PostgresFixture) = let status, body = reconciliation fundId Assert.Equal(200, status) Assert.Contains("\"openingCash\":\"10000.00\"", body) - Assert.Contains("\"netInflow\":\"3450.00\"", body) - Assert.Contains("\"closingCash\":\"13450.00\"", body) - Assert.Contains("\"ledgerBalance\":\"13450.00\"", body) + Assert.Contains("\"netInflow\":\"2450.00\"", body) + Assert.Contains("\"closingCash\":\"12450.00\"", body) + Assert.Contains("\"ledgerBalance\":\"12450.00\"", body) Assert.Contains("\"difference\":\"0.00\"", body) Assert.Equal("5000.00", (sourceEntry body "capital_deposit").GetProperty("netAmount").GetString()) Assert.Equal("-1000.00", (sourceEntry body "stock_buy").GetProperty("netAmount").GetString()) Assert.Equal("400.00", (sourceEntry body "stock_sell").GetProperty("netAmount").GetString()) + Assert.Equal("-1000.00", (sourceEntry body "bond_buy").GetProperty("netAmount").GetString()) Assert.Equal("50.00", (sourceEntry body "bond_coupon").GetProperty("netAmount").GetString()) Assert.Equal("-1000.00", (sourceEntry body "subscription").GetProperty("netAmount").GetString()) @@ -274,7 +307,7 @@ type CashReconciliationTests(fixture: PostgresFixture) = deposit fundId 5000.00m (fixture.Key "cash-backfill-deposit") buyStockDebit fundId "600519" 100m 10.00m (fixture.Key "cash-backfill-buy") sellStock fundId "600519" 40m 10.00m (fixture.Key "cash-backfill-sell") - buyBond fundId "110075" 10m 100.00m + buyBondNoDebit fundId "110075" 10m 100.00m recordBondCoupon fundId "110075" 50.00m (fixture.Key "cash-backfill-coupon") subscribe fundId (seedInstrument ()) 1000.00m (fixture.Key "cash-backfill-subscribe") @@ -295,3 +328,102 @@ type CashReconciliationTests(fixture: PostgresFixture) = Assert.Equal(expected, ledgerEventCount fundId) Assert.Equal(0.00m, (repository().GetCashReconciliation fundId |> Result.defaultWith failwith).Difference) + + [<Fact>] + member _.``manual stock buy debits available cash and records a stock_buy event``() = + let fundId = createFund 10000.00m + buyStock fundId "600519" 100m 10.00m (fixture.Key "cash-manual-stock-buy") + + let status, body = reconciliation fundId + Assert.Equal(200, status) + Assert.Contains("\"openingCash\":\"10000.00\"", body) + Assert.Contains("\"closingCash\":\"9000.00\"", body) + Assert.Contains("\"ledgerBalance\":\"9000.00\"", body) + Assert.Contains("\"difference\":\"0.00\"", body) + Assert.Equal("-1000.00", (sourceEntry body "stock_buy").GetProperty("netAmount").GetString()) + Assert.Equal(1, eventCount body "stock_buy") + + [<Fact>] + member _.``manual bond buy debits available cash and records a bond_buy event``() = + let fundId = createFund 10000.00m + buyBond fundId "110075" 10m 100.00m + + let status, body = reconciliation fundId + Assert.Equal(200, status) + Assert.Contains("\"openingCash\":\"10000.00\"", body) + Assert.Contains("\"closingCash\":\"9000.00\"", body) + Assert.Contains("\"ledgerBalance\":\"9000.00\"", body) + Assert.Contains("\"difference\":\"0.00\"", body) + Assert.Equal("-1000.00", (sourceEntry body "bond_buy").GetProperty("netAmount").GetString()) + Assert.Equal(1, eventCount body "bond_buy") + + [<Fact>] + member _.``non-debiting buys leave cash and ledger untouched``() = + let fundId = createFund 10000.00m + buyStockNoDebit fundId "600519" 100m 10.00m (fixture.Key "cash-nodebit-stock") + buyBondNoDebit fundId "110075" 10m 100.00m + + let status, body = reconciliation fundId + Assert.Equal(200, status) + Assert.Contains("\"closingCash\":\"10000.00\"", body) + Assert.Contains("\"ledgerBalance\":\"10000.00\"", body) + Assert.Contains("\"difference\":\"0.00\"", body) + Assert.Equal(0, eventCount body "stock_buy") + Assert.Equal(0, eventCount body "bond_buy") + + [<Fact>] + member _.``manual stock buy beyond available cash is rejected and changes nothing``() = + let fundId = createFund 1000.00m + + let result = + repository().CreateStockTrade( + fixture.Key "cash-manual-stock-overspend", + fundId, + { InstrumentCode = "600519"; StockName = Some "贵州茅台"; Quantity = 100m; Price = 20.00m } + ) + + Assert.Equal( + StockTradeWriteResult.StockTradeInsufficientFunds "available cash is not enough for a stock purchase of 2000.00", + result + ) + + let status, body = reconciliation fundId + Assert.Equal(200, status) + Assert.Contains("\"closingCash\":\"1000.00\"", body) + Assert.Contains("\"difference\":\"0.00\"", body) + Assert.Equal(0, eventCount body "stock_buy") + + [<Fact>] + member _.``manual bond buy beyond available cash is rejected and changes nothing``() = + let fundId = createFund 1000.00m + + let result = + repository().CreateBondTrade( + fixture.Key "cash-manual-bond-overspend", + fundId, + { + InstrumentCode = "110075" + BondName = Some "南航转债" + Quantity = 10m + Price = 200.00m + CleanPrice = 200.00m + AccruedInterest = 0m + ParValue = 100m + SettlementDate = today + CouponRate = None + ValueDate = None + MaturityDate = None + TradeDate = Some today + } + ) + + Assert.Equal( + BondTradeWriteResult.BondTradeInsufficientFunds "available cash is not enough for a bond purchase of 2000.00", + result + ) + + let status, body = reconciliation fundId + Assert.Equal(200, status) + Assert.Contains("\"closingCash\":\"1000.00\"", body) + Assert.Contains("\"difference\":\"0.00\"", body) + Assert.Equal(0, eventCount body "bond_buy") |
