summaryrefslogtreecommitdiff
path: root/tests/FundLab.Web.Tests/BoundaryTests.fs
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-21 11:21:51 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-21 11:21:51 +0800
commitb8811922f4e167d50efb8817da35a12d8a9d653b (patch)
treeb85cc56677be5d4d58ad2277f00cbc935933301e /tests/FundLab.Web.Tests/BoundaryTests.fs
parent3d9398234a54774544eb48a1caed6a1b2d5132eb (diff)
downloadfund-lab-b8811922f4e167d50efb8817da35a12d8a9d653b.tar.gz
Add redemption order minimal closed loop (3d-3)
Redeem confirmed holdings end to end with exact decimal semantics: submission freezes position units (reserved_units) after an available-units check without touching cash, confirmation settles at the trade-date NAV with evidence and deferral rules identical to subscriptions, credits the fund with proceeds net of the stated fee, and writes off position cost pro rata (full redemption removes the position row). Cash conservation holds at every step: frozen shares and receivable cash never appear as available cash before settlement. Domain gains RedemptionPolicy (request validation plus proceeds/fee/ cost-release computation) with unit tests. Persistence adds redemption_orders, idempotency tables, reserved_units on positions and Create/Confirm repository members; the API exposes POST/GET /funds/{id}/redemptions and POST .../confirm. The frontend adds a 06/REDEEM panel (submit, confirm, pending reasons, confirmed proceeds/cost display), frozen-share display on the holdings panel, and refresh wiring. Browser QA gains a K-series covering freeze, settlement, cash accounting, replay and over-redemption rejection.
Diffstat (limited to 'tests/FundLab.Web.Tests/BoundaryTests.fs')
-rw-r--r--tests/FundLab.Web.Tests/BoundaryTests.fs76
1 files changed, 76 insertions, 0 deletions
diff --git a/tests/FundLab.Web.Tests/BoundaryTests.fs b/tests/FundLab.Web.Tests/BoundaryTests.fs
index d48d08b..015ed45 100644
--- a/tests/FundLab.Web.Tests/BoundaryTests.fs
+++ b/tests/FundLab.Web.Tests/BoundaryTests.fs
@@ -379,6 +379,7 @@ module ConfirmationBoundaryTests =
[|
{ instrumentCode = "000001"
units = "40.00000000"
+ reservedUnits = "0.00000000"
costCash = "100.00"
lastConfirmedAt = "2026-09-21T11:00:00+00:00"
valuationNav = someText "2.61000000"
@@ -386,6 +387,7 @@ module ConfirmationBoundaryTests =
valuationCollectedAt = someText "2026-09-21T09:30:00+00:00" }
{ instrumentCode = "110011"
units = "10.00000000"
+ reservedUnits = "5.00000000"
costCash = "25.00"
lastConfirmedAt = "2026-09-21T11:00:00+00:00"
valuationNav = null
@@ -559,3 +561,77 @@ module ConfirmationBoundaryTests =
let stalePositions, _ = update (PositionsReadCompleted(3, samplePositions ())) cleared
Assert.Equal(None, stalePositions.positions)
+
+module RedemptionBoundaryTests =
+
+ let sampleRedemptionRaw (status: string) : RawRedemption =
+ {
+ id = "9b2f1c3d-0000-0000-0000-000000000001"
+ instrumentCode = "000001"
+ units = "150.00000000"
+ feeAmount = "1.50"
+ status = status
+ submittedAt = "2026-09-21T10:00:00+00:00"
+ tradeDate = "2026-09-21"
+ pendingReason = null
+ confirmedAt = null
+ confirmedNav = null
+ confirmedNavDate = null
+ confirmedProceeds = null
+ confirmedCostReleased = null
+ }
+
+ let confirmedRedemptionRaw () : RawRedemption =
+ let someText (value: string) : obj = box { case = "Some"; fields = [| value |] }
+
+ { sampleRedemptionRaw "confirmed" with
+ pendingReason = null
+ confirmedAt = someText "2026-09-21T11:00:00+00:00"
+ confirmedNav = someText "2.50000000"
+ confirmedNavDate = someText "2026-09-21"
+ confirmedProceeds = someText "373.50"
+ confirmedCostReleased = someText "375.00" }
+
+ [<Fact>]
+ let ``redemption decode keeps decimal strings and option fields`` () =
+ let confirmed = Api.decodeRedemption (confirmedRedemptionRaw ())
+
+ Assert.Equal("confirmed", confirmed.status)
+ Assert.Equal("150.00000000", confirmed.units)
+ Assert.Equal("1.50", confirmed.feeAmount)
+ Assert.Equal("2.50000000", confirmed.confirmedNav.Value)
+ Assert.Equal("373.50", confirmed.confirmedProceeds.Value)
+ Assert.Equal("375.00", confirmed.confirmedCostReleased.Value)
+ Assert.Equal(None, confirmed.pendingReason)
+
+ let sampleRedemptionPositions () : RawPositions =
+ {
+ availableCash = "9900.00"
+ reservedCash = "101.50"
+ positions =
+ [|
+ { instrumentCode = "000001"
+ units = "40.00000000"
+ reservedUnits = "0.00000000"
+ costCash = "100.00"
+ lastConfirmedAt = "2026-09-21T11:00:00+00:00"
+ valuationNav = null
+ valuationNavDate = null
+ valuationCollectedAt = null }
+ { instrumentCode = "110011"
+ units = "10.00000000"
+ reservedUnits = "5.00000000"
+ costCash = "25.00"
+ lastConfirmedAt = "2026-09-21T11:00:00+00:00"
+ valuationNav = null
+ valuationNavDate = null
+ valuationCollectedAt = null }
+ |]
+ }
+
+ [<Fact>]
+ let ``positions decode keeps reserved units for frozen shares`` () =
+ let decoded = Api.decodePositions (sampleRedemptionPositions ())
+
+ Assert.Equal("0.00000000", decoded.positions[0].reservedUnits)
+ Assert.Equal("5.00000000", decoded.positions[1].reservedUnits)