Block 1.D — NAV-fund flows (architecture view)
For per-feature technical detail see nav-fund-flows. For staff-facing flow see the business finance doc.
What Block 1.D closes
Phase 0 → 1.C built the plumbing. Block 1.D plugs four real money flows into that plumbing, as a parallel product to the existing Mudarabah investment flow:
The two products write to the same GL but use different account ranges so they never collide:
- Mudarabah → dynamically configured accounts via
investmentAccountMapping - NAV-fund → AAOIFI-seeded accounts (
1002,6101,6001,1301,1302,2001,2003, …)
Posting handlers added
Each handler:
- Pure
buildLines()— returns journal line specs from the typed request. postSubledgers()— writes the matching subledger rows inside the engine's transaction.
State machines
Invariants Block 1.D introduces
- Two products, one ledger. Mudarabah investments and NAV-fund subscriptions both post to
journal_entries. They never share an account code; the hash chain stitches both in time order. The same trial balance reflects both products correctly. - Shariah gates every trade.
BLOCK_HARAM_SECTORrule fires on everyequity_tradeandsukuk_tradeposting. Unscreened or haram-screened securities cannot be traded. Thebroker_tradesrow is markedfailed(not silently deleted) for audit. - Unit balances are subledger-truth. Before allowing a redemption, the service queries the client subledger's closing unit balance. The
redemption_requeststable never decides "client has enough units" from a denormalised count; always from the live subledger. - Pricing always uses a published NAV. Neither subscription unit-allocation nor redemption pricing can use a draft or frozen NAV. Only
published. This guarantees the NAVPU was reviewed and locked in. - Trade subledger predates the GL posting.
TradesService.recordTradecreates thebroker_tradesrow before the engine post. On engine rejection the row is updated tofailed. The row is the evidence; the journal is the consequence.
Dependency map
| Block 1.D piece | Reads from | Writes to |
|---|---|---|
| Subscription cash receipt | — | fund_subscriptions, client_subledger, GL |
| Subscription allocation | nav_snapshots (published), fund_subscriptions | fund_subscriptions, client_subledger, fund_subledger, GL |
| Redemption submit | client_subledger (unit balance check) | redemption_requests |
| Redemption pricing | nav_snapshots, redemption_requests | redemption_requests, client_subledger, fund_subledger, GL |
| Redemption payment | redemption_requests | redemption_requests, client_subledger, GL |
| Trade record | shariah_screening_results (via engine), broker_trades (uniqueness) | broker_trades, custodian_positions, GL |
| Trade settle | broker_trades | broker_trades, GL |
What changes for daily reconciliation
The Block 1.A reconciliation engine still runs nightly at 23:00. With Block 1.D actually populating subledger rows, the previously-empty checks (client_cash_control, client_securities_control, fund_unit_control) start showing real comparisons. A subscription that posts the GL journal but fails to write the client subledger row would create a delta immediately — the system surfaces the inconsistency the same day rather than letting it compound.
The Mudarabah flow doesn't write to these subledgers (it never has), so reconciliation deltas from Mudarabah cash sit only in the GL — that's fine because Mudarabah accounts (investmentAccountMapping-driven) are not in the AAOIFI control-account range. The reconciliation engine ignores them by checking control prefixes.
Failure modes explicitly handled
- Subscription cash receipt rejected by validation → the
fund_subscriptionslifecycle row was inserted first; it stays with a placeholder journal id. Currently rare; a follow-up sweep would clean these up. - Trade rejected by Shariah engine →
broker_tradesrow markedfailed, error in metadata.ForbiddenExceptionraised. No GL or subledger writes. - Redemption priced with stale-NAV snapshot → blocked at NAV calculator (you can't publish a snapshot with stale-price lines without
allowStale); pricing won't reach a stale source. - Insufficient units on redemption → service rejects with
400and the unit balance in the error message. No row created. - Settle on already-settled trade → service returns
409from the broker-ledger layer.
What's still deferred (intentional)
- Customer-portal integration. No customer-facing UI for NAV-fund subscriptions or redemptions yet. Everything is staff-driven via ERP. Customer portal will hook in via a future block once the back-office flow is validated.
- Realised gain/loss on equity sale. Today the SELL handler posts at the trade price (using
grossas the proxy for cost basis). A moving-average cost engine is a Phase 2 add. - Auto-pricing on NAV publish.
RedemptionService.autoPriceForFundexists and works; the NAV publish path doesn't call it yet. Wiring lands when month-end close orchestrator (2.E) finalises the publish-side effects. broker_trade.assetClasscolumn. Stored in metadata for now; can be promoted to a first-class column when query patterns demand it.
How Block 1.D completes Phase 1
After Block 1.D the operating model spans the full pooled-fund lifecycle:
- Operations onboard a new fund (Phase 0 auto-provisions its control accounts).
- Operations price securities + record FX rates (Block 1.C).
- Operations record client subscriptions; allocate units after NAV publishes (Block 1.D).
- Operations record fund trades through brokers, settle on T+2 (Block 1.D).
- NAV calculates daily, snapshots move draft → frozen → published (Block 1.C).
- Operations process redemption requests at the published NAVPU (Block 1.D).
- Reconciliation runs nightly; the hash chain verifier runs at 02:00 (Phase 0 + Block 1.A).
What still needs Phase 2 to be production-ready: purification, zakat, FX revaluation cron, fee accrual, dividend posting, period locking, month-end orchestration, regulatory reports. Phase 1 ends with the operating model intact, regulator-aware features deferred.