# Customers Scan — Implementation Tasks

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

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

## Prerequisites

- [ ] The `Customer` model exists with SoftDeletes and the `type`/`type_label` appended attributes (see `customers-crud`). 🟢 (`Customer.php:9-12,25,49-55`)
- [ ] The `customers` table has `phone`, `fullname`, and `deleted_at` columns. 🟢 (data-dictionary)
- [ ] The admin route group (`['web','admin']`, `config('admin.route.*')`) is registered so the endpoint sits behind admin auth. 🟢 (`routes/web.php:23-28`)
- [ ] The route `GET /customers/scan` is declared **before** `resource('/customers')` so it is not shadowed by the resource `show` route. 🟢 (`routes/web.php:53,62`)

## Tasks

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

- [ ] T-01, Register `GET /customers/scan` → `CustomerController@scan` inside the admin route group, above `resource('/customers')`.
  - Legacy origin: `routes/web.php:53,62`
  - Done when: hitting `/customers/scan` reaches `scan()` and `/customers/{id}` still reaches the resource `show`.
  - Confidence: 🟢

- [ ] T-02, Read the query token from the request: `$q = request('q')` (no validation, no default).
  - Legacy origin: `CustomerController.php:285`
  - Done when: a request with no `q` yields `$q === null` and does not error.
  - Confidence: 🟢

- [ ] T-03, Build the search: `Customer::where('phone','like',"%$q%")->orWhere('fullname','like',"%$q%")->take(10)->get()`.
  - Legacy origin: `CustomerController.php:287`
  - Done when: a token matching ≥11 customers returns exactly 10; matching on either `phone` or `fullname` substring works.
  - Confidence: 🟢

- [ ] T-04, Return the collection as JSON: `return response()->json($result)` — always a `200` array (empty `[]` when nothing matches).
  - Legacy origin: `CustomerController.php:289-291`
  - Done when: a no-match query returns HTTP `200` with body `[]`.
  - Confidence: 🟢

- [ ] T-05, Preserve the (unreachable) `204` branch verbatim OR consciously drop it — the team confirmed the dead `response()->json(null, 204)` is harmless and left as-is.
  - Legacy origin: `CustomerController.php:293`; `_reversa_sdd/flowcharts/customers.md:71`
  - Done when: reimplementation documents that the `204` path never executes (a `Collection` is always truthy) and matches the chosen decision.
  - Confidence: 🟢

- [ ] T-06, Ensure the serialized payload includes the appended `type` and `type_label` attributes so the POS client can bind the pricing tier without a second call.
  - Legacy origin: `Customer.php:15,25,49-55`
  - Done when: each result object exposes `type` (defaulting to `khach_le`) and `type_label`.
  - Confidence: 🟢

- [x] T-07 (verified, no action needed), Confirmed the SoftDeletes `deleted_at IS NULL` constraint already scopes both branches via Eloquent's automatic global-scope nesting (`Builder::callScope`) — no grouping fix required here, unlike `customers-crud`'s hand-written `month` filter.
  - Legacy origin: `CustomerController.php:287`; verified against `vendor/laravel/framework/.../Eloquent/Builder.php:943-962`
  - Done when: confirmed — a soft-deleted customer does not appear via either the phone or fullname path.
  - Confidence: 🟢

- [ ] T-08 (decision), Decide the empty/missing-`q` contract (return `[]` vs. the current first-10-rows behavior) and, if changed, short-circuit before the query.
  - Legacy origin: `CustomerController.php:285-287`
  - Done when: the agreed empty-`q` behavior is implemented and tested.
  - Confidence: 🔴

- [ ] T-09 (hardening), Add lightweight observability (debug log of `q` length + result count) to this per-keystroke endpoint.
  - Legacy origin: `CustomerController.php:283-294` (absence)
  - Done when: each call emits a structured debug log without logging full PII.
  - Confidence: 🔴

## Test Tasks

- [ ] TT-01, Happy path: `q` matching a customer's phone substring returns a `200` array containing that customer (see `requirements.md` Acceptance Criteria).
- [ ] TT-02, Happy path: `q` matching a `fullname` substring returns the customer.
- [ ] TT-03, Cap: seed ≥11 matching customers → response contains exactly 10.
- [ ] TT-04, Empty result: a non-matching `q` returns `200` with body `[]` (never `204`).
- [ ] TT-05, Auth: an unauthenticated request is redirected (`302`) to `auth/login`.
- [ ] TT-06, Soft-delete via fullname: a soft-deleted customer does not appear when matched only by name.
- [ ] TT-07, A soft-deleted customer matched by phone must be absent from results (verified: Eloquent's scope-nesting already guarantees this).
- [ ] TT-08, Payload shape: a result object includes `type` and `type_label`.

## Data Migration Tasks

- Not applicable — read-only endpoint, no schema or data changes. 🟢

## Suggested Order

1. T-01 → T-02 → T-03 → T-04 (the working endpoint), then T-06 (payload assertion).
2. T-05 documents the dead-branch decision.
3. T-08 is a human-decision gate — resolve before finalizing behavior (T-07 is already verified, no decision needed).
4. T-09 hardening can land independently.

## Pending Gaps (🔴)

- **T-08 — empty/missing `q` contract.** A blank query currently returns the first 10 rows rather than `[]`; needs a product/lead decision.
- **T-09 — observability.** No logging/metrics on a per-keystroke hot path; decide the acceptable telemetry (mind customer PII).
