# User Stories — Accounts Receivable (Debt)

> Produced by the Reversa **Writer** (phase: generation) · doc_level: `complete`
> Generated on 2026-09-21

**Confidence scale:** 🟢 CONFIRMED · 🟡 INFERRED · 🔴 GAP

**Actor:** Store Administrator.
**Owning units:** `debts` (`GET /debts`), `customers-debt-actions` (per-customer ledger + writes).

Debt is a customer accounts-receivable ledger: an append-only `customer_debts` table plus a running `debt_total` on the customer, kept in lockstep. Four entry types share the ledger — `pos_debt` (posted at checkout, see [point-of-sale.md](point-of-sale.md)), `manual_debt`, `repayment`, and `debt_void` (written when a debt-bearing order is deleted, see [order-management.md](order-management.md)). This journey covers the two staff-initiated writes and the read screens. 🟢

---

### US-DEBT-1 — See who owes money

**As a** Store Administrator, **I want** a list of customers with an outstanding balance, **so that** I can chase repayments.

- **Given** the debts overview
- **When** I open `GET /debts`
- **Then** it lists customers with `debt_total > 0`, ordered by `debt_total` desc, paginated 30/page, balances read **live** off `customers.debt_total` 🟢
- **When** I search `q`
- **Then** a **grouped** closure matches phone OR fullname while staying ANDed with `debt_total > 0` (the correct pattern) 🟢

Notes / gaps:
- Read-only screen — all mutations delegate to `customers-debt-actions`. 🟢
- The modal customer picker sources any live customer via `/customers/scan` (not scoped to debtors), so a repayment can be started for a non-debtor and is only rejected downstream. 🟡

Traces to: `debts/` (`DebtController::index`)

---

### US-DEBT-2 — Inspect one customer's ledger

**As a** Store Administrator, **I want** to open a customer's debt history, **so that** I can see every charge and payment.

- **Given** a customer
- **When** I open `GET /customers/{customer}/debt`
- **Then** the **live** `debt_total` shows, plus `CustomerDebt::with('order')` ordered id desc, 20/page — each entry snapshots `balance_after`, and `pos_debt` rows show the order `code` (falling back to the note / "đã xóa" when the order was hard-deleted) 🟢

Traces to: `customers-debt-actions/` (`debt`)

---

### US-DEBT-3 — Record a manual debt

**As a** Store Administrator, **I want** to add a debt outside a sale, **so that** I can record credit extended off-terminal.

- **Given** a customer and an amount
- **When** I submit `POST /customers/{customer}/debts` (`amount` required|numeric|min:0.01, `note` nullable)
- **Then** `CustomerDebt::record` opens a transaction, locks the customer row, adds `amount` to `debt_total`, writes a `manual_debt` ledger row with `balance_after`, stamps `created_by`, and redirects back with a toastr 🟢

Notes / gaps:
- `record()` is called **bare** here (manual_debt never takes the throwing over-balance branch) — intentional asymmetry with repayment. 🟢
- No upper bound / no confirmation step — a mistyped large debt is only reversible via a repayment or `debt_void`. 🟡
- `amount` min:0.01 admits sub-0.1 values that round to 0.0 under `decimal(15,1)` storage. 🟡

Traces to: `customers-debt-actions/` (`storeDebt`, `CustomerDebt::record`)

---

### US-DEBT-4 — Record a repayment

**As a** Store Administrator, **I want** to record a customer paying down their balance, **so that** the ledger reflects the payment.

- **Given** a customer with a balance
- **When** I submit `POST /customers/{customer}/repayments` (`amount` required|numeric|min:0.01)
- **Then** `record` subtracts from `debt_total`, writes a `repayment` row with `balance_after`, and redirects back 🟢
- **Given** the amount exceeds `debt_total`
- **When** `record` throws
- **Then** `storeRepayment` catches it and returns back-with-errors "Số tiền thu nợ vượt quá số dư nợ hiện tại (<balance> ₫)" 🟢

Notes / gaps:
- Concurrency is correctly handled (`lockForUpdate` + transaction, no overdraw race). 🟢 (not a gap)
- `created_by` is an unconstrained `unsignedInteger` with no FK to `admin_users`. 🟡
- No observability on this money-mutating path beyond the ledger row itself. 🔴

Traces to: `customers-debt-actions/` (`storeRepayment`, `CustomerDebt::record`)
