Engine overview
The ledger is deliberately small at its core. Understanding these five moving parts is enough to reason about almost every feature.
The five moving parts
- Source adapter — HTTP client for one upstream. Handles auth, pagination, error retries. Pure I/O; no business logic.
- Importer — maps a raw upstream row → a
TransactionPayload. Idempotent on(source, source_txn_id). Where the "what does this event MEAN" logic lives. - Posting writer — the boundary.
record_transaction()writes theTransactionrow + calls the posting rule for thattxn_type, wraps in one DB txn, returns anidempotentflag if it was a duplicate. - Posting rules — pure functions. One per
txn_type(wire_in,buy_security,brokerage,fx_spread, …). Take the transaction, return a list ofPostingLines. Never touch the DB. - Recost — some brokers (GTN, ViewTrade) bundle fees into the trade price. Ingest posts a placeholder cost leg; the recost job replaces it with the real broker/Valura split. Runs on-demand or after every sync.
Domain model
The two India-only tables (lrs_remittances, dividend_events) also exist in
the UAE schema (same migrations apply to both books) but stay empty — India-side
checks degrade to ok when the table is empty. See Two-book model.
Request lifecycle (READ)
- Broker fetches always use their OWN client, never the DB session.
- Failures on one customer never abort the firm report — captured as
status=error.
Request lifecycle (WRITE / job)
- Idempotency everywhere.
record_transactionshort-circuits on(source, source_txn_id)conflict. - Injectable brokers. Every
sync_*function takes an optionalbroker. Tests pass a fake; live code uses the real client.