From 205e2a592bf781f08fe23f3577cdfe74ff9a076a Mon Sep 17 00:00:00 2001 From: "Somhairle H. Marisol" Date: Mon, 21 Sep 2026 11:44:07 +0800 Subject: Add additional capital deposit flow (3d-4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deposits credit a fund's available cash directly: Domain gains CapitalPolicy (positive, two-decimal, overflow-checked amounts plus a settle function that never touches units, NAV or realized P&L). Persistence records deposits in fund_capital_deposits with a fund_capital_idempotencies table, crediting cash and the deposit row atomically under the same replay/conflict semantics as orders. The API exposes POST /funds/{id}/capital/deposit and GET /funds/{id}/capital/deposits behind the bearer token and idempotency keys. The frontend adds an '追加资金' entry on the fund cash card with in-place balance refresh, and repeats of the same amount reuse the idempotency key so double submits credit cash exactly once. Browser QA extends the J-series (J12-J18): invalid-amount rejection without POST, single credited deposit with a 32-hex key, instant balance refresh, unchanged units/cost proving no realized-income impact, and same-key replay crediting only once. --- tests/FundLab.Web.Tests/BoundaryTests.fs | 72 ++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) (limited to 'tests/FundLab.Web.Tests/BoundaryTests.fs') diff --git a/tests/FundLab.Web.Tests/BoundaryTests.fs b/tests/FundLab.Web.Tests/BoundaryTests.fs index 015ed45..5226b65 100644 --- a/tests/FundLab.Web.Tests/BoundaryTests.fs +++ b/tests/FundLab.Web.Tests/BoundaryTests.fs @@ -635,3 +635,75 @@ module RedemptionBoundaryTests = Assert.Equal("0.00000000", decoded.positions[0].reservedUnits) Assert.Equal("5.00000000", decoded.positions[1].reservedUnits) + +module CapitalBoundaryTests = + + let sampleFund () : FundSummary = + { + id = "3fa85f64-5717-4562-b3fc-2c963f66afa6" + name = "追加资金测试" + currency = "CNY" + initialCash = "10000.00" + initialUnitNav = "1.00000000" + isSynthetic = false + availableCash = "10000.00" + reservedCash = "0.00" + status = "active" + } + + [] + let ``capital deposit request requires fund token and positive amount`` () = + let noFund, _ = update CapitalDepositRequested { init () with token = "qa-token" } + + Assert.Equal(0, noFund.capitalSeq) + Assert.False(noFund.capitalInFlight) + + let noToken, _ = + update CapitalDepositRequested { init () with createdFund = Some(sampleFund ()) } + + Assert.Equal(Some "请输入 API token", noToken.error) + Assert.Equal(0, noToken.capitalSeq) + + let invalidAmount, _ = + update CapitalDepositRequested + { init () with token = "qa-token"; createdFund = Some(sampleFund ()); capitalAmount = "0.00" } + + Assert.Equal(Some "追加金额必须是大于零的两位小数金额,例如 1000.00", invalidAmount.error) + Assert.Equal(0, invalidAmount.capitalSeq) + + [] + let ``valid deposit starts one attempt and completion refreshes the fund`` () = + let started, _ = + update CapitalDepositRequested + { init () with token = "qa-token"; createdFund = Some(sampleFund ()); capitalAmount = "500.00" } + + Assert.Equal(1, started.capitalSeq) + Assert.True(started.capitalInFlight) + Assert.Equal("500.00", started.lastCapitalAttempt.Value.amount) + Assert.Equal(32, started.lastCapitalAttempt.Value.idempotencyKey.Length) + + let completed, _ = update (CapitalDepositCompleted(1, "500.00")) started + + Assert.False(completed.capitalInFlight) + Assert.Equal(Some "追加成功 + 500.00(现金余额已刷新)", completed.capitalNote) + + [] + let ``stale capital completion is ignored and token change clears the entry`` () = + let model = + { init () with + token = "qa-token" + createdFund = Some(sampleFund ()) + capitalSeq = 4 + capitalInFlight = true + lastCapitalAttempt = Some { idempotencyKey = "k"; amount = "500.00" } } + + let stale, _ = update (CapitalDepositCompleted(3, "500.00")) model + + Assert.True(stale.capitalInFlight) + + let cleared, _ = update (TokenChanged "new-token") model + + Assert.Equal("", cleared.capitalAmount) + Assert.Equal(None, cleared.createdFund) + Assert.False(cleared.capitalInFlight) + Assert.Equal(None, cleared.lastCapitalAttempt) -- cgit v1.2.3