# POS Terminal — Implementation Tasks

> Produced by the Reversa **Writer** (phase: generation) · doc_level: `complete`
> Generated on 2026-09-19
> Reimplements `GET /pos` (`PosController::index`) and its injected client cart engine.

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

## Prerequisites

- [ ] Dependencies listed in `design.md` are available: `pos-scan` (`GET /pos/scan`), `products-pricing` (`GET /products/get-price`), `customers-scan`/`customers-crud`, `orders-crud`/`orders-scan`.
- [ ] `Product`, `ProductUnit`, `Unit`, `Order` models and the `products.unit` string + `product_units` conversion table exist and are populated.
- [ ] The `['web','admin']` auth middleware group (or its equivalent) protects the route; the root `/` → `/pos` redirect is configured.
- [ ] Front-end libraries the engine assumes are loaded: jQuery, select2, jsrender (`$.templates`), numeral, swal, a `.bindings` model plugin, `inputNumber`, hotkeys — or their chosen replacements.

## Tasks

- [ ] T-01, Register `GET /pos` (index only) behind the admin auth middleware and render the terminal shell view.
  - Origem no legado: `routes/web.php:81`, `app/Http/Controllers/PosController.php:18-23,678`
  - Critério de pronto: authenticated `GET /pos` returns the terminal page with header "Sales" ("Bán hàng"); anonymous is redirected to login.
  - Confiança: 🟢

- [ ] T-02, Add the root landing redirect `/` → `/pos` (301).
  - Origem no legado: `routes/web.php:22`
  - Critério de pronto: `GET /` responds `301` with `Location: /pos`.
  - Confiança: 🟢

- [ ] T-03, Seed the client with the collaborator URLs (product scan, customer scan, draft scan, order submit, customer-detail link with an `#id#` placeholder).
  - Origem no legado: `PosController.php:25-29`
  - Critério de pronto: the rendered page exposes all five URLs to the cart engine.
  - Confiança: 🟢

- [ ] T-04, Implement `buildUnitsPayload(product)`: base unit first (`unit_id:null`, label = product's `unit` string, `conversion_qty:1`), then each `ProductUnit` (unit_id, related unit name, conversion_qty, is_default); if none default, force the base default.
  - Origem no legado: `PosController.php:786-810`
  - Critério de pronto: payload always starts with the base unit and has exactly one `is_default=true`.
  - Confiança: 🟢

- [x] T-05, Implement optional draft preload: when `?id=` is present, load the order with `customer` + `products.units.unit` and replace each product's `units` with `buildUnitsPayload`, handing it to `fillOrderDraft` at boot. A missing/invalid id falls back to no draft (does not error).
  - Origem no legado: `PosController.php:31-41,610`
  - Critério de pronto: `GET /pos?id=N` renders the terminal pre-filled with order N; `GET /pos?id=<invalid>` renders an empty terminal instead of erroring.
  - Confiança: 🟢 — ✅ Fixed 2026-09-19 (was unguarded against a missing/invalid id).

- [ ] T-06, Implement scan/add: Enter-key on the scanner input queries `GET /pos/scan?q=`; `is_barcode:true` adds directly, otherwise render a per-product×unit pre-priced autocomplete whose items add on click.
  - Origem no legado: `PosController.php:237-306,194-228`
  - Critério de pronto: scanning a code adds the product; a name search shows the autocomplete; a no-match shows the empty message.
  - Confiança: 🟢

- [ ] T-07, Implement `addItem`/`updateItem` with line-key dedup (`code + '__' + (unit_id||'base')`): first add creates a line (qty 1 or pivot qty), a repeat of the same product+unit increments qty; each change re-prices synchronously and sets `total = qty * round(price,1)`.
  - Origem no legado: `PosController.php:89-152`
  - Critério de pronto: same product+unit collapses to one line with summed qty; totals recompute on every change.
  - Confiança: 🟢

- [ ] T-08, Implement quantity edit via `.input-number` → `updateItem` (re-price + re-render the row).
  - Origem no legado: `PosController.php:347-352`
  - Critério de pronto: editing a line's number re-prices and re-renders it.
  - Confiança: 🟢

- [ ] T-09, Implement unit change: relabel + re-price preserving qty; if the target unit line exists, merge by direct qty addition and remove the source line.
  - Origem no legado: `PosController.php:354-407`
  - Critério de pronto: switching a line into an existing unit-line sums quantities into one; switching to a new unit keeps qty and re-prices.
  - Confiança: 🟢

- [ ] T-10, Implement customer selection (select2 against `customers/scan`, remapping `id = phone`, `oid = original id`), `fillCustomerInfo`, and full-cart re-price for the customer's type on change.
  - Origem no legado: `PosController.php:445-511,413-431`
  - Critério de pronto: selecting a customer re-prices every line for that customer's type and shows the debt hint when `debt_total > 0`.
  - Confiança: 🟢

- [ ] T-11, Implement inline quick-add customer modal (POST to `customers`, select the created customer on success, surface field errors).
  - Origem no legado: `PosController.php:439-443,619-650`
  - Critério de pronto: a new customer created from the modal is selected and filled into the terminal.
  - Confiança: 🟢

- [ ] T-12, Implement running totals `updatePOS`: `subtotal = Σ round(line.total,1)`, `total = subtotal − discount`, mirror `count/discount/debt`; re-run on the bindings `model-change`.
  - Origem no legado: `PosController.php:168-189,336-345`
  - Critério de pronto: totals update on every cart/line/discount/debt change.
  - Confiança: 🟢

- [ ] T-13, Implement debt-input state: cleared+disabled when no customer; disabled for a `debt_locked` loaded order; enabled otherwise.
  - Origem no legado: `PosController.php:415-422,549-554`
  - Critério de pronto: no-customer and debt_locked states both leave the debt input disabled.
  - Confiança: 🟢

- [ ] T-14, Implement submit-time debt validation: when debt > 0 and the input is enabled, require a selected customer and require debt ≤ (subtotal − discount); block with the matching message otherwise.
  - Origem no legado: `PosController.php:308-334`
  - Critério de pronto: the two block conditions fire the correct messages; a valid or zero/disabled debt submits.
  - Confiança: 🟢

- [ ] T-15, Implement draft rehydration + update repoint: `fillOrderDraft` fills customer/cart/notes/totals/status, styles a `done` order, and for a loaded order pushes `?id=`, sets `_method=PUT` and the form action to `/orders/{id}`.
  - Origem no legado: `PosController.php:519-564`
  - Critério de pronto: loading a draft fills the terminal and submitting issues a `PUT` to that order.
  - Confiança: 🟢

- [ ] T-16, Implement the draft picker (`#orderDraftKey` select2 against `orders/scan?status=draft`) triggering `fillOrderDraft` on selection.
  - Origem no legado: `PosController.php:566-617`
  - Critério de pronto: picking a draft rehydrates the terminal from that order.
  - Confiança: 🟢

- [ ] T-17, Implement form submission: write the button's `data-status` (`draft`/`done`) into the hidden `status` input and submit `#pos_form` to the order endpoint.
  - Origem no legado: `PosController.php:308-334,332-333`
  - Critério de pronto: the save-draft button posts status `draft`; the finalize button posts status `done`.
  - Confiança: 🟢

- [ ] T-18, Implement ergonomics: `onbeforeunload` unsaved-work guard and `F2` hotkey to refocus the scanner.
  - Origem no legado: `PosController.php:652-665`
  - Critério de pronto: leaving with a non-empty cart prompts a confirmation; F2 focuses the scanner input.
  - Confiança: 🟢

- [ ] T-19, Do NOT implement the unused resource verbs (`create/store/show/edit/update/destroy`).
  - Origem no legado: `PosController.php:694-753`
  - Critério de pronto: only `GET /pos` is exposed by this unit.
  - Confiança: 🟢

## Test Tasks

- [ ] TT-01, Happy path: authenticated `GET /pos` renders the terminal; `GET /` 301-redirects to `/pos` (see `requirements.md` Acceptance Criteria).
- [ ] TT-02, Anonymous `GET /pos` redirects to `auth/login`.
- [ ] TT-03, Scan the same product+unit twice → one line, qty 2, total = 2 × rounded price.
- [ ] TT-04, Same product in two units → two distinct lines.
- [ ] TT-05, Unit change into an existing unit-line merges quantities into one line.
- [ ] TT-06, Selecting a customer re-prices every existing cart line for that customer's type.
- [ ] TT-07, Submit with debt > 0 and no customer → blocked with "Phải chọn khách hàng khi có tiền nợ.".
- [ ] TT-08, Submit with debt > (subtotal − discount) → blocked with "Số tiền nợ không được lớn hơn tổng tiền hàng.".
- [ ] TT-09, Load a draft via `?id=` and via the picker → terminal pre-filled; submit issues `PUT /orders/{id}`.
- [ ] TT-10, `debt_locked` / `done` order → debt input disabled (and customer search locked for `done`).
- [ ] TT-11, `buildUnitsPayload` for a product with no `ProductUnit` → base unit only, `is_default=true`.

## Data Migration Tasks (if applicable)

- [ ] TM-01, None — this unit persists nothing; order/customer data is owned by the `orders-*` / `customers-*` units.

## Suggested Order

1. Server shell first: T-01, T-02, T-03, T-04, T-05 (route, redirect, URL seeding, units payload, draft preload).
2. Core selling loop: T-06 → T-07 → T-08 → T-12 (scan/add, dedup, qty edit, totals).
3. Multi-unit + customer pricing: T-09, T-10, T-11.
4. Draft + submission: T-13 → T-14 → T-15 → T-16 → T-17.
5. Ergonomics and cleanup: T-18, T-19.
   - Blockers: T-07/T-08/T-09/T-10 all depend on `products-pricing` (T-07 re-pricing); draft tasks depend on `orders-scan`/`orders-crud`; customer tasks depend on `customers-scan`/`customers-crud`.

## Pending Gaps (🔴)

- 🟢 Single source of truth confirmed 2026-09-25 (`questions.md#question-17`): the server (`OrderController::store`/`update`) is authoritative; the client mirror here is UX-only. Given the trade-offs of removing the client check (loses instant inline feedback) or building a shared-validation endpoint (bigger architecture change), the decision was to keep the duplication but mark it explicitly: cross-referencing comments were added in both `PosController.php` (`:314-317`) and `OrderController.php` (`:125,303`) so a future edit to one side is more likely to prompt updating the other.
- ✅ Missing/invalid `?id=` on draft preload — fixed 2026-09-19 (falls back to no draft instead of erroring).
- 🟡 Confirm the required front-end library set (jQuery/select2/jsrender/numeral/swal/`.bindings`/inputNumber/hotkeys) is acceptable for the target stack or choose replacements.
