From b8811922f4e167d50efb8817da35a12d8a9d653b Mon Sep 17 00:00:00 2001 From: "Somhairle H. Marisol" Date: Mon, 21 Sep 2026 11:21:51 +0800 Subject: 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. --- tests/FundLab.Domain.Tests/DomainTests.fs | 48 +++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) (limited to 'tests/FundLab.Domain.Tests/DomainTests.fs') diff --git a/tests/FundLab.Domain.Tests/DomainTests.fs b/tests/FundLab.Domain.Tests/DomainTests.fs index f2b4138..6a9fc66 100644 --- a/tests/FundLab.Domain.Tests/DomainTests.fs +++ b/tests/FundLab.Domain.Tests/DomainTests.fs @@ -491,3 +491,51 @@ module DomainTests = Assert.Equal(Error(InvalidObservation "net assets cannot be negative"), negative) Assert.Equal(Error(InvalidObservation "first net assets must be positive"), zero) + +module RedemptionPolicyTests = + + open Xunit + open FundLab.Domain + + let private unwrap result = + match result with + | Ok value -> value + | Error error -> failwithf "%A" error + + [] + let ``redemption request above available units is rejected`` () = + Assert.Equal(Error "requested units exceed available holdings", RedemptionPolicy.validateRequest 10.00000000m 10.00000001m 0.00m) + Assert.Equal(Error "redemption units must be positive", RedemptionPolicy.validateRequest 10.00000000m 0m 0.00m) + Assert.Equal(Error "redemption fee cannot be negative", RedemptionPolicy.validateRequest 10.00000000m 5.00000000m -0.01m) + Assert.Equal(Ok(), RedemptionPolicy.validateRequest 10.00000000m 10.00000000m 1.50m) + + [] + let ``full redemption releases the entire position cost`` () = + let computed = RedemptionPolicy.compute 40.00000000m 2.5m 1.00m 100.00m 40.00000000m |> unwrap + + Assert.Equal(100.00m, computed.GrossCash) + Assert.Equal(1.00m, computed.Fee) + Assert.Equal(99.00m, computed.Proceeds) + Assert.Equal(100.00m, computed.RedeemedCost) + + [] + let ``partial redemption writes off cost pro rata rounded down`` () = + let computed = RedemptionPolicy.compute 10.00000000m 2.5m 0.00m 100.00m 40.00000000m |> unwrap + + Assert.Equal(25.00m, computed.GrossCash) + Assert.Equal(0.00m, computed.Fee) + Assert.Equal(25.00m, computed.Proceeds) + Assert.Equal(25.00m, computed.RedeemedCost) + + let uneven = RedemptionPolicy.compute 1.00000000m 3m 0.00m 100.00m 3.00000000m |> unwrap + + Assert.Equal(3.00m, uneven.GrossCash) + Assert.Equal(33.33m, uneven.RedeemedCost) + + [] + let ``redemption computation rejects invalid economics`` () = + Assert.Equal(Error "redemption units exceed position units", RedemptionPolicy.compute 41.00000000m 2.5m 0.00m 100.00m 40.00000000m) + Assert.Equal(Error "unit nav must be positive", RedemptionPolicy.compute 1.00000000m 0m 0.00m 100.00m 40.00000000m) + Assert.Equal(Error "redemption fee exceeds redemption value", RedemptionPolicy.compute 1.00000000m 2.5m 3.00m 100.00m 40.00000000m) + Assert.Equal(Error "redemption proceeds rounds to zero", RedemptionPolicy.compute 0.00000001m 1500000m 0.01m 100.00m 40.00000000m) + Assert.Equal(Error "redemption cash rounds to zero", RedemptionPolicy.compute 0.00000001m 2m 0.00m 100.00m 40.00000000m) -- cgit v1.2.3