# Customers CRUD — Contracts

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

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

External HTTP contract exposed by the `customers-crud` unit — the Laravel resource `resource('/customers', 'CustomerController')` under the admin group (`['web','admin']`, empty admin prefix). All actions require an authenticated admin session; unauthenticated requests get `302 → auth/login`. Reads/forms return **HTML** (Blade `pages.customer*`) with a redirect-back-on-write convention; `store` returns **JSON** when the request expects JSON (POS quick-add), and `destroy` always returns **JSON**. 🟢 (`routes/web.php:62`, `CustomerController.php`)

> The nine sibling `/customers/*` routes (`scan`, `{customer}/orders`, `check-gift`, `gift-received`, `statistic`, `debt`, `POST redeem-points`, `POST debts`, `POST repayments`) are declared **before** this resource so they are not captured by the resource `show`/`update`; they belong to the `customers-scan`, `customers-purchase-history`, `customers-statistics`, `customers-debt-actions` and `customers-loyalty` units and are documented there. 🟢 (`routes/web.php:53-62`)

---

## GET `/customers` — listing 🟢 (`:27-49`)

- **Auth:** required; anonymous → `302 auth/login`.
- **Request (query string):**

  | Field | Type | Required | Notes |
  |-------|------|----------|-------|
  | `q` | string | ❌ | matches `phone` **exactly** OR `fullname LIKE %q%`, grouped so it stays AND-scoped to `month`. ✅ Fixed 2026-09-21 (was ungrouped). 🟢 (`:38-42`) |
  | `month` | int (1–12) | ❌ | filters `MONTH(birthday2) = month`; resets ordering and sorts by day-of-month ascending. 🟢 (`:41-45`) |
  | `page` | int | ❌ | standard Laravel paginator page. 🟢 (`:46`) |

- **Response:** `200` HTML `pages.customers` with a `LengthAwarePaginator` of ≤30 customers (`list`). 🟢 (`:46-48`)

## GET `/customers/create` — create form 🟢 (`:56-65`)

- **Auth:** required. **Response:** `200` HTML `pages.customer-add` (no tier list — the create form has no `type` selector). 🟢

## POST `/customers` — create (web form **and** POS quick-add) 🟢 (`:73-105`)

- **Auth:** required. **CSRF:** required for the web-form path. **Content negotiation:** the response format depends on `request()->expectsJson()`.
- **Request body:**

  | Field | Type | Required | Rule / Notes |
  |-------|------|----------|--------------|
  | `fullname` | string | ✅ | `required\|string` 🟢 |
  | `phone` | numeric | ✅ | `required\|numeric\|unique:customers,phone,NULL,id,deleted_at,NULL` (live-only) 🟢 |
  | `email` | string | ❌ | `nullable\|string` 🟢 |
  | `address` | string | ❌ | `nullable\|string` 🟢 |
  | `gender` | string | ❌ | `nullable\|string\|in:male,female,other` 🟢 |
  | `birthday` | string | ❌ | `nullable\|string\|regex d/m/yyyy`; parsed to `birthday2` (`Y-m-d`); parse failure → `birthday` nulled 🟡 |
  | `dependant` | string | ❌ | `nullable\|string` 🟢 |
  | `type` | — | ❌ | **not accepted** on create; defaults to `khach_le` via the model accessor 🟢 |

- **Responses:**
  - **JSON-expecting request** (POS quick-add): `200` with the created `Customer` as JSON on success; `400` with a `null` body if `Customer::create` returns falsy. 🟢 (`:93-98`)
  - **Web form:** `302` redirect to `customers.index` with `admin_toastr('Thêm thành công')` on success; `302` back to `customers.index` with input + `'Thêm thất bại'` error on falsy create. `422` (redirect back with errors) on validation failure. 🟢 (`:100-104`)
- Source: `store` (`:73-105`).

## GET `/customers/{id}` — show 🟢 (`:113-116`)

- **Empty stub** — the method has no body; the route exists but returns an empty response. No contract to rely on. 🟢

## GET `/customers/{id}/edit` — edit form 🟢 (`:124-137`)

- **Auth:** required. **Response:** `200` HTML `pages.customer-edit` with the `item` (`Customer::findOrFail`) and `types` (`Customer::$types`). `404` if the customer does not exist or is soft-deleted. 🟢

## PUT/PATCH `/customers/{id}` — update 🟢 (`:146-167`)

- **Auth:** required. **CSRF:** required.
- **Request body:**

  | Field | Type | Required | Rule / Notes |
  |-------|------|----------|--------------|
  | `fullname` | string | ✅ | `required\|string` 🟢 |
  | `phone` | string | ✅ | `required\|string\|unique:customers,phone,{id},id,deleted_at,NULL` (live-only, excludes self; `string` not `numeric` — differs from create) 🟡 |
  | `email` | string | ❌ | `nullable\|string` 🟢 |
  | `address` | string | ❌ | `nullable\|string` 🟢 |
  | `gender` | string | ❌* | `string\|in:male,female,other` (no `nullable`: an empty string fails; an absent field passes) 🟡 |
  | `birthday` | string | ❌ | `nullable\|string` — **not** re-parsed into `birthday2` on update 🟡 |
  | `dependant` | string | ❌ | `nullable\|string` 🟢 |
  | `type` | string | ❌ | `nullable\|in:khach_le,si_1,si_2` — the only action that sets the tier 🟢 |

- **Behavior:** `findOrFail`, `fill($valided)`, `save()`. `birthday2` is not recomputed. 🟡
- **Responses:**
  - `302` redirect **back** with `admin_toastr('Cập nhật thành công')` on success.
  - `302` back with input + `'Cập nhật thất bại'` if `save()` returns falsy.
  - `422` validation errors on invalid input.
  - `404` if the customer does not exist.
- Source: `update` (`:146-167`).

## DELETE `/customers/{id}` — soft-delete 🟢 (`:175-188`)

- **Auth:** required. **CSRF:** required. **Response:** JSON.
- **Behavior:** `Customer::destroy($id)` soft-deletes (SoftDeletes). No name-rename side effect (unlike `products`). 🟢
- **Responses:**

  ```json
  { "status": true,  "message": "<admin.delete_succeeded>" }   // on truthy result
  { "status": false, "message": "<admin.delete_failed>" }      // on falsy result
  ```

- Source: `destroy` (`:175-188`).

---

## Consumed contracts (owned by other units)

`customers-crud` calls no external services. Its persisted `customers` rows are **consumed by**:

| Reads | Consumer unit | Purpose |
|-------|---------------|---------|
| `customers` (`phone`, `fullname`) | `customers-scan`, `pos-terminal` | customer search / select2 add-to-cart |
| `customers.type` | `products-pricing` | resolve the per-tier wholesale price |
| `customers.points` | `customers-loyalty` | gift-redemption balance |
| `customers.debt_total` | `customers-debt-actions`, `debts` | A/R balance and ledger |
| `customers` (FK) | `orders-*` | order → customer association |
| `customers` counts | `dashboard`, `customers-statistics` | KPIs and rollups |

The POS terminal calls **`POST /customers` (JSON)** as a *producer* of this contract (quick-add), not as a consumer of another unit. 🟢 (`:93-98`; cross-ref `pos-terminal`)

---

## Cross-cutting contract notes

- **Method surface:** the full resource verb set (`index/create/store/show/edit/update/destroy`); `show` is an empty stub. 🟢 (`routes/web.php:62`)
- **Content types:** HTML for reads/forms, `302` redirect-back for web writes, JSON for the `store` quick-add path and for `destroy`. 🟢
- **CSRF:** required on `POST`/`PUT`/`PATCH`/`DELETE` (web middleware group). 🟢 (`routes/web.php:24-28`)
- **Identity key:** `phone` (not `id`) is the customer's business identity — the search token, the POS `select2` value, and `scopeCode`. 🟢 (`:39`, `Customer.php:58-61`)
- **Soft-delete semantics:** deleted customers vanish from every default-scoped query; their `phone`/`email` become reusable (live-only unique indexes). No rename. 🟢 (`Customer.php:9-12`, `:78,150`)
- **Tier default:** create never sets `type`; new customers are `khach_le` until edited. ✅ Confirmed intentional with the team (2026-09-21). 🟢 (`:76-84`)
- **Balances are read-only here:** `points` and `debt_total` are `fillable` but never accepted by these validators; do not treat this contract as a way to set them. 🟢 (`Customer.php:15`)
