# Orders Scan Design

## Data Model

### Tables and relationships

`orders-scan` reads three tables via Eloquent; it writes nothing.

```
orders (1) ─────────────── (0..1) customers
   │
   └─── order_product (pivot) ─── products
```

| Table | Columns consumed | Notes |
|---|---|---|
| `orders` | all columns; appended `code`, `is_editable`, `debt_locked` | query root |
| `customers` | `phone`, `fullname` (search); full row eager-loaded | `Order::customer()` belongsTo |
| `order_product` | `qty`, `price`, `unit_id`, `conversion_qty` | pivot withPivot on `Order::products()` belongsToMany |
| `products` | full row | eager-loaded through pivot |

### `Order` model appended attributes

| Attribute | Formula | Type |
|---|---|---|
| `code` | `#QT78-{id}` | string |
| `is_editable` | draft, or done within 24 h of `updated_at` | bool |
| `debt_locked` | a `pos_debt` ledger row exists | bool |

Walk-in orders have `customer = null`; these rows exist in `orders` but never satisfy a `q` filter because `whereHas('customer')` requires a related customer row.

---

## Internal Flows

### Main query flow

```mermaid
flowchart TD
    A[GET /orders/scan] --> B{admin middleware}
    B -- unauthenticated --> C[302 → auth/login]
    B -- authenticated --> D[Order::query]
    D --> E{request q truthy?}
    E -- yes --> F["whereHas('customer',\nphone LIKE %q%\nOR fullname LIKE %q%)"]
    E -- no --> G{request status present?}
    F --> G
    G -- yes --> H["where('status', status)"]
    G -- no --> I["orderBy id desc\nwith customer, products\ntake 10\nget()"]
    H --> I
    I --> J{Collection always truthy}
    J -- true branch always taken --> K[response()->json(result) 200]
    J -. dead code .-> L[json(null, 204) — never reached]
```

### Alternative flows

| Condition | Behaviour |
|---|---|
| `q` absent or falsy | `whereHas` block skipped; customerless orders are eligible |
| `status` absent | status `where` skipped; drafts and done orders both appear |
| No rows match | `get()` returns empty `Collection` → serialised as `[]` with `200` |
| `q` present but order has no customer | `whereHas` requires a related customer row — walk-in orders silently excluded |
| Unauthenticated request | Admin group middleware intercepts before `scan()` runs → `302` |

### Response serialisation

Each element in the returned array is a fully serialised `Order`:

```
Order {
  // all orders columns
  code,            // appended
  is_editable,     // appended
  debt_locked,     // appended
  customer: Customer | null,
  products: [
    Product {
      pivot: { qty, price, unit_id, conversion_qty }
    }
  ]
}
```

---

## Technical Decisions

| Decision | Rationale | Source |
|---|---|---|
| Single fully-hydrated response (customer + products in one payload) | Lets `pos-terminal` repopulate a cart from a saved draft without a second round-trip | `OrderController.php:430` |
| Search via `whereHas('customer', fn => where OR orWhere)` | The OR is confined inside the EXISTS subquery — no OR-leak against the `status` filter | `OrderController.php:422-426` |
| Hard cap of `take(10)`, no pagination | The endpoint is a picker, not a report; limiting rows bounds query cost | `OrderController.php:430` |
| Route declared before `resource('/orders')` | Prevents the resource `show` wildcard from shadowing `/orders/scan` | `routes/web.php:72-75` |
| Dead `204` fallback retained | A `Collection` is always truthy, so the `else` branch is unreachable; it mirrors `customers-scan` but produces no wrong behaviour | `OrderController.php:432-436` |

---

## Notes

- **Customerless orders invisible to `q` search.** `whereHas('customer')` silently excludes walk-in orders from any `q` result. The `?status=draft`-only path (no `q`) still lists them. Confirm this is the intended POS behaviour.
- **Un-indexable search.** Leading-wildcard `LIKE '%q%'` on `phone`/`fullname` inside an EXISTS subquery cannot use a B-tree index; cost grows linearly with the customer table.
- **LIKE metacharacter pass-through.** `%` and `_` typed by a user in `q` act as LIKE wildcards (parameter-bound, so no SQL injection — only broader-than-expected matches).
- **No observability.** `scan()` emits no log, metric, or trace. A slow EXISTS search or an empty picker produces no signal.
- **No per-record authorisation.** Any authenticated admin may enumerate the 10 most recent orders and search any customer's orders via `q`. (ADR-0009)
