# Customers Debt Actions — Implementation Tasks

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

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

## Prerequisites

- [ ] The `Customer` model exists with SoftDeletes, a `debt_total` fillable column cast to `float`, and a `debts()` `hasMany(CustomerDebt)` relation (see `customers-crud`). 🟢 (`Customer.php:9-19,39-42`)
- [ ] The `customer_debts` table exists: `id`, `customer_id` (FK cascade), `order_id` (FK set null, nullable), `related_debt_id` (self FK set null, nullable), `type` enum(`pos_debt`,`manual_debt`,`repayment`,`debt_void`), `amount` decimal(15,1), `balance_after` decimal(15,1) nullable, `note` string nullable, `created_by` unsignedInteger nullable, timestamps. 🟢 (`migration:2026_08_28_000001`)
- [ ] `customers.debt_total` decimal column exists (added by `2026_08_28_000002_add_debt_total_to_customers_table.php`). 🟢
- [ ] The `Order` model exists (eager-loaded by the ledger page for `pos_debt` rows) with an appended `code`. 🟢 (`orders-crud`)
- [ ] The admin auth route group, shared admin layout (`$this->view`), `admin_toastr` helper, and `Admin::user()` facade are available. 🟢 (`routes/web.php:23-28`)
- [ ] A `pages.customer-debt` view exists rendering the balance, the ledger table, and the two modals. 🟢 (`customer-debt.blade.php`)

## Tasks

> Each task references the legacy file the behavior was extracted from.

- [ ] T-01, Register the three routes **before** `resource('/customers')`: `GET /customers/{customer}/debt` (`customers.debt`), `POST /customers/{customer}/debts` (`customers.debts.store`), `POST /customers/{customer}/repayments` (`customers.repayments.store`).
  - Origem no legado: `routes/web.php:58,60,61,62`
  - Critério de pronto: each path reaches its action and none is shadowed by the resource routes.
  - Confiança: 🟢

- [ ] T-02, Implement `CustomerDebt::record(Customer, type, amount, orderId=null, note=null, createdBy=null)`: open a DB transaction, `lockForUpdate` the customer, adjust `debt_total` (`−amount` for `repayment`, else `+amount`), throw on a repayment `> debt_total` with the over-balance message, insert one ledger row with `balance_after`, and `save()` the customer.
  - Origem no legado: `app/Models/CustomerDebt.php:38-64`
  - Critério de pronto: a `manual_debt` increases and a `repayment` decreases `debt_total`; the new row's `balance_after` equals the post-mutation balance; a repayment above the balance throws and rolls back with no row written.
  - Confiança: 🟢

- [ ] T-03, Implement `CustomerController::debt($id)`: set header `Công nợ` + breadcrumb, `Customer::findOrFail($id)` (404), read live `debt_total`, and `CustomerDebt::with('order')->where('customer_id',$id)->orderBy('id','desc')->paginate(20)`.
  - Origem no legado: `CustomerController.php:263-280`
  - Critério de pronto: a valid id renders the page with the live balance and the paginated ledger (20/page, id desc); `pos_debt` rows show the order without a per-row query; unknown/soft-deleted id → 404.
  - Confiança: 🟢

- [ ] T-04, Render `pages.customer-debt` with `compact('item','debtTotal','debtOrders')`, including the repayment modal (posts to `customers.repayments.store`) and the manual-debt modal (posts to `customers.debts.store`), plus the type-labelled ledger table with sign and `balance_after` columns.
  - Origem no legado: `CustomerController.php:279`, `resources/views/pages/customer-debt.blade.php`
  - Critério de pronto: the page shows the balance, both modals wired to the correct named routes, and a row per ledger entry with the right type label and +/− sign.
  - Confiança: 🟢

- [ ] T-05, Implement `CustomerController::storeDebt($customerId, Request)`: validate `amount => required|numeric|min:0.01`, `note => nullable|string`; `findOrFail`; call `CustomerDebt::record($customer,'manual_debt',(float)$amount,null,$note ?? null,Admin::user()?->id)`; then `admin_toastr('Ghi nợ thành công')` + `redirect()->back()`. No try/catch.
  - Origem no legado: `CustomerController.php:379-399`
  - Critério de pronto: a valid POST adds a `manual_debt` row increasing `debt_total`, stamps `created_by`, and redirects back with the success toastr.
  - Confiança: 🟢

- [ ] T-06, Implement `CustomerController::storeRepayment($customerId, Request)`: same validation + `findOrFail`; wrap `CustomerDebt::record($customer,'repayment',(float)$amount,null,$note ?? null,Admin::user()?->id)` in try/catch; on exception `redirect()->back()->withErrors($e->getMessage())`; on success `admin_toastr('Thu nợ thành công')` + `redirect()->back()`.
  - Origem no legado: `CustomerController.php:401-425`
  - Critério de pronto: a valid repayment decreases `debt_total`; a repayment above the balance leaves the balance unchanged and redirects back with the over-balance error.
  - Confiança: 🟢

- [ ] T-07, Ensure the two POST forms carry the CSRF token (Laravel `Form::open` / `@csrf`) and the ledger `paginate` links append the current query string.
  - Origem no legado: `resources/views/pages/customer-debt.blade.php`
  - Critério de pronto: the modals submit with a valid CSRF token; pagination links preserve `?page=`.
  - Confiança: 🟢

- [ ] T-08, Enforce the admin auth precondition via the `['web','admin']` route group (inherited, not re-declared here).
  - Origem no legado: `routes/web.php:23-28`
  - Critério de pronto: an unauthenticated request to any of the three routes → `302` to `auth/login`.
  - Confiança: 🟢

## Tarefas de Teste

- [ ] TT-01, Happy path — manual debt: `POST /customers/{id}/debts amount=100` on a customer at `50` → balance `150`, one `manual_debt` row with `balance_after=150` and `created_by` set (see `requirements.md`, Acceptance Criteria).
- [ ] TT-02, Happy path — repayment: `POST /customers/{id}/repayments amount=30` on a customer at `150` → balance `120`, one `repayment` row with `balance_after=120`.
- [ ] TT-03, Error path — over-balance repayment: `amount > debt_total` → no row, balance unchanged, back-with-errors carrying the over-balance message.
- [ ] TT-04, Validation: `amount=0`, negative, or non-numeric on either write → validation error, no ledger row.
- [ ] TT-05, Concurrency: two simultaneous repayments on the same customer cannot both succeed past the balance; assert the second re-reads the reduced balance under the lock (no overdraw).
- [ ] TT-06, Ledger page: a customer with 25 entries shows 20 on page 1 / 5 on page 2, id desc; a `pos_debt` entry whose order was deleted renders the note / "Đơn hàng đã bị xóa" (order is null).
- [ ] TT-07, Not found: unknown/soft-deleted id on any of the three routes → HTTP `404`.
- [ ] TT-08, Auth: an unauthenticated request → `302` to `auth/login`.

## Tarefas de Migração de Dados (se aplicável)

- [ ] TM-01, Preserve existing `customer_debts` rows and each customer's `debt_total` exactly on reimplementation — the balance is denormalised and must stay equal to the running ledger; do not recompute/round in a way that diverges from the stored `balance_after` trail. 🟡

## Ordem Sugerida

1. T-02 (`CustomerDebt::record`) is the foundation — both write endpoints and the balance invariant depend on it; implement and test it first.
2. T-01 (routes) → T-03/T-04 (ledger page + view) so the balance and history are visible.
3. T-05 (manual debt) then T-06 (repayment) — repayment reuses the same primitive plus the try/catch guard.
4. T-07 (CSRF/pagination) and T-08 (auth, inherited from the route group) round out the surface.

## Lacunas Pendentes (🔴)

- 🟡 **Amount minimum vs decimal(15,1) precision** (T-04/T-05/T-06) — `min:0.01` admits sub-0.1 amounts that round to `0.0` on storage; confirm the intended monetary granularity and tighten the rule if ₫ amounts should be whole numbers.
- 🟡 **Manual debt has no upper bound / no confirmation** (T-05) — a mistyped large `manual_debt` inflates the balance irreversibly except via a repayment/`debt_void`; decide whether a confirmation or cap is wanted.
- 🟡 **`created_by` is an unconstrained integer** (T-05/T-06) — no FK to `admin_users`; decide whether to constrain it or accept the loose audit link.
- 🟡 **No per-record authorization** — any admin writes debt to any customer (ADR-0009); confirm this matches the intended access model.
- 🔴 **Observability absent** — no logging/metrics/audit beyond the ledger row on a money-mutating path; decide whether to add an audit log and alerting on rejected repayments/transaction failures during reimplementation.
