# SPECIFICATIONS/06_API/06_02_CUSTOMERS_ENDPOINTS.md

**PURPOSE:** Specifies the customer management API surface.

> **Business rules enforced by these endpoints:** `../01_BUSINESS_CONTEXT/03_BUSINESS_RULES.md` → BR-001-*
> **Computed field formulas:** `../05_ALGORITHMS/04_COMPUTED_FIELDS.md` → Sections 5.3, 5.5
> **Performance strategy for list endpoint:** `../03_ARCHITECTURE/03_PERFORMANCE_STRATEGY.md`
> **Data minimization rules:** `../../CONSTITUTIONS/07_BOUNDARY_LAYERS.md` → Controller Confinement

---

## GET /api/v1/customers

**Permission:** `customers.view`

**Inputs (Query Params):**

| Field | Type | Default | Validation |
|:---|:---|:---|:---|
| `search` | string | null | max 150 chars |
| `from_date` | date (Y-m-d) | null | Valid date |
| `to_date` | date (Y-m-d) | null | Valid date |
| `sort_by` | string | `nearest_due_installment` | Exact match |
| `sort_direction` | string | `asc` | `asc` or `desc` |
| `status_filter` | string | null | `overdue`, `due`, `fully_paid`, `no_installments` |
| `per_page` | integer | 20 | 1 to 100 |
| `cursor` | string | null | Valid cursor string |

**Outputs:**
```json
{
    "success": true,
    "message": "string",
    "data": [
        {
            "id": 1,
            "name": "string",
            "phone": "string",
            "avatar_url": "string|null",
            "next_due_date": "date|null",
            "next_due_product_name": "string|null",
            "total_remaining_amount": "0.00"
        }
    ],
    "meta": {
        "current_cursor": "string|null",
        "has_more": false,
        "per_page": 20
    }
}
```

**Behavior:**
- Data source: `customer_listing_mv` (pre-computed, not live query)
- Customers without active installments (`next_due_date = null`) always appear last regardless of sort direction
- When primary sort values are equal, `client_id` is used as secondary sort (same direction) for pagination stability
- Time filtering uses `first_contract_date` stored in the MV
- Data may be up to 5 minutes stale
- `client_type_flags` and `reference_number` are strictly excluded from this response

**Status filter values:**

| Value | Meaning |
|:---|:---|
| `overdue` | Has at least one overdue installment |
| `due` | Has an installment due this month (not overdue) |
| `fully_paid` | All installments are paid (completed contracts) |
| `no_installments` | No active or completed contracts |

---

## POST /api/v1/customers

**Permission:** `customers.create`

**Inputs:**

| Field | Type | Required | Validation |
|:---|:---|:---|:---|
| `name` | string | Yes | — |
| `phone` | string | Yes | unique, regex: `^09\d{8}$` |
| `description` | text | No | nullable |
| `avatar` | file | No | image, jpg/jpeg/png/webp, max 5MB |

**Outputs:**
```json
{
    "success": true,
    "message": "string",
    "data": {
        "id": 6,
        "name": "string",
        "phone": "0912345678",
        "avatar_url": "string|null",
        "description": "string|null",
        "financial_summary": {
            "total_installment_amount": "0.00",
            "total_collected_amount": "0.00",
            "total_remaining_amount": "0.00",
            "total_pending_installments": 0,
            "total_overdue_installments": 0,
            "total_received_payments": 0,
            "total_number_installments": 0
        }
    }
}
```

**Behavior:**
- Creates client record with `customer` flag assigned immediately
- Generates `reference_number` automatically
- If avatar provided: stored securely, old file deleted on replace
- `financial_summary` returns all zeros at creation (no contracts yet)
- Triggers async refresh of `customer_listing_mv`

---

## GET /api/v1/customers/{id}

**Permission:** `customers.view`

**Inputs:** None (route param: `id`)

**Outputs:**
```json
{
    "success": true,
    "message": "string",
    "data": {
        "id": 1,
        "name": "string",
        "phone": "string",
        "avatar_url": "string|null",
        "description": "string|null",
        "financial_summary": {
            "total_installment_amount": "0.00",
            "total_collected_amount": "0.00",
            "total_remaining_amount": "0.00",
            "total_pending_installments": 0,
            "total_overdue_installments": 0,
            "total_received_payments": 0,
            "total_number_installments": 0
        }
    }
}
```

**Behavior:**
- Data source: live query (not Materialized View)
- Financial summary computed from all active/completed contracts for this customer
- `client_type_flags` and `reference_number` are strictly excluded from this response

---

## PUT /api/v1/customers/{id}

**Permission:** `customers.update`

**Inputs:**

| Field | Type | Required | Validation |
|:---|---|:---|:---|
| `name` | string | No | — |
| `phone` | string | No | unique (ignore current), regex: `^09\d{8}$` |
| `description` | text | No | nullable |
| `avatar` | file | No | image, jpg/jpeg/png/webp, max 5MB |

**Outputs:** Same shape as POST create response (with computed financial summary)

**Behavior:**
- Allowed even if the customer has active contracts
- If new avatar provided: old avatar deleted securely, new one stored
- Never modifies `client_type_flags` or `reference_number`
- At least one field must be provided
- Triggers async refresh of `customer_listing_mv`

---

## DELETE /api/v1/customers/{id}

**This endpoint does not exist.**

Customers are hard immutable entities (BR-001-1). Deletion is prohibited at both code and database levels.