# Orders-Scan — Implementation Tasks

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

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

## Prerequisites

- [ ] `Order` model with `$appends = ['code','is_editable','debt_locked']`, `customer()` belongsTo, and `products()` belongsToMany (`order_product` withPivot `qty,price,unit_id,conversion_qty`). 🟢 (`app/Models/Order.php:14,29-41`)
- [ ] `Customer` model reachable from `Order::customer()` and searchable by `phone`/`fullname`. 🟢 (`app/Models/Customer.php`)
- [ ] Admin route group `['web','admin']`; `/orders/scan` registered **before** `resource('/orders')`. 🟢 (`routes/web.php:72-75`)

## Tasks

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

- [ ] T-01, Register `GET /orders/scan` → `OrderController@scan` in the admin group, **before** the `/orders` resource so it is not shadowed by the resource `show` route.
  - Legacy origin: `routes/web.php:72`
  - Done when: `GET /orders/scan` resolves to `scan()` for an authenticated admin; anonymous → `302` to `auth/login`.
  - Confidence: 🟢

- [ ] T-02, Implement `scan()`: `$list = Order::query()`; apply the optional filters; then `->orderBy('id','desc')->with('customer','products')->take(10)->get()` and return `response()->json($result)`.
  - Legacy origin: `OrderController.php:421,430-433`
  - Done when: the response is a `200` JSON array of ≤ 10 orders, newest first, each with nested `customer` and `products` (pivot included).
  - Confidence: 🟢

- [ ] T-03, Apply the optional `q` filter as `whereHas('customer', fn => where('phone','like','%q%')->orWhere('fullname','like','%q%'))` (OR grouped inside the `whereHas`), only when `request('q')` is truthy.
  - Legacy origin: `OrderController.php:422-426`
  - Done when: `?q=` returns only orders whose related customer's phone/name matches; customerless orders never appear in a `q` result; absence of `q` skips the filter.
  - Confidence: 🟢

- [ ] T-04, Apply the optional `status` filter (`where('status',$status)`) when `request()->status` is present.
  - Legacy origin: `OrderController.php:427-429`
  - Done when: `?status=draft` narrows to draft orders (the POS resume-draft case).
  - Confidence: 🟢

- [ ] T-05, Serialise the appended attributes (`code`, `is_editable`, `debt_locked`) on each order so the client can gate editability.
  - Legacy origin: `app/Models/Order.php:14,49-67`
  - Done when: each JSON object carries `code = #QT78-{id}`, `is_editable`, and `debt_locked`.
  - Confidence: 🟢

- [ ] T-06, (Cleanup, matches legacy behaviour) Preserve the always-`200`-array contract: the empty case returns `[]`, not `204`.
  - Legacy origin: `OrderController.php:432-436` (dead `204` branch)
  - Done when: an empty match returns `200 []`; the unreachable `204` branch is removed or explicitly documented.
  - Confidence: 🟢

- [ ] T-07, (Improvement, not in legacy) Add observability — log/metric on the lookup (query latency, result count).
  - Legacy origin: `OrderController.php:419-437` (absence)
  - Done when: each request emits a structured log or metric.
  - Confidence: 🔴

## Test Tasks

- [ ] TT-01, Cap + order: with 12 orders, `GET /orders/scan` returns the 10 newest by `id` desc, each with `customer` and `products` (pivot `qty/price/unit_id/conversion_qty`) — see `requirements.md`, Acceptance Criteria.
- [ ] TT-02, Status filter: `?status=draft` returns only drafts.
- [ ] TT-03, Customer search: `?q=<phone>` returns the matching customer's order and excludes a walk-in order (whereHas requires a customer).
- [ ] TT-04, Empty result: a non-matching filter returns `200 []` (not `204`).
- [ ] TT-05, Grouping guard: a `q` + `status` combination keeps the OR inside the `whereHas` (no status-filter leak).
- [ ] TT-06, Auth: anonymous `GET /orders/scan` → `302` to `auth/login`.

## Data Migration Tasks (if applicable)

- None. Read-only projection over `orders`/`order_product`/`customers`; introduces no schema. 🟢 (`data-dictionary.md:200-237`)

## Suggested Order

1. T-01 → T-02 (route + core query/response).
2. T-03 → T-05 (filters + appended attributes).
3. T-06 (empty-case contract), then T-07 (observability).

## Pending Gaps (🔴)

- **No observability (T-07):** a slow EXISTS search or empty picker produces no signal.
- **Dead `204` branch:** unreachable (`Collection` always truthy) — decide whether to remove or make the empty case explicit. (`OrderController.php:432-436`)
- **Customerless orders excluded from `q`:** confirm the `whereHas`-only search is the intended behaviour for the POS draft picker. (`:423`)
