Block 1.C — NAV engine (architecture view)
For per-feature technical detail see NAV engine. For staff-facing flow see the business finance doc.
What Block 1.C adds
Block 1.C plugs the price-source, FX-source, and NAV calculator into the existing posting pipeline. The calculator does not post journals on calculate() — it's pure. The journal-post on publish is deferred (see nav-engine).
Component map
Invariants Block 1.C introduces
- One snapshot per fund per date. Unique index
(fundId, valuationDate)guarantees no two NAVs can ever exist for the same day. Drafts get replaced; frozen/published rows are immutable. - Snapshot lines reconstitute the formula.
sum(asset lines) = totalAssets;sum(liability lines) = totalLiabilities; the formula(totalAssets − totalLiabilities) / unitsOutstanding = navPerUnitmust hold within rounding. Operators can audit a NAV without reading code. - Pricing is append-only.
SecurityPriceis a timeseries — never overwritten except by re-recording the exact same(securityId, priceDate, source)(idempotency). History is preserved for back-testing and audit. - Stale gates publish. A snapshot with any stale-price line cannot be published without an explicit
allowStale=trueoverride (which is audit-logged). Default behaviour fails closed. - Cross-rate derivation, not double entry. FX rates are stored one-direction. Inverse and pivot-via-NGN rates are computed on read. Operators never enter the same rate twice.
How Block 1.C consumes earlier Blocks
| Source | Used by | Purpose |
|---|---|---|
| Phase 0 — AAOIFI COA seed | Calculator | Read GL prefixes 1001/1002/1200/1203/2000/2003/2100/3202/6201/6302 |
| Phase 0 — per-fund control accounts | (Future, Block 2.E) | 6002* for NAV adjustment journal |
Block 1.A — CustodianPosition | Calculator step 2 | Source of holdings |
Block 1.A — FundSubledger.getControlBalance | Calculator step 11 | Source of unitsOutstanding |
Block 1.A — GlService.getBalanceForPrefix | Calculator steps 3, 5, 7, 8, 9 | Cash + receivables + payables + reserves |
Block 1.B — PostingEngine (not yet) | Future runAndAccrue | Posting management fee accrual + FX reval together |
Block 1.B — createSystemJournal (not yet) | Future publish | Posting the NAV adjustment journal |
Failure modes explicitly handled
- Missing price → line uses cost basis; flagged as stale; counted into
staleLineCount. - Missing FX rate for non-base price →
fxRate=1, value translation skipped; this currently silently treats the non-base price as a base-currency value (acceptable for first-draft data quality; future improvement is to mark the line asrequires_fx). - Units = 0 → NAVPU = 0, not NaN.
- Inactive fund →
BadRequestExceptionat calculate. - Concurrent recalculation → transaction wrap prevents partial writes; the unique index makes the second writer fail cleanly.
- Stale price on publish →
400 BadRequestExceptionunlessallowStale=true. - Recalculate-on-frozen/published →
409 Conflict.
What's still deferred
| Item | Block | Reason |
|---|---|---|
| Management fee accrual on calculation | 2.D | Per-fund fee config (managementFeeRate) lands then |
| FX revaluation journal posting | 2.C | Standalone FX reval handler + cron lands then |
| NAV adjustment journal on publish | 2.E | Needs all of the above + per-fund 6002 resolver — month-end orchestrator landing this trio |
| CBN auto-fetch of FX rates | 2.C | Optional but recommended in the plan |
The NAV pricing path — what users see on the portal — is fully functional today. The GL postings that describe the NAV calculation in accounting terms land in Phase 2.
How Block 1.D depends on Block 1.C
| Block 1.D task | Depends on |
|---|---|
| 1.D.1 Subscription refactor | None new — Block 1.A subledgers + Block 1.B engine are enough |
| 1.D.2 Redemption lifecycle | NAV snapshot for pricing — the redemption auto-pricer reads the latest published snapshot on the fund |
| 1.D.3 Equity trade handler | PriceSourceService for trade-date pricing + Security for screening lookup |
| 1.D.4 Sukuk trade handler | Same as 1.D.3 |