# Orders-Scan — Requirements

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

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

## Overview

`orders-scan` is the JSON order-lookup endpoint (`GET /orders/scan`, `OrderController::scan`) the POS terminal calls to list and search recent orders — chiefly to **resume a saved draft** back into the cart. It returns up to 10 orders, newest first, each fully hydrated with its customer and its product lines (with pivot data), so the client can repopulate a cart without a second round-trip. It is read-only. 🟢 (`routes/web.php:72`, `OrderController.php:419-437`)

The route is declared **before** `resource('/orders')`, so `/orders/scan` is not captured by the resource's `show` wildcard. It has no route name. 🟢 (`routes/web.php:72-75`)

## Responsibilities

- Build a base query over all orders (`Order::query()`). 🟢 (`:421`)
- When `q` is truthy, restrict to orders whose **related customer** matches: `whereHas('customer', phone LIKE %q% OR fullname LIKE %q%)` — the OR is scoped inside the `whereHas` EXISTS subquery. 🟢 (`:422-426`)
- When `status` is present, filter by exact `status` (`draft`|`done`). 🟢 (`:427-429`)
- Order by `id` desc, eager-load `customer` and `products`, cap at 10, and return the collection as JSON. 🟢 (`:430-436`)

## Business Rules

- **Hard cap of 10 results, no pagination.** `->take(10)->get()` — the client never receives more than 10 orders and there is no page cursor. 🟢 (`:430`)
- **Newest first.** `orderBy('id','desc')` — the most recently created orders lead. 🟢 (`:430`)
- **Search is by the related customer only.** `whereHas('customer', …)` means an order matches `q` **only if it has a customer** whose phone or fullname matches; a walk-in (customerless) order can never satisfy a `q` search. Without `q`, customerless orders are included. 🟢 (`:422-426`)
- **The `q` OR is correctly grouped.** The `phone`/`fullname` OR lives inside the `whereHas` closure, so it forms a single EXISTS predicate — no OR-leak against the `status` filter. ⚠ Reviewer 2026-09-22: removed a stale contrast to "the ungrouped `orWhere` in `orders-crud` `index`" — that `index` search was fixed to a grouped closure on 2026-09-21, so it is no longer a valid negative example. 🟢 (`:425-427`)
- **Full order payload.** Each result serialises the whole `Order` (including appended `code` = `#QT78-{id}`, `is_editable`, `debt_locked`) plus the nested `customer` and `products` with their `order_product` pivot (`qty`, `price`, `unit_id`, `conversion_qty`) — everything the POS client needs to rehydrate a draft. 🟢 (`:430`; `Order.php:14,39-41`)
- **Always a `200` JSON array.** `$result` is an Eloquent `Collection` (truthy even when empty), so the `if ($result)` branch always wins and the `response()->json(null, 204)` fallback is **dead code** (harmless, mirrors `customers-scan`). An empty result serialises as `[]`. 🟢 (`:432-436`)
- **No input validation.** `q` and `status` are read raw via `request()`; absent/empty values simply skip their filters. 🟢 (`:422,427`)

## Functional Requirements

| ID | Requirement | Priority | Acceptance criterion |
|----|-------------|----------|----------------------|
| RF-01 | Return up to 10 orders, newest first, as a JSON array | Must | `GET /orders/scan` returns `200` with ≤ 10 order objects ordered by `id` desc. 🟢 |
| RF-02 | Eager-load `customer` and `products` (with pivot) on each order | Must | Each object embeds its `customer` and a `products` array carrying `pivot.qty/price/unit_id/conversion_qty`. 🟢 |
| RF-03 | Optional `q` search over the related customer's phone or fullname | Should | `?q=090` returns only orders whose customer's phone/name contains `090`; customerless orders are excluded from the `q` result. 🟢 |
| RF-04 | Optional `status` filter (`draft`\|`done`) | Should | `?status=draft` returns only draft orders (the POS "resume draft" case). 🟢 |
| RF-05 | Serialise appended `code`/`is_editable`/`debt_locked` per order | Should | Each object exposes `code` (`#QT78-{id}`), `is_editable`, and `debt_locked` so the client can decide editability. 🟢 |
| RF-06 | Require an authenticated admin session | Must | Anonymous request → `302` to `auth/login`. 🟢 (`routes/web.php:24-28`) |

## Non-Functional Requirements

| Type | Inferred requirement | Evidence in code | Confidence |
|------|----------------------|------------------|------------|
| Security | Admin authentication required (admin route group middleware `['web','admin']`) | `routes/web.php:24-28,72` | 🟢 |
| Performance | Result hard-capped at 10 rows (`take(10)`); `customer`/`products` eager-loaded to avoid N+1 across the 10 orders | `OrderController.php:430` | 🟢 |
| Performance | `q` search uses leading-wildcard `LIKE '%q%'` on customer `phone`/`fullname` (no index) plus an EXISTS subquery per row | `OrderController.php:424` | 🟡 |
| Observability | None — no log/metric/trace on this lookup path | `OrderController.php:419-437` (absence) | 🔴 |

> Inferred from code. Validate with the operations team.

## Acceptance Criteria

```gherkin
Given an authenticated administrator and at least 12 existing orders
When he calls GET /orders/scan
Then he receives HTTP 200 with a JSON array of the 10 most recent orders (id desc), each with customer and products (including pivot qty/price/unit_id/conversion_qty)

Given draft and finalised orders
When GET /orders/scan?status=draft is called
Then the array returns only orders with status "draft" (the POS "resume draft" case)

Given an order whose customer has phone "0901" and a walk-in order with no customer
When GET /orders/scan?q=0901 is called
Then the "0901" customer's order appears and the walk-in order does not (whereHas requires a related customer)

Given no order matches the filter
When GET /orders/scan is called
Then it receives HTTP 200 with an empty JSON array [] (the 204 branch is dead code)

Given a request with no authenticated admin session
When GET /orders/scan is called
Then it receives HTTP 302 redirecting to auth/login
```

## Priority (MoSCoW)

| Requirement | MoSCoW | Justification |
|-------------|--------|---------------|
| Return ≤ 10 orders newest-first (RF-01) | Must | The endpoint's reason to exist — the POS order/draft picker |
| Eager-load customer + products (RF-02) | Must | The client rehydrates a full cart from this single payload |
| `status` filter (RF-04) | Should | Drives the primary caller (`?status=draft` to resume drafts) |
| `q` customer search (RF-03) | Should | Convenience narrowing by customer |
| Appended flags (RF-05) | Should | Lets the client gate editability client-side |
| Admin authentication (RF-06) | Must | Enforced by the route group |

> Priority inferred from the endpoint's role as the POS draft/order picker and its consumption by `pos-terminal`.

## Code Traceability

| File | Function / Class | Coverage |
|------|------------------|----------|
| `app/Http/Controllers/OrderController.php:419-437` | `OrderController::scan` | 🟢 |
| `routes/web.php:72` | `GET /orders/scan` (declared before the resource) | 🟢 |
| `app/Models/Order.php:14,29-41,49-67` | `Order` (`$appends`, `customer()`, `products()` withPivot, `code`/`is_editable`/`debt_locked`) | 🟢 |
| `app/Models/Customer.php:58-61` | `Customer` (searched via `phone`/`fullname` in `whereHas`) | 🟢 |
| `_reversa_sdd/data-dictionary.md:200-237` | `orders` + `order_product` schema | 🟢 |
