summaryrefslogtreecommitdiff
path: root/tests/FundLab.Api.Tests/CashReconciliationTests.fs
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-22 13:03:36 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-22 13:03:36 +0800
commit2dedc30358d1724f60ab646b7a24e93f3672dff1 (patch)
tree515258bf4b3e1546fdf97c481e12740c573ac7c8 /tests/FundLab.Api.Tests/CashReconciliationTests.fs
parent39965954968436911b32e774d32d3cca9527e4e2 (diff)
downloadfund-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.fs144
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")