# Orders-Scan — Technical Design

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

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

## Interface

Single JSON GET endpoint, admin group (`['web','admin']`, empty admin prefix), no route name. 🟢 (`routes/web.php:72`)

| Method | Path | Input | Output | Status codes |
|--------|------|-------|--------|--------------|
| GET | `/orders/scan` | `q?: string`, `status?: draft\|done` (query) | `application/json` — array of `Order` (≤ 10) | 200, 302 (unauthenticated → `auth/login`) |

Controller symbol:

| Symbol | Signature | Return | Note |
|--------|-----------|--------|------|
| `OrderController::scan` | `()` | `JsonResponse` | Reads `q`/`status` via the `request()` helper (no method params). 🟢 (`:419-437`) |

## Main Flow 🟢 (`:419-437`)

1. `GET /orders/scan` reaches `scan()` through the admin group (route registered before `resource('/orders')` so it is not shadowed). 🟢 (`routes/web.php:72`)
2. `$list = Order::query()` — a fresh builder. 🟢 (`:421`)
3. If `$q = request('q')` is truthy: `->whereHas('customer', fn($query) => $query->where('phone','like',"%$q%")->orWhere('fullname','like',"%$q%"))`. The OR is inside the `whereHas` closure, so it becomes a single EXISTS-over-customer predicate. 🟢 (`:422-426`)
4. If `$status = request()->status` is present: `->where('status', $status)`. 🟢 (`:427-429`)
5. `->orderBy('id','desc')->with('customer','products')->take(10)->get()` into `$result`. 🟢 (`:430`)
6. `if ($result) return response()->json($result);` — always taken (a `Collection` is truthy even when empty → `[]`). The `response()->json(null, 204)` fallback never executes. 🟢 (`:432-436`)

## Alternative Flows

- **No `q`:** the `whereHas` block is skipped; customerless orders are eligible. 🟢 (`:422`)
- **No `status`:** the status filter is skipped. 🟢 (`:427`)
- **No matches:** `get()` returns an empty `Collection` → serialised as `[]` with `200` (not `204`). 🟢 (`:432-436`)
- **`q` matches an order with no customer:** impossible — `whereHas('customer')` requires a related customer, so walk-in orders never appear in a `q` result. 🟢 (`:423`)
- **Unauthenticated:** admin group middleware → `302 auth/login` before the action runs. 🟢 (`routes/web.php:24-28`)

## Response shape

Each element is a serialised `Order`: all table columns plus the appended `code` (`#QT78-{id}`), `is_editable`, `debt_locked`, and the eager-loaded relations:

- `customer`: the full `Customer` (or `null` for a walk-in). 🟢 (`Order.php:29-32`)
- `products`: an array of `Product`, each with a `pivot` object carrying `qty`, `price`, `unit_id`, `conversion_qty`. 🟢 (`Order.php:39-41`)

This is exactly the payload `pos-terminal` needs to repopulate a cart from a stored draft. 🟢 (`pos-terminal`)

## Dependencies

- **`Order` model** — the query subject; supplies the appended attributes and the `customer`/`products` relations. 🟢 (`app/Models/Order.php`)
- **`Customer` model** — the `whereHas('customer')` search target (`phone`/`fullname`). 🟢 (`app/Models/Customer.php`)
- **`Product` model + `order_product` pivot** — eager-loaded lines with sale-time pivot data. 🟢 (`data-dictionary.md:225-237`)
- **`pos-terminal`** — the consumer; calls `orders/scan?status=draft` to list resumable drafts and hydrate the cart. 🟢 (`pos-terminal`)

## Identified Design Decisions

| Decision | Evidence in code | Confidence |
|----------|------------------|------------|
| Single JSON endpoint returning fully-hydrated orders (one round-trip to resume a draft) | `->with('customer','products')->take(10)->get()` → JSON | 🟢 (`:430`) |
| Search scoped to the related customer via a grouped `whereHas` (no OR-leak) | `whereHas('customer', fn => where OR orWhere)` | 🟢 (`:406-408`) |
| Hard cap of 10, no pagination (a picker, not a report) | `take(10)` | 🟢 (`:430`) |
| Dead `204` fallback retained (Collection always truthy) | `if ($result) … else json(null,204)` | 🟢 (`:432-436`) |

## Internal State

None. `scan()` holds only the request-scoped `$list`/`$result` locals and performs no writes. 🟢 (`:419-437`)

## Observability

None. The action emits no log, metric, or trace; a slow `whereHas` EXISTS search or an empty result produces no signal. 🔴 (`OrderController.php:419-437`, absence)

## Risks and Gaps

- 🟡 **Customerless orders invisible to search.** `whereHas('customer')` silently drops walk-in orders from any `q` result; an operator searching for a draft that had no customer attached will never find it via `q`. Confirm this is acceptable (the `status=draft`-only path still lists them).
- 🟡 **Un-indexable search.** Leading-wildcard `LIKE '%q%'` on `phone`/`fullname` inside an EXISTS subquery cannot use an index; cost grows with the customer/order tables.
- 🟡 **LIKE metacharacter leak.** A user-typed `%`/`_` in `q` acts as a wildcard (parameter-bound, so no injection — only broader matches). (`:424`)
- 🔴 **Dead `204` branch.** `response()->json(null, 204)` is unreachable because a `Collection` is always truthy — harmless but misleading (mirrors `customers-scan`); either remove it or make the empty case explicit.
- 🟡 **No per-record authorization.** Any authenticated admin can enumerate the 10 most recent orders (and any customer's, via `q`). (ADR-0009)
- 🔴 **No observability** on the lookup path (see above).
- 🟢 **Search grouping is correct** (not a gap) — the OR is inside the `whereHas`, forming a single EXISTS predicate. ⚠ Reviewer 2026-09-22: dropped the stale "unlike the ungrouped `orWhere` in `orders-crud` `index`" contrast — that `index` search was fixed to a grouped closure on 2026-09-21.
