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:

  1. Pure buildLines() — returns journal line specs from the typed request.
  2. postSubledgers() — writes the matching subledger rows inside the engine's transaction.

State machines

Invariants Block 1.D introduces

  1. 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.
  2. Shariah gates every trade. BLOCK_HARAM_SECTOR rule fires on every equity_trade and sukuk_trade posting. Unscreened or haram-screened securities cannot be traded. The broker_trades row is marked failed (not silently deleted) for audit.
  3. Unit balances are subledger-truth. Before allowing a redemption, the service queries the client subledger's closing unit balance. The redemption_requests table never decides "client has enough units" from a denormalised count; always from the live subledger.
  4. 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.
  5. Trade subledger predates the GL posting. TradesService.recordTrade creates the broker_trades row before the engine post. On engine rejection the row is updated to failed. The row is the evidence; the journal is the consequence.

Dependency map

Block 1.D pieceReads fromWrites to
Subscription cash receiptfund_subscriptions, client_subledger, GL
Subscription allocationnav_snapshots (published), fund_subscriptionsfund_subscriptions, client_subledger, fund_subledger, GL
Redemption submitclient_subledger (unit balance check)redemption_requests
Redemption pricingnav_snapshots, redemption_requestsredemption_requests, client_subledger, fund_subledger, GL
Redemption paymentredemption_requestsredemption_requests, client_subledger, GL
Trade recordshariah_screening_results (via engine), broker_trades (uniqueness)broker_trades, custodian_positions, GL
Trade settlebroker_tradesbroker_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_subscriptions lifecycle 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 enginebroker_trades row marked failed, error in metadata. ForbiddenException raised. 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 400 and the unit balance in the error message. No row created.
  • Settle on already-settled trade → service returns 409 from 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 gross as the proxy for cost basis). A moving-average cost engine is a Phase 2 add.
  • Auto-pricing on NAV publish. RedemptionService.autoPriceForFund exists 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.assetClass column. 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:

  1. Operations onboard a new fund (Phase 0 auto-provisions its control accounts).
  2. Operations price securities + record FX rates (Block 1.C).
  3. Operations record client subscriptions; allocate units after NAV publishes (Block 1.D).
  4. Operations record fund trades through brokers, settle on T+2 (Block 1.D).
  5. NAV calculates daily, snapshots move draft → frozen → published (Block 1.C).
  6. Operations process redemption requests at the published NAVPU (Block 1.D).
  7. 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.