# Debts Specification

## Purpose

The Debts capability exposes a read-only accounts-receivable overview screen (`GET /debts`) that lists every customer currently carrying a positive outstanding balance, ordered largest-first, with optional name/phone search and 30-per-page pagination. It performs no writes; all mutations are delegated to the `customers-debt-actions` unit.

## Requirements

### Requirement: Admin Authentication Required

The system SHALL reject any unauthenticated request to `GET /debts` with an HTTP `302` redirect to `auth/login`, enforced by the admin route-group middleware `['web','admin']`. (Implemented in `routes/web.php:23-27,67`)

#### Scenario: Unauthenticated request is redirected

- **GIVEN** a client that has no authenticated admin session
- **WHEN** the client sends `GET /debts`
- **THEN** the system responds with HTTP `302` and a `Location` header pointing to `auth/login`

#### Scenario: Authenticated request is served

- **GIVEN** a client that holds a valid authenticated admin session
- **WHEN** the client sends `GET /debts`
- **THEN** the system responds with HTTP `200` and the accounts-receivable HTML page

---

### Requirement: Only Positive-Balance Customers Are Listed

The system SHALL include in the debtor list only customers whose `debt_total` is strictly greater than zero; customers with a zero or negative balance SHALL NOT appear. Soft-deleted customers SHALL be excluded automatically via the `SoftDeletes` default scope. (Implemented in `app/Http/Controllers/DebtController.php:17-18`, `app/Models/Customer.php:6,12`)

#### Scenario: Zero-balance customer is excluded

- **GIVEN** customers with `debt_total` values of `0`, `50`, and `120`
- **WHEN** an authenticated admin requests `GET /debts`
- **THEN** the response lists exactly two rows — the customers with balances `50` and `120` — and the zero-balance customer does not appear

#### Scenario: Soft-deleted customer is excluded

- **GIVEN** a customer with `debt_total = 80` whose record has a non-null `deleted_at`
- **WHEN** an authenticated admin requests `GET /debts`
- **THEN** that customer does not appear in the list

---

### Requirement: Debtor List Is Ordered by Outstanding Balance Descending

The system SHALL order the debtor list by `debt_total` descending so the customer with the largest outstanding balance appears first. (Implemented in `app/Http/Controllers/DebtController.php:18`)

#### Scenario: Rows appear largest-balance-first

- **GIVEN** three customers with `debt_total` values of `50`, `120`, and `75`
- **WHEN** an authenticated admin requests `GET /debts`
- **THEN** the rows are rendered in the order `120`, `75`, `50`

---

### Requirement: Debtor List Is Paginated at Thirty Records Per Page

The system SHALL paginate the debtor list to thirty records per page using `paginate(30)`, honoring the `page` query parameter. (Implemented in `app/Http/Controllers/DebtController.php:26`)

#### Scenario: First page contains thirty rows when more exist

- **GIVEN** thirty-five customers each with `debt_total > 0`
- **WHEN** an authenticated admin requests `GET /debts`
- **THEN** the response contains exactly thirty debtor rows and a pagination control indicating a second page

#### Scenario: Second page contains the remainder

- **GIVEN** thirty-five customers each with `debt_total > 0`
- **WHEN** an authenticated admin requests `GET /debts?page=2`
- **THEN** the response contains exactly five debtor rows

#### Scenario: Out-of-range page renders the empty state

- **GIVEN** ten customers each with `debt_total > 0`
- **WHEN** an authenticated admin requests `GET /debts?page=99`
- **THEN** the response is HTTP `200` showing the empty-state row and no debtor rows

---

### Requirement: Optional Search Filters by Phone or Full Name Within the Debtor Scope

When the `q` query parameter is present and non-empty, the system SHALL restrict the debtor list to customers whose `phone LIKE %q%` OR `fullname LIKE %q%`, with the OR wrapped in a grouped closure so the `debt_total > 0` predicate remains AND-scoped against the whole OR group. When `q` is absent or empty, the system SHALL return the full debtor list without additional filtering. (Implemented in `app/Http/Controllers/DebtController.php:20-24`)

#### Scenario: Search narrows the list by phone

- **GIVEN** a debtor whose phone contains `"090"` and another debtor whose phone does not
- **WHEN** an authenticated admin requests `GET /debts?q=090`
- **THEN** only the matching debtor appears

#### Scenario: Search narrows the list by full name

- **GIVEN** a debtor whose `fullname` contains `"Nguyen"` and another whose does not
- **WHEN** an authenticated admin requests `GET /debts?q=Nguyen`
- **THEN** only the matching debtor appears

#### Scenario: Zero-balance customer matching the search term is still excluded

- **GIVEN** a customer with `debt_total = 0` whose `fullname` contains `"Tran"`, and no debtor whose name contains `"Tran"`
- **WHEN** an authenticated admin requests `GET /debts?q=Tran`
- **THEN** no rows appear; the `debt_total > 0` constraint is not overridden by the search

#### Scenario: Absent q returns the full debtor list

- **GIVEN** five customers with `debt_total > 0`
- **WHEN** an authenticated admin requests `GET /debts` with no `q` parameter
- **THEN** all five debtors are listed

---

### Requirement: Empty Debtor List Renders Without Error

When no customer has `debt_total > 0` (or no customer matches the active search within the debtor scope), the system SHALL respond with HTTP `200` and render the empty-state indicator rather than an error. (Implemented in `resources/views/pages/debts.blade.php`)

#### Scenario: No debtors exist

- **GIVEN** no customer has `debt_total > 0`
- **WHEN** an authenticated admin requests `GET /debts`
- **THEN** the response is HTTP `200` and the page renders the empty-state row with no debtor rows

---

### Requirement: Each Debtor Row Links to the Customer Debt Ledger

The system SHALL render each debtor row as a navigable link to the `customers.debt` route for that customer (`GET /customers/{id}/debt`), owned by the `customers-debt-actions` unit. (Implemented in `resources/views/pages/debts.blade.php`)

#### Scenario: Row click navigates to the ledger

- **GIVEN** a debtor row is displayed on the accounts-receivable page
- **WHEN** the authenticated admin clicks the row
- **THEN** the browser navigates to `GET /customers/{id}/debt` for that customer's `id`

---

### Requirement: Outstanding Balance Is Displayed with One Decimal Place in VND

The system SHALL display each debtor's outstanding balance formatted as `number_format(debt_total, 1) ₫` (one decimal place followed by the Vietnamese dong symbol). (Implemented in `resources/views/pages/debts.blade.php`)

#### Scenario: Balance rendered with one decimal and currency symbol

- **GIVEN** a debtor with `debt_total = 1200.5`
- **WHEN** the accounts-receivable page is rendered
- **THEN** the balance cell displays `"1,200.5 ₫"`

---

### Requirement: The Controller Performs No Writes

The system SHALL treat `GET /debts` as a strictly read-only action; `DebtController::index` SHALL issue no database writes, no state mutations, and no calls to external write endpoints. All mutation entry points (repayment modal, manual-debt modal) SHALL target routes owned by the `customers-debt-actions` unit. (Implemented in `app/Http/Controllers/DebtController.php:10-29`)

#### Scenario: Repayment modal posts to the customers-debt-actions route

- **GIVEN** an authenticated admin on the accounts-receivable page who selects a customer in the repayment modal and submits
- **WHEN** the form is submitted
- **THEN** the browser sends `POST /customers/{id}/repayments` (owned by `customers-debt-actions`) and `DebtController` receives no write request

#### Scenario: Manual-debt modal posts to the customers-debt-actions route

- **GIVEN** an authenticated admin on the accounts-receivable page who selects a customer in the manual-debt modal and submits
- **WHEN** the form is submitted
- **THEN** the browser sends `POST /customers/{id}/debts` (owned by `customers-debt-actions`) and `DebtController` receives no write request
