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

  1. 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.
  2. Snapshot lines reconstitute the formula. sum(asset lines) = totalAssets; sum(liability lines) = totalLiabilities; the formula (totalAssets − totalLiabilities) / unitsOutstanding = navPerUnit must hold within rounding. Operators can audit a NAV without reading code.
  3. Pricing is append-only. SecurityPrice is a timeseries — never overwritten except by re-recording the exact same (securityId, priceDate, source) (idempotency). History is preserved for back-testing and audit.
  4. Stale gates publish. A snapshot with any stale-price line cannot be published without an explicit allowStale=true override (which is audit-logged). Default behaviour fails closed.
  5. 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

SourceUsed byPurpose
Phase 0 — AAOIFI COA seedCalculatorRead 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 — CustodianPositionCalculator step 2Source of holdings
Block 1.A — FundSubledger.getControlBalanceCalculator step 11Source of unitsOutstanding
Block 1.A — GlService.getBalanceForPrefixCalculator steps 3, 5, 7, 8, 9Cash + receivables + payables + reserves
Block 1.B — PostingEngine (not yet)Future runAndAccruePosting management fee accrual + FX reval together
Block 1.B — createSystemJournal (not yet)Future publishPosting 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 pricefxRate=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 as requires_fx).
  • Units = 0 → NAVPU = 0, not NaN.
  • Inactive fundBadRequestException at calculate.
  • Concurrent recalculation → transaction wrap prevents partial writes; the unique index makes the second writer fail cleanly.
  • Stale price on publish400 BadRequestException unless allowStale=true.
  • Recalculate-on-frozen/published409 Conflict.

What's still deferred

ItemBlockReason
Management fee accrual on calculation2.DPer-fund fee config (managementFeeRate) lands then
FX revaluation journal posting2.CStandalone FX reval handler + cron lands then
NAV adjustment journal on publish2.ENeeds all of the above + per-fund 6002 resolver — month-end orchestrator landing this trio
CBN auto-fetch of FX rates2.COptional 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 taskDepends on
1.D.1 Subscription refactorNone new — Block 1.A subledgers + Block 1.B engine are enough
1.D.2 Redemption lifecycleNAV snapshot for pricing — the redemption auto-pricer reads the latest published snapshot on the fund
1.D.3 Equity trade handlerPriceSourceService for trade-date pricing + Security for screening lookup
1.D.4 Sukuk trade handlerSame as 1.D.3