# POS Scan — Implementation Tasks

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

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

## Prerequisites

- [ ] `Product` model with soft-delete, `code` scope, `units` (hasMany `ProductUnit`), and the `wholesale_prices`/`is_expired` accessors is available (see `products-catalog` unit).
- [ ] `ProductUnit` + `Unit` models exist with `unit_id`, `conversion_qty`, `is_default` and a `unit` relation exposing `name` (see `units` unit).
- [ ] The admin route group / `admin` auth guard is in place (see `auth` unit).
- [ ] `products.code` is unique/indexed and `products.name` exists (schema per `data-dictionary.md`).

## Tasks

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

- [ ] T-01, Register `GET /pos/scan` → `PosController@scan` **before** the `/pos` resource route so it is not shadowed by the resource `show` route.
  - Legacy source: `routes/web.php:80-81`
  - Done when: `GET /pos/scan?q=x` reaches `scan()`, not `PosController@show`.
  - Confidence: 🟢

- [ ] T-02, Implement `scan()`: read `q` from the query string via the request helper.
  - Legacy source: `app/Http/Controllers/PosController.php:763`
  - Done when: the action reads `q` and tolerates a missing value (`null`).
  - Confidence: 🟢

- [ ] T-03, Barcode path: `Product::with('units.unit')->code($q)->first()`; on hit, return `{is_barcode:true, data: product.toArray() + units}`.
  - Legacy source: `app/Http/Controllers/PosController.php:765-771`; `Product::scopeCode` `app/Models/Product.php:79-82`
  - Done when: an exact `code` match returns a single product object with `is_barcode:true` and no name query is run.
  - Confidence: 🟢

- [ ] T-04, Name path (fallback only): `Product::with('units.unit')->where('name','like',"%$q%")->take(10)->get()`; map each to `toArray() + units`; return `{is_barcode:false, data:[...]}` (possibly empty).
  - Legacy source: `app/Http/Controllers/PosController.php:774-783`
  - Done when: with no code match, up to 10 name matches are returned as an array with `is_barcode:false`; zero matches → `data:[]`.
  - Confidence: 🟢

- [ ] T-05, Implement `buildUnitsPayload(Product $product)`: seed a synthetic base unit (`unit_id:null`, `label = product.unit ?: ''`, `conversion_qty:1`, `is_default:false`), append each `ProductUnit` (`unit_id`, `label = unit?.name ?: ''`, `conversion_qty` cast to float, `is_default` cast to bool), and if no ProductUnit is default, force the base unit default.
  - Legacy source: `app/Http/Controllers/PosController.php:786-810`
  - Done when: every product's `units[]` has exactly one `is_default:true` and the base unit is first.
  - Confidence: 🟢

- [ ] T-06, Ensure soft-deleted products are excluded from both paths (default SoftDeletes scope).
  - Legacy source: `app/Models/Product.php:10,35-38`
  - Done when: a soft-deleted product's barcode/name returns no match.
  - Confidence: 🟢

- [ ] T-07, Enforce authentication via the admin route group (no anonymous access).
  - Legacy source: `routes/web.php:24-28,80`
  - Done when: an unauthenticated request is redirected to `auth/login`.
  - Confidence: 🟢

- [ ] T-08 (improvement, optional), Escape `%`/`_` in `q` before the `LIKE` and/or define the empty-`q` contract explicitly.
  - Legacy source: `app/Http/Controllers/PosController.php:763,774`
  - Done when: user-typed wildcards match literally (or the decision to keep current behavior is recorded).
  - Confidence: 🟡

## Test Tasks

- [ ] TT-01, Happy path — barcode: scanning an existing `code` returns `{is_barcode:true, data:{...units...}}` with exactly one default unit. (see `requirements.md` Acceptance Criteria)
- [ ] TT-02, Happy path — name: a fragment matching several products returns `{is_barcode:false, data:[...≤10...]}`.
- [ ] TT-03, No match: unknown `q` returns `{is_barcode:false, data:[]}`.
- [ ] TT-04, Units guarantee: a product with zero `ProductUnit` rows returns a single base unit forced `is_default:true`; a product whose ProductUnits already include a default keeps the base unit non-default.
- [ ] TT-05, Soft-delete: a soft-deleted product is not returned by either path.
- [ ] TT-06, Auth: anonymous request is redirected to `auth/login`, not served JSON.
- [ ] TT-07, Cap: a fragment matching >10 products returns exactly 10.

## Data Migration Tasks (if applicable)

- [ ] None. This endpoint is read-only and introduces no schema.

## Suggested Order

1. T-01 (route ordering) and T-05 (`buildUnitsPayload`) first — the other tasks depend on both.
2. T-03 then T-04 (barcode-first, name-fallback), reusing T-05.
3. T-06/T-07 are cross-cutting (model scope + route group) — verify alongside T-03/T-04.
4. T-08 is an optional hardening step after parity is proven.

## Pending Gaps (🔴)

- Define the intended contract for empty/missing `q` (currently `LIKE '%%'` returns arbitrary non-deleted rows). 🔴
- Decide whether to add observability (the action currently emits no logs/metrics). 🔴
