# ADR-0004 — Customer debt as an append-only ledger with a cached running balance

> Produced by the Reversa **Detective** (phase: interpretation) · doc_level: `complete`
> Retroactive ADR reconstructed from Git history: MR!9 "Debt | Updated Implement Customer Debt Management" (`2f54498`), `chore(debt): update migration debt_amount` (`874333d`), MR!22 "Debt | Added paginate" (`4d57a9a`).

- **Status:** Accepted (as-built) 🟢
- **Confidence:** 🟢 CONFIRMED

## Context

The shop sells on credit ("Nợ"). Staff need to know each customer's outstanding balance at a glance, take repayments, add manual debt, and keep an auditable history — even for debts whose originating order is later deleted.

## Decision

- Model debt as an **append-only ledger** (`customer_debts`) with typed entries: `pos_debt`, `manual_debt`, `repayment`, `debt_void`. Each row records `amount` and `balance_after`.
- Keep a **cached running balance** on `customers.debt_total` for fast listing/filtering.
- All mutations go through `CustomerDebt::record`, which locks the customer row (`lockForUpdate`), rejects over-repayment, adjusts `debt_total`, and writes the ledger row atomically.
- `/debts` is a read-only projection of customers with `debt_total > 0`.
- `order_id` on the ledger is nullable and set-null on order delete, so the trail survives a hard-deleted order (a `debt_void` entry is written first).

## Consequences

- 🟢 Fast A/R listing (`debt_total` index) without summing the ledger each read.
- 🟢 Full audit trail with `balance_after` snapshots; survives order deletion.
- 🟡 The cached balance can in principle diverge from the ledger sum if a write path bypasses `record()`; today all paths go through it.
- 🟢 Pagination was retrofitted (MR!22) once debt lists grew — originally `->get()` with no paging.
- 🟢 GAP-O1: hard-deleted orders leave a `debt_void` trail but no order — confirmed with the team (2026-09-18) this matches reporting expectations.
