From 9818daebaf6fb99124a3196f880b3b8a7998d3a6 Mon Sep 17 00:00:00 2001 From: "Somhairle H. Marisol" Date: Tue, 22 Sep 2026 11:08:05 +0800 Subject: Add cash ledger event-source breakdown and reconciliation API/UI (3d-31 B4c) --- src/FundLab.Api/App.fs | 77 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) (limited to 'src/FundLab.Api/App.fs') diff --git a/src/FundLab.Api/App.fs b/src/FundLab.Api/App.fs index d5c3a50..3d89dae 100644 --- a/src/FundLab.Api/App.fs +++ b/src/FundLab.Api/App.fs @@ -487,6 +487,35 @@ type CapitalDepositResponse = createdAt: string } +type CashLedgerEventResponse = + { + source: string + eventDate: string + amount: string + referenceType: string + referenceId: string + note: string option + } + +type CashLedgerSourceResponse = + { + source: string + netAmount: string + eventCount: int + } + +type CashReconciliationResponse = + { + fundId: Guid + openingCash: string + netInflow: string + closingCash: string + ledgerBalance: string + difference: string + sources: CashLedgerSourceResponse list + events: CashLedgerEventResponse list + } + type FundReturnsPointResponse = { date: string @@ -846,6 +875,39 @@ module App = createdAt = timestampText deposit.CreatedAt } + let private cashLedgerEventResponse (event: CashLedger.CashLedgerEvent) : CashLedgerEventResponse = + { + source = CashLedger.sourceText event.Source + eventDate = dateText event.EventDate + amount = cashText event.Amount + referenceType = event.ReferenceType + referenceId = event.ReferenceId + note = event.Note + } + + let private cashReconciliationResponse + (fundId: Guid) + (reconciliation: CashLedger.CashReconciliation) + (events: CashLedger.CashLedgerEvent list) + : CashReconciliationResponse = + { + fundId = fundId + openingCash = cashText reconciliation.OpeningCash + netInflow = cashText reconciliation.NetInflow + closingCash = cashText reconciliation.ClosingCash + ledgerBalance = cashText reconciliation.LedgerBalance + difference = cashText reconciliation.Difference + sources = + reconciliation.Sources + |> List.map (fun total -> + { + source = CashLedger.sourceText total.Source + netAmount = cashText total.NetAmount + eventCount = total.EventCount + }) + events = events |> List.map cashLedgerEventResponse + } + let private sipPlanResponse (plan: SipPlanRecord) : SipPlanResponse = { id = plan.Id @@ -1763,6 +1825,20 @@ module App = with _ -> errorResponse 500 "PERSISTENCE_ERROR" "capital deposit persistence failed" next ctx + let private getCashReconciliation (repository: FundRepository) (fundIdText: string) : HttpHandler = + fun next ctx -> + match Guid.TryParse fundIdText with + | false, _ -> errorResponse 400 "INVALID_FUND_ID" "fund id must be a UUID" next ctx + | true, fundId -> + try + match repository.GetCashReconciliation fundId with + | Error _ -> errorResponse 404 "FUND_NOT_FOUND" "fund was not found" next ctx + | Ok reconciliation -> + let events = repository.GetCashLedger fundId + json (cashReconciliationResponse fundId reconciliation events) next ctx + with _ -> + errorResponse 500 "PERSISTENCE_ERROR" "cash reconciliation failed" next ctx + let private createSipPlan (repository: FundRepository) (fundIdText: string) : HttpHandler = fun next ctx -> task { @@ -3555,6 +3631,7 @@ module App = GET >=> routef "/funds/%s/positions" (getPositions repository) POST >=> routef "/funds/%s/capital/deposit" (createCapitalDeposit repository) GET >=> routef "/funds/%s/capital/deposits" (getCapitalDeposits repository) + GET >=> routef "/funds/%s/cash-reconciliation" (getCashReconciliation repository) POST >=> routef "/funds/%s/sip/plans" (createSipPlan repository) GET >=> routef "/funds/%s/sip/plans" (getSipPlans repository) POST >=> routef "/funds/%s/sip/advance" (advanceSipPlans repository probes) -- cgit v1.2.3