# API Contract: List Customers

**Endpoint**: `GET /api/v1/customers`
**Permission**: `customers.view` (Spatie, `admin` guard)
**Auth**: `Authorization: Bearer {token}` (Sanctum)

## Request

### Headers

| Header | Required | Value |
|---|---|---|
| `Authorization` | Yes | `Bearer {sanctum_token}` |
| `Accept` | Yes | `application/json` |

### Query Parameters

| Param | Type | Required | Default | Constraints | Description |
|---|---|---|---|---|---|
| `search` | string | No | `null` | max 150 chars | Matches `name` OR `phone` using `ILIKE %term%` (case-insensitive, partial). |
| `from_date` | date (Y-m-d) | No | `null` | format `YYYY-MM-DD` | Filter: customer's `first_contract_date >= from_date`. Stored in the MV. |
| `to_date` | date (Y-m-d) | No | `null` | format `YYYY-MM-DD`; `>= from_date` if both present | Filter: customer's `first_contract_date <= to_date`. Stored in the MV. |
| `per_page` | integer | No | `20` | 1–100 | Page size. |
| `cursor` | string | No | `null` | opaque base64 | Cursor for the next page (returned in `meta.next_cursor` of the previous response). |

### Example Request

```http
GET /api/v1/customers?search=أحمد&from_date=2025-01-01&to_date=2026-12-31&per_page=20 HTTP/1.1
Authorization: Bearer {token}
Accept: application/json
```

## Success Response

### 200 OK

```json
{
  "success": true,
  "message": "قائمة العملاء",
  "data": [
    {
      "id": 42,
      "name": "أحمد محمد",
      "phone": "0912345678",
      "avatar": "avatars/abc123.jpg",
      "avatar_url": "http://host/storage/avatars/abc123.jpg",
      "first_contract_date": "2025-03-15",
      "next_due_date": "2026-07-01",
      "total_remaining_amount": "1500.00"
    },
    {
      "id": 17,
      "name": "أحمد علي",
      "phone": "0987654321",
      "avatar": null,
      "avatar_url": null,
      "first_contract_date": "2024-11-01",
      "next_due_date": "2026-06-15",
      "total_remaining_amount": "3200.50"
    },
    {
      "id": 99,
      "name": "سامي خالد",
      "phone": "0955512345",
      "avatar": null,
      "avatar_url": null,
      "first_contract_date": null,
      "next_due_date": null,
      "total_remaining_amount": null
    }
  ],
  "meta": {
    "path": "http://host/api/v1/customers",
    "per_page": 20,
    "next_cursor": "eyJ...",   // null if no more pages
    "prev_cursor": null
  }
}
```

### Ordering

Records are returned ordered by `next_due_date ASC NULLS LAST`, with a secondary `client_id ASC` for deterministic pagination. Customers with `next_due_date = NULL` (no active or completed contracts) appear at the end.

### Notes

- `total_remaining_amount` is the sum of installment amounts with `status IN ('pending', 'overdue')` for all the customer's `active` and `completed` contracts, pre-computed by the MV.
- Data may be up to 5 minutes stale.
- The response does NOT include a `total` count (that's the whole point of cursor pagination).

## Error Responses

### 401 Unauthorized

```json
{ "success": false, "message": "Unauthenticated." }
```

### 403 Forbidden (missing `customers.view` permission)

```json
{ "success": false, "message": "You do not have permission to perform this action." }
```

### 422 Unprocessable Entity (bad query params)

```json
{
  "success": false,
  "message": {
    "per_page": ["يجب أن لا يتجاوز عدد العناصر في الصفحة 100."],
    "to_date": ["يجب أن يكون تاريخ النهاية بعد أو يساوي تاريخ البداية."]
  }
}
```

## Server-Side Behavior

1. Validate query params via `ListCustomersRequest`.
2. Build query on `CustomerListingMv`:
   - `when($search, fn($q) => $q->search($search))`
   - `when($fromDate, fn($q) => $q->firstContractFrom($fromDate))`
   - `when($toDate, fn($q) => $q->firstContractTo($toDate))`
   - `->orderByNextDue()` (encapsulates `ORDER BY next_due_date ASC NULLS LAST, client_id ASC`)
3. `->cursorPaginate($perPage)`.
4. Transform via `CustomerListResource::collection($paginator->items())`.
5. Build `meta` from the paginator: `path`, `per_page`, `next_cursor`, `prev_cursor`.
6. Return `success(data: $items, msg: 'قائمة العملاء', meta: $meta)`.

## Test Cases (Feature Tests)

| # | Scenario | Expected |
|---|---|---|
| 1 | GET with no params | 200, paginated list ordered by `next_due_date ASC NULLS LAST` |
| 2 | GET with `search=أحمد` | 200, list filtered to clients with `name` or `phone` containing `أحمد` (case-insensitive) |
| 3 | GET with `from_date=2025-01-01&to_date=2026-12-31` | 200, list filtered to clients whose `first_contract_date` is in range |
| 4 | GET with `from_date` only | 200, no upper-bound filter applied |
| 5 | GET with `to_date` only | 200, no lower-bound filter applied |
| 6 | GET with `from_date > to_date` | 422 validation error |
| 7 | GET with `per_page=200` | 422 (max 100) |
| 8 | GET with `per_page=20&cursor=<next>` | 200, second page returned |
| 9 | GET with `cursor=<last page cursor>` | 200, `next_cursor` is null |
| 10 | GET on a customer with no active/completed contracts | The customer appears in the list with `next_due_date: null`, ordered at the end |
| 11 | GET as a user without `customers.view` | 403 |
| 12 | GET without `Authorization` | 401 |
| 13 | Verify the response data is read from the MV (not a live JOIN) | Database query log assertion: only one SELECT on `customer_listing_mv`; no JOIN of `clients` + `contracts` + `installments` |
| 14 | Verify the response time stays < 1 s for 10,000 seeded customers | Performance assertion (manual or `assertLessThan(1000, $elapsedMs)`) |
