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 | |
| 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')
| -rw-r--r-- | tests/FundLab.Api.Tests/CashReconciliationTests.fs | 144 | ||||
| -rw-r--r-- | tests/FundLab.Api.Tests/FundValuationTests.fs | 9 | ||||
| -rw-r--r-- | tests/FundLab.Api.Tests/RebalanceStockTests.fs | 3 | ||||
| -rw-r--r-- | tests/FundLab.Api.Tests/StockSellTests.fs | 8 | ||||
| -rw-r--r-- | tests/FundLab.Api.Tests/StockTradeTests.fs | 2 |
5 files changed, 151 insertions, 15 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") diff --git a/tests/FundLab.Api.Tests/FundValuationTests.fs b/tests/FundLab.Api.Tests/FundValuationTests.fs index 409bfba..eacd5cd 100644 --- a/tests/FundLab.Api.Tests/FundValuationTests.fs +++ b/tests/FundLab.Api.Tests/FundValuationTests.fs @@ -107,7 +107,8 @@ type FundValuationTests(fixture: PostgresFixture) = repository().CreateStockTrade( key, fundId, - { InstrumentCode = code; StockName = Some "贵州茅台"; Quantity = quantity; Price = price } + { InstrumentCode = code; StockName = Some "贵州茅台"; Quantity = quantity; Price = price }, + debitAvailableCash = false ) with | StockTradeWriteResult.StockTradeCreated _ -> () @@ -133,7 +134,8 @@ type FundValuationTests(fixture: PostgresFixture) = ValueDate = None MaturityDate = None TradeDate = Some(DateOnly(2026, 9, 21)) - } + }, + debitAvailableCash = false ) with | BondTradeWriteResult.BondTradeCreated _ -> () @@ -159,7 +161,8 @@ type FundValuationTests(fixture: PostgresFixture) = ValueDate = Some(DateOnly(2020, 10, 15)) MaturityDate = Some(DateOnly(2026, 10, 15)) TradeDate = Some(DateOnly(2026, 9, 21)) - } + }, + debitAvailableCash = false ) with | BondTradeWriteResult.BondTradeCreated _ -> () diff --git a/tests/FundLab.Api.Tests/RebalanceStockTests.fs b/tests/FundLab.Api.Tests/RebalanceStockTests.fs index 3aed5ae..caaa7c9 100644 --- a/tests/FundLab.Api.Tests/RebalanceStockTests.fs +++ b/tests/FundLab.Api.Tests/RebalanceStockTests.fs @@ -69,7 +69,8 @@ type RebalanceStockTests(fixture: PostgresFixture) = repository().CreateStockTrade( key, fundId, - { InstrumentCode = code; StockName = Some "贵州茅台"; Quantity = quantity; Price = price } + { InstrumentCode = code; StockName = Some "贵州茅台"; Quantity = quantity; Price = price }, + debitAvailableCash = false ) with | StockTradeWriteResult.StockTradeCreated _ -> () diff --git a/tests/FundLab.Api.Tests/StockSellTests.fs b/tests/FundLab.Api.Tests/StockSellTests.fs index 972293a..fb17a50 100644 --- a/tests/FundLab.Api.Tests/StockSellTests.fs +++ b/tests/FundLab.Api.Tests/StockSellTests.fs @@ -129,7 +129,7 @@ type StockSellTests(fixture: PostgresFixture) = let status, fundBody = fund sellQuotes fundId Assert.Equal(200, status) - Assert.Contains("\"availableCash\":\"1480.00\"", fundBody) + Assert.Contains("\"availableCash\":\"480.00\"", fundBody) [<Fact>] member _.``selling all units removes the position row and credits the proceeds``() = @@ -150,7 +150,7 @@ type StockSellTests(fixture: PostgresFixture) = Assert.Contains("\"positions\":[]", positionsBody) let _, fundBody = fund sellQuotes fundId - Assert.Contains("\"availableCash\":\"2200.00\"", fundBody) + Assert.Contains("\"availableCash\":\"1200.00\"", fundBody) [<Fact>] member _.``overselling is rejected with a reason and leaves cash and position unchanged``() = @@ -171,7 +171,7 @@ type StockSellTests(fixture: PostgresFixture) = Assert.Contains("\"quantity\":\"100.00000000\"", positionsBody) let _, fundBody = fund quotes fundId - Assert.Contains("\"availableCash\":\"1000.00\"", fundBody) + Assert.Contains("\"availableCash\":\"0.00\"", fundBody) [<Fact>] member _.``valuation after a partial sale prices only the remaining units``() = @@ -215,7 +215,7 @@ type StockSellTests(fixture: PostgresFixture) = Assert.DoesNotContain("\"quantity\":\"40.00000000\"", positionsBody) let _, fundBody = fund quotes fundId - Assert.Contains("\"availableCash\":\"1300.00\"", fundBody) + Assert.Contains("\"availableCash\":\"300.00\"", fundBody) [<Fact>] member _.``a persisted snapshot price wins over the live quote when selling``() = diff --git a/tests/FundLab.Api.Tests/StockTradeTests.fs b/tests/FundLab.Api.Tests/StockTradeTests.fs index f8a8492..0b52faf 100644 --- a/tests/FundLab.Api.Tests/StockTradeTests.fs +++ b/tests/FundLab.Api.Tests/StockTradeTests.fs @@ -46,7 +46,7 @@ type StockTradeTests(fixture: PostgresFixture) = let command = { Name = "股票闭环测试 FOF" - InitialCash = 100000.00m + InitialCash = 200000.00m InitialUnitNav = 1.00000000m IsSynthetic = true } |
