# Customers CRUD — Implementation Tasks

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

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

## Prerequisites

- [ ] `Customer` model with SoftDeletes, `$fillable` (`fullname, phone, address, gender, birthday, email, dependant, birthday2, points, debt_total, type`), `debt_total` float cast, `birthday2`/`deleted_at` dates, `$types` map, `type`/`type_label` accessors, and `scopeCode` (`where('phone', …)`) (see the `customers` schema in `data-dictionary.md`).
- [ ] The admin route group / `admin` auth guard is in place (see `auth` unit).
- [ ] `admin_toastr()` and `trans('admin.*')` (Encore\Admin) available.
- [ ] `Carbon` available for `birthday` → `birthday2` parsing.
- [ ] `customers.phone` has a unique index over live rows (`unique(phone, deleted_at)`); `email` unique index likewise (per migrations).
- [ ] Blade views `pages.customers`, `pages.customer-add`, `pages.customer-edit` exist (presentation, out of scope here).

## Tasks

> Each task references the legacy file the behavior was extracted from.

- [ ] T-01, Register the `/customers` resource route **after** the nine named `/customers/*` routes so those (scan/orders/statistic/debt/loyalty) are not shadowed by the resource `show`/`update`.
  - Legacy source: `routes/web.php:53-62`
  - Done when: the named routes reach their actions and the seven resource verbs reach `CustomerController`.
  - Confidence: 🟢

- [ ] T-02, Implement `index`: select the listed columns, `orderBy('id','desc')`, `paginate(30)`, render `pages.customers`.
  - Legacy source: `app/Http/Controllers/CustomerController.php:27-49`
  - Done when: `GET /customers` returns ≤30 rows newest-first.
  - Confidence: 🟢

- [x] T-03, Add the `q` search: when present, match `phone` exactly OR `fullname LIKE %q%`, wrapped in a closure so it stays AND-scoped to any other filter.
  - Legacy source: `app/Http/Controllers/CustomerController.php:38-42`
  - Done when: `?q=` narrows by exact phone or name substring; combining with `?month=` does not let a phone match escape the month filter.
  - Confidence: 🟢 — ✅ Fixed 2026-09-21 (was an ungrouped `orWhere`).

- [ ] T-04, Add the `month` filter: when present, add the `date_format(birthday2,'%d') AS day` select, reset ordering, `whereMonth('birthday2','=',month)`, order by `day` asc.
  - Legacy source: `app/Http/Controllers/CustomerController.php:41-45`
  - Done when: `?month=9` returns customers with `birthday2` month 9 ordered by day ascending.
  - Confidence: 🟢

- [ ] T-05, Implement `create`: render `pages.customer-add` (no tier list). Use the correct "Customer" breadcrumb label (the legacy `__("Sản phẩm")` crumb is a mislabel).
  - Legacy source: `app/Http/Controllers/CustomerController.php:56-65`
  - Done when: `GET /customers/create` renders the add form; breadcrumb reads Customer.
  - Confidence: 🟢

- [ ] T-06, Implement `store` validation: `fullname` required, `phone` required + numeric + live-unique, `email`/`address`/`dependant` nullable string, `gender nullable|in:male,female,other`, `birthday nullable` + `d/m/yyyy` regex.
  - Legacy source: `app/Http/Controllers/CustomerController.php:76-84`
  - Done when: valid data passes; a duplicate live phone fails; a malformed `birthday` fails the regex.
  - Confidence: 🟢

- [ ] T-07, Implement `store` birthday parsing: parse `birthday` (`d/m/Y`) into `birthday2` (`Y-m-d`); on parse failure null `birthday`. Consider surfacing an error instead of silently dropping it (documented gap).
  - Legacy source: `app/Http/Controllers/CustomerController.php:86-90`
  - Done when: a valid `d/m/Y` birthday sets `birthday2`; an unparseable one does not crash the create.
  - Confidence: 🟡

- [ ] T-08, Implement `store` persistence + content negotiation: `Customer::create`, then if `expectsJson()` return JSON customer (or `400`/`null` on falsy), else `admin_toastr` + redirect `customers.index` (or redirect back with `'Thêm thất bại'` on falsy).
  - Legacy source: `app/Http/Controllers/CustomerController.php:92-104`
  - Done when: a web POST redirects with a toastr; a JSON-expecting POST returns the created customer JSON.
  - Confidence: 🟢

- [ ] T-09, Implement `edit`: `Customer::findOrFail($id)` + `$types = Customer::$types`, render `pages.customer-edit` with `item`, `types`.
  - Legacy source: `app/Http/Controllers/CustomerController.php:124-137`
  - Done when: the edit form renders pre-filled with the customer and the tier selector; a missing id → `404`.
  - Confidence: 🟢

- [ ] T-10, Implement `update`: validate (`phone` string+live-unique-except-id, `type nullable|in:…`, `gender in:…`), `findOrFail`, `fill` + `save`, toastr + redirect back (or back-with-errors on falsy). Recommend aligning `phone` rule with `store` and adding `nullable` to `gender` (documented gaps).
  - Legacy source: `app/Http/Controllers/CustomerController.php:146-167`
  - Done when: a valid `type` persists; a missing id → `404`.
  - Confidence: 🟢

- [ ] T-11, (Recommended) Recompute `birthday2` from `birthday` on `update` as `store` does, so the queryable date does not go stale after an edit (documented gap — not in current code).
  - Legacy source: `app/Http/Controllers/CustomerController.php:146-160` (absence) vs `:86-90`
  - Done when: editing `birthday` updates `birthday2` consistently.
  - Confidence: 🟡

- [ ] T-12, Implement `destroy`: `Customer::destroy($id)` (soft-delete, no rename), return `{status, message}` JSON via `trans('admin.delete_succeeded'|'delete_failed')`.
  - Legacy source: `app/Http/Controllers/CustomerController.php:175-188`
  - Done when: `DELETE /customers/{id}` soft-deletes and returns the JSON status.
  - Confidence: 🟢

- [ ] T-13, Leave `show` as an empty stub (the resource route exists but returns no body).
  - Legacy source: `app/Http/Controllers/CustomerController.php:113-116`
  - Done when: `GET /customers/{id}` returns an empty response (parity with the legacy behaviour) — or is intentionally redefined.
  - Confidence: 🟢

## Test Tasks

- [ ] TT-01, Happy path: `GET /customers` returns 30 rows newest-first; page 2 returns the remainder (see `requirements.md`, Acceptance Criteria).
- [ ] TT-02, `store` web form: valid POST creates a customer, parses `birthday`→`birthday2`, redirects with a success toastr; `type` defaults to `khach_le`.
- [ ] TT-03, `store` POS quick-add: POST with `Accept: application/json` returns the created customer JSON; a duplicate live phone fails validation.
- [ ] TT-04, `update`: setting `type=si_1` persists; `findOrFail` returns `404` for a missing id.
- [ ] TT-05, `destroy`: soft-deletes and returns `{status:true, message:<delete_succeeded>}`; the phone becomes reusable on a new create.
- [ ] TT-06, Search + month filter: `?q=<phone>&month=9` does not leak a phone match outside month 9 (guards the ungrouped-OR fix).
- [ ] TT-07, Auth: an unauthenticated request to any `/customers` action redirects (302) to `auth/login`.

## Data Migration Tasks (if applicable)

- [ ] TM-01, Ensure `customers` carries `birthday` (display string) and `birthday2` (nullable date), the unique `phone`/`email` indexes over live rows, and `type`/`points`/`debt_total` columns (see `data-dictionary.md`, `customers`).

## Suggested Order

1. T-01 (route order) → T-02..T-04 (index) — establishes the read path.
2. T-05..T-08 (create + POS quick-add) — the highest-traffic write path.
3. T-09..T-11 (edit/update) then T-12/T-13 (destroy, show).
4. Tests (TT-01..TT-07) alongside each group.

## Pending Gaps (🔴)

- ✅ Resolved (2026-09-21): `type` intentionally stays retail-only at create (not settable). T-06/T-08 unaffected. (`:76-84`)
