Skip to main content

Posting engine

The double-entry core. Small, pure, testable, source-agnostic.

The contract

Every Transaction gets ONE call to record_transaction(). It:

  1. Checks idempotency on (source, source_txn_id). Duplicate → short-circuit and return idempotent=True.
  2. Inserts the Transaction row.
  3. Dispatches to the posting rule for that txn_type.
  4. Inserts the returned journal lines.
  5. Wraps in one DB transaction.
result = await record_transaction(
session,
TransactionPayload(
source="VIEWTRADE",
source_txn_id="ORDER_12345",
customer_id="cust-abc",
txn_date=date.today(),
txn_type="buy_security",
currency="USD",
amount_native=Decimal("1500.00"),
amount_usd=Decimal("1500.00"),
symbol="AAPL",
isin="US0378331005",
qty=Decimal("10"),
),
commit=False,
)

Posting rules

One pure function per txn_type, in services/posting.py. Returns a list of PostingLines. Wallet debits are scoped to customer_id; the customer-cash- wallet account (2010 / 2000) is per-customer partitioned.

txn_typeDebitCreditWhen
wire_in1100 Omnibus cash2010 Customer walletBroker confirms deposit landed
wire_out20101100Broker confirms withdrawal sent
buy_security1200 custody + 2010 wallet2100 sec liability + 2010 walletExecuted order (bundled fees)
sell_security2100 + 20101100 + 2010Executed sell (bundled fees)
brokerage20104000 (placeholder → recost splits)Per trade
taf_sec_fee2220 payable4020 revenue, 2010 walletUS regulatory fee
dividend11002010Broker credits dividend (gross)
wht_tax20102200 payableWithheld on dividend
fx_spread1300 GlomoPay FX Receivable4030 FX Spread RevenueGlomoPay status=paid (India)
fx_spread_settle1010 operating bank1300GlomoPay settlement batch pays out
deposit_in_transit1500 Cash-in-Transit2010Optional GL path
deposit_settle11001500Clearing the 1500 leg
reversalmirror of the reversed txnmirrorRefund / correction

Balance invariant

Every posting rule MUST return lines where Σ debits = Σ credits in USD. The insert enforces it — a non-balancing rule fails with a PostingError.

The trial balance check (Category A, trial_balance_zero) sums every journal line across every account and asserts ΣDR = ΣCR on every reconciliation cycle.

Idempotency

Unique constraint: (source, source_txn_id). A duplicate insert triggers the short-circuit: record_transaction returns {txn_id: <existing>, idempotent: True} without creating anything.

For events that need multiple postings from the same upstream record (e.g. a GlomoPay order that produces both an fx_spread accrual and an fx_spread_settle clearing), the importer manufactures distinct source_txn_ids (e.g. "ORDER_x" and "ORDER_x:fxsettle") so both stay idempotent independently.

Reversal

services/reversal.py::reverse_transaction(txn_id) posts a new Transaction with txn_type="reversal" and reverses_txn_id=<original>. It mirrors the ACTUAL posted journal lines rather than re-running the poster — correct even for recosted fees, which carry the real broker split rather than the placeholder legs.

The reversal is itself idempotent: a second reverse_transaction(txn_id) returns idempotent=True because the derived source_txn_id ({original}-REV) already exists.

Customer wallet derivation

Each customer's wallet balance = Σ 2010 journal lines for that customer. The wallet_derivation check (Category B) recomputes this from the journal and asserts equality with whatever cached view the app is serving.