Skip to main content

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

  1. Source adapter — HTTP client for one upstream. Handles auth, pagination, error retries. Pure I/O; no business logic.
  2. Importer — maps a raw upstream row → a TransactionPayload. Idempotent on (source, source_txn_id). Where the "what does this event MEAN" logic lives.
  3. Posting writer — the boundary. record_transaction() writes the Transaction row + calls the posting rule for that txn_type, wraps in one DB txn, returns an idempotent flag if it was a duplicate.
  4. Posting rules — pure functions. One per txn_type (wire_in, buy_security, brokerage, fx_spread, …). Take the transaction, return a list of PostingLines. Never touch the DB.
  5. 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_transaction short-circuits on (source, source_txn_id) conflict.
  • Injectable brokers. Every sync_* function takes an optional broker. Tests pass a fake; live code uses the real client.