# ADR: on-hand concurrency — self-contained lock, not `ledger.debitWithRead`

**Decision:** `internal/post-movement.ts` uses its own `SELECT ... FOR UPDATE` + fresh-SUM-of-`stock_movement` pattern inside the caller's transaction, rather than composing `@platform-modules/ledger`'s `debitWithRead`/`appendEntry`.

**Why:** `ledger`'s primitives are typed around `bigint` deltas/balances (`ledgerEntries.delta: bigint`, `walletBalances.balance: bigint`, `guardedDebitUpdate`'s `gte(balanceColumn, amount: bigint)`) — an integer-unit domain (money minor units, points). This module's `stock_movement.qty_delta` is `numeric(18,4)` — genuinely fractional (UoM-based quantities, not just cents). Composing `ledger` would require lossy fixed-point scaling of quantities into bigint or a type-incompatible balance column, for no benefit — `ledger`'s composite-key support (`ColumnBalanceTarget.where`) is fine, but the amount type is not. This is the spec's documented fallback case: *"self-contained lock ONLY if seam-review proves ledger's wallet shape can't carry composite (item, location) key"* — extended here to the amount-type shape, not just the key shape.

**Mechanism:** multi-statement-in-tx MVCC pattern (lock candidate rows → fresh `SUM(qty_delta)` read → insert movement), same shape already proven in this repo's `reserveStock` MVCC fix (single-statement CTEs summing a child table oversell under READ COMMITTED; multi-statement-in-tx is the correct fix).

**External contract unaffected:** public export signatures (`postReceipt(db, {...})` etc.) are identical either way — this is an internal mechanism choice, not a graph-changing one.
