# Customers Debt Actions Specification

## Purpose

The Customers Debt Actions capability is the per-customer accounts-receivable (A/R) surface of the admin back office. It provides a read-only ledger page showing a customer's live debt balance and transaction history, and two write endpoints that append entries to the ledger — one to increase the balance (manual debt) and one to decrease it (repayment).

## Requirements

### Requirement: Debt Ledger Page Rendering

The system SHALL render an HTML ledger page for a resolved customer, displaying the customer's current debt balance and their paginated ledger history. (Implemented in `app/Http/Controllers/CustomerController.php:263-280`)

#### Scenario: Ledger page renders for a customer with entries

- **GIVEN** an authenticated admin and a customer who has existing ledger entries
- **WHEN** the admin requests `GET /customers/{customer}/debt`
- **THEN** the system responds with HTTP 200 and a page showing the customer's current debt balance and their ledger entries

#### Scenario: Ledger page renders for a customer with no entries

- **GIVEN** an authenticated admin and a customer who has no ledger entries
- **WHEN** the admin requests `GET /customers/{customer}/debt`
- **THEN** the system responds with HTTP 200 and a page indicating no entries exist for the customer

### Requirement: Live Balance Display

The system SHALL display the customer's current debt balance as it exists at the moment of the request, not a cached or nightly-computed snapshot. (Implemented in `app/Http/Controllers/CustomerController.php:272`, `app/Models/Customer.php:15-19`)

#### Scenario: Balance shown reflects the most recent mutation

- **GIVEN** an authenticated admin and a customer whose balance was updated by a write immediately before the request
- **WHEN** the admin requests `GET /customers/{customer}/debt`
- **THEN** the page shows the balance that results from that write, not an older value

### Requirement: Ledger Pagination

The system SHALL paginate ledger entries at 20 per page in descending entry-id order. (Implemented in `app/Http/Controllers/CustomerController.php:274-277`)

#### Scenario: Customer with more than 20 entries spans multiple pages

- **GIVEN** an authenticated admin and a customer with 25 ledger entries
- **WHEN** the admin requests `GET /customers/{customer}/debt`
- **THEN** the first page shows the 20 most recent entries and a second page is available with the remaining 5 entries

#### Scenario: Page query parameter selects a specific page

- **GIVEN** an authenticated admin and a customer with 25 ledger entries
- **WHEN** the admin requests `GET /customers/{customer}/debt?page=2`
- **THEN** the system responds with HTTP 200 showing the 5 remaining entries

### Requirement: Manual Debt Recording

The system SHALL append a `manual_debt` ledger entry and increase the customer's debt balance by the submitted amount when a valid manual debt is posted. (Implemented in `app/Http/Controllers/CustomerController.php:379-399`)

#### Scenario: Valid manual debt increases the customer's balance

- **GIVEN** an authenticated admin and a customer with a debt balance of 50
- **WHEN** the admin posts `POST /customers/{customer}/debts` with `amount=100`
- **THEN** the customer's debt balance becomes 150, one `manual_debt` ledger entry is created recording a post-mutation balance of 150, and the admin is redirected back with a success notification

#### Scenario: Optional note is stored with the manual debt entry

- **GIVEN** an authenticated admin and a customer
- **WHEN** the admin posts `POST /customers/{customer}/debts` with `amount=50` and `note="outstanding invoice"`
- **THEN** the new `manual_debt` entry stores the supplied note text

### Requirement: Repayment Recording

The system SHALL append a `repayment` ledger entry and decrease the customer's debt balance by the submitted amount when a valid repayment is posted and the amount does not exceed the current balance. (Implemented in `app/Http/Controllers/CustomerController.php:401-425`)

#### Scenario: Valid repayment decreases the customer's balance

- **GIVEN** an authenticated admin and a customer with a debt balance of 150
- **WHEN** the admin posts `POST /customers/{customer}/repayments` with `amount=30`
- **THEN** the customer's debt balance becomes 120, one `repayment` ledger entry is created recording a post-mutation balance of 120, and the admin is redirected back with a success notification

#### Scenario: Repayment exactly equal to the balance is accepted

- **GIVEN** an authenticated admin and a customer with a debt balance of 100
- **WHEN** the admin posts `POST /customers/{customer}/repayments` with `amount=100`
- **THEN** the customer's debt balance becomes 0, one `repayment` entry is created recording a post-mutation balance of 0, and the admin is redirected back with a success notification

#### Scenario: Optional note is stored with the repayment entry

- **GIVEN** an authenticated admin and a customer with a non-zero debt balance
- **WHEN** the admin posts `POST /customers/{customer}/repayments` with a valid `amount` and `note="paid in full"`
- **THEN** the new `repayment` entry stores the supplied note text

### Requirement: Over-Balance Repayment Rejection

The system SHALL reject a repayment whose amount exceeds the customer's current debt balance, leaving the balance and ledger unchanged and returning an error describing the excess. (Implemented in `app/Models/CustomerDebt.php:43-47`, `app/Http/Controllers/CustomerController.php:410-421`)

#### Scenario: Repayment above the current balance is refused

- **GIVEN** an authenticated admin and a customer with a debt balance of 100
- **WHEN** the admin posts `POST /customers/{customer}/repayments` with `amount=150`
- **THEN** no ledger entry is created, the debt balance remains 100, and the admin is redirected back with an error message indicating the repayment amount exceeds the current balance

### Requirement: Amount Validation on Writes

The system SHALL reject any write request where `amount` is absent, non-numeric, zero, or less than 0.01, before any ledger mutation occurs. (Implemented in `app/Http/Controllers/CustomerController.php:381-384,403-406`)

#### Scenario: Zero amount is rejected

- **GIVEN** an authenticated admin and a customer
- **WHEN** the admin posts to either write endpoint with `amount=0`
- **THEN** the system redirects back with a validation error and no ledger entry is created

#### Scenario: Negative amount is rejected

- **GIVEN** an authenticated admin and a customer
- **WHEN** the admin posts to either write endpoint with a negative `amount`
- **THEN** the system redirects back with a validation error and no ledger entry is created

#### Scenario: Non-numeric amount is rejected

- **GIVEN** an authenticated admin and a customer
- **WHEN** the admin posts to either write endpoint with a non-numeric `amount` value
- **THEN** the system redirects back with a validation error and no ledger entry is created

#### Scenario: Amount of exactly 0.01 is accepted

- **GIVEN** an authenticated admin and a customer
- **WHEN** the admin posts to either write endpoint with `amount=0.01`
- **THEN** the request proceeds to ledger mutation without a validation error

### Requirement: Customer Resolution

The system SHALL resolve the `{customer}` route parameter to an existing, non-deleted customer record, or respond with HTTP 404, on all three routes. (Implemented in `app/Http/Controllers/CustomerController.php:271,386,408`)

#### Scenario: Unknown customer id returns 404

- **GIVEN** an authenticated admin and a customer id that does not exist in the system
- **WHEN** the admin requests any of the three routes with that id
- **THEN** the system responds with HTTP 404

#### Scenario: Soft-deleted customer id returns 404

- **GIVEN** an authenticated admin and a customer id whose record has been soft-deleted
- **WHEN** the admin requests any of the three routes with that id
- **THEN** the system responds with HTTP 404

### Requirement: Serialised Concurrent Balance Mutations

The system SHALL serialise concurrent balance mutations for the same customer so that no two simultaneous writes can both pass the balance check for the same funds. (Implemented in `app/Models/CustomerDebt.php:38-64`)

#### Scenario: Concurrent repayments do not both succeed against the same balance

- **GIVEN** a customer with a debt balance of 100 and two simultaneous repayment requests each with `amount=80`
- **WHEN** both requests are processed concurrently
- **THEN** exactly one repayment succeeds and reduces the balance; the other is rejected with an over-balance error, and no overdraft results

### Requirement: Admin Authentication

The system SHALL require an authenticated admin session for all three routes and SHALL redirect unauthenticated requests to the login page. (Implemented in `routes/web.php:23-28,58-61`)

#### Scenario: Unauthenticated request is redirected to login

- **GIVEN** no active admin session
- **WHEN** any of the three routes is requested
- **THEN** the system responds with HTTP 302 redirecting to `auth/login`

### Requirement: CSRF Protection on Write Endpoints

The system SHALL require a valid CSRF token on both POST endpoints and SHALL reject requests that omit or supply an invalid token before any ledger mutation occurs. (Implemented in `resources/views/pages/customer-debt.blade.php`)

#### Scenario: POST without a CSRF token is rejected

- **GIVEN** an authenticated admin
- **WHEN** a POST is submitted to either write endpoint without a valid CSRF token
- **THEN** the system rejects the request and no ledger entry is created

### Requirement: Admin Provenance on Ledger Entries

The system SHALL record the acting admin's identifier on every ledger entry produced by a write, storing a null value when no admin identity can be resolved at the time of the write. (Implemented in `app/Http/Controllers/CustomerController.php:394,417`)

#### Scenario: Successful write stores the acting admin's identifier

- **GIVEN** an authenticated admin with a known identifier
- **WHEN** the admin successfully posts to either write endpoint
- **THEN** the new ledger entry carries that admin's identifier as its provenance field

### Requirement: Append-Only Ledger with Per-Entry Balance Snapshot

The system SHALL never modify or delete existing ledger entries; each successful write SHALL append exactly one new entry that records the resulting balance at the time of the mutation. (Implemented in `app/Models/CustomerDebt.php:38-64`, `database/migrations/2026_08_28_000001_create_customer_debts_table.php`)

#### Scenario: Prior entries are unchanged after a new write

- **GIVEN** a customer with existing ledger entries
- **WHEN** a successful manual debt or repayment is posted
- **THEN** all prior entries remain unchanged and exactly one new entry is appended, recording the resulting balance

#### Scenario: Ledger entry for a deleted order remains visible

- **GIVEN** a ledger entry originating from an order that has since been hard-deleted
- **WHEN** the ledger page is rendered
- **THEN** the entry remains visible with its stored balance snapshot intact, and the absent order reference is shown as missing rather than causing an error
