ntro.valuations turns a property’s facts and a set of assumptions into a
forecast cashflow and the comparable valuation lanes the Analysis UI renders.
It has three parts: a pure engine, a typed domain model, and a versioned
store.
The whole namespace obeys one law:
value = f(facts, assumptions, engine_version)
Outputs are never an input. You store the facts (assets) and the assumptions,
record which engine produced a snapshot, and re-derive (or read a cache) on
demand. Four data kinds flow through:
Engine
run_model is deterministic, pure, and entity-agnostic — same inputs, same
bytes out. It is the single source of truth; the ui-tenant sensitivity controls
call this one engine rather than reimplementing it.
ENGINE_VERSION (re-0.1.0) stamps every snapshot. Bump it when the calc
changes; cached forecasts produced by an older version are treated as stale.
Output shape
MonthRow — one row per model month:
FYRow — fiscal-year rollup: gross_rental_income, ebitda, npat,
gross_yield, net_yield, property_value (NBV at the FY-end month).
Domain model
ntro.valuations.models is the typed input surface. All money is Decimal.
Store
ntro.valuations.store persists versioned valuation sets. Two set kinds,
discriminated by engine_version:
save_* for an (entity, label) writes model_version = max(existing) + 1
— old versions are retained so you can diff Forecast-v2 vs v3, or Bank vs
Indicative.
Caching: inputs_hash / forecast_is_stale
A saved DCF set caches its forecast snapshot keyed by inputs_hash — a sha256
of (assumptions, assets, engine_version) via compute_inputs_hash. Before
trusting a cached forecast, check forecast_is_stale(row, assumptions, assets):
True when the inputs or the engine version moved, and you should re-run_model.
Storage layout
Two schemas, deliberately split:ledgers.assets— the facts (oneAssetRowper asset). Lives in theledgersschema alongside the GL because it is reconciliation-grade truth.analysis.valuations— the forward / analytical model (assumption set, engine version, cached forecast, asset references). Theanalysisschema is the forward lane, never an accounting ledger. This is the model’s own store — not a subledger you code against directly; go throughntro.valuations.store.
CashflowModel references the AssetRow ids it was built from, so re-loading
reconstructs the exact facts the snapshot was derived from.
Worked example
The engine is authoritative. The Analysis page’s sensitivity controls send
adjusted assumptions to this same
run_model — there is no JavaScript
reimplementation to drift out of sync.Related
assets
AssetRow — the per-title facts the engine models.Subledgers overview
Row base + the ledgers / analysis schema split.Data capability
Data-plane access used by the store helpers.
Typing
Period and the shared field types.