# 19 — Phase 3 Checklist & Status

Working tracker for [13 — Implementation Plan, Phase 3](13-implementation-plan.md) —
POS core, offline trading. Status date: **2026-08-05**.

**Architecture note:** executed under decision [D17](14-conventions.md) (owner,
2026-08-05): *we have nothing to do with hardware — the client installs our PWA wherever
and whenever they want.* The doc 13 branch-service bullet (NSSM/WinSW, branch SQLite,
installer, ESC/POS) is superseded; the POS is an installable **PWA** at `public/pos/`
served from the centre — service worker + IndexedDB, sale-path domain logic in JS held
in parity with the PHP kernel by generated vectors (D20), receipts through the OS print
dialog (D23). Zero network calls on the sale path, unchanged.

---

## Exit criteria (doc 13, restated per D17)

| Criterion | Status | Evidence |
|---|---|---|
| A simulated full trading day — mid-day power cut, no internet at all — ends with cash reconciling and a correct Z-report | ✅ Automated | `tests/Pwa/run.mjs` (196 assertions): offline PIN login → shift with counted float → cash / discount-with-supervisor / USD-with-FX / mixed-tender sales → prep queue + 86 → void + refund → drawer movements → **power cut mid-commit (atomic: nothing lands) → restart from committed state (sequence continues, outbox intact)** → blind close reconciling **to the dinar** → variance-beyond-tolerance path (pending_review + owner escalation) → frozen Z matching hand-computed figures → post-close sale posts to the next day flagged `late_arrival`. Run: `composer test-pwa` (also in CI) |
| Same drill on a client-installed device | 🔲 Manual pass pending | Install the PWA on any device (browser → Install app), run a real day with networking off; the app is offline-complete by construction (service-worker shell + IndexedDB) |

## Delivered

- **PWA** `public/pos/` — installable (manifest + SW app-shell precache, API never
  cached), Arabic-first full RTL, landscape POS layout, single-writer tab lock,
  `navigator.storage.persist()` requested.
- **JS kernel mirrors** (`js/kernel/`): money/rounding/FX (integer-only, half away from
  zero), business-day cutoff, capability matrix + PIN reachability, WebCrypto PBKDF2 PIN
  verify, UUID — all pinned to the PHP kernel by `tools/generate-pwa-vectors.php` +
  `tests/Pwa/run.mjs` (D20; CI step added).
- **Data layer** (`js/data/`): IndexedDB stores (orders, shifts, movements, day_closes,
  outbox, audits, master, counters, kv) with **one atomic commit per domain mutation**;
  pull client applying each page atomically with its cursor at quiet moments only; 401 →
  keep trading, never discard local data.
- **Trading domain** (`js/domain/`): doc 04 lifecycle (CONFIRMED only when settled; no
  open tickets), full snapshots via the PriceResolver mirror, `<CODE>-<YYYYMMDD>-<seq>`
  daily numbering, cash rounding **on the cash leg only** with signed recorded
  difference (D1 nearest), USD tender with FX snapshot + IQD change, mixed tender,
  discounts with cashier LIMITED cap + supervisor PIN, voids (REQUEST + reason codes +
  `prep_state_at_void`), refunds as opposing documents (derived refunded-total,
  cross-tender cash rule), prep queue with `confirmed_at → ready_at` KPI and one-tap 86
  (local-first, D21), shifts (counted float, movements incl. audited no-sale opens,
  **blind close**, per-branch tolerance + hard threshold from D22 columns), business-day
  close with **frozen Z** and late-arrival roll-forward.
- **Outbox**: every domain mutation commits exactly one event (plus `audit.event`
  mirrors) in the same transaction — `order.confirmed/voided/refunded`,
  `shift.opened/closed`, `cash_movement.recorded`, `business_day.closed` — per
  [15 — Sync Contract **1.2.0**](15-sync-contract.md); Phase 4 drains it.
- **Receipts** (D23): paid receipt / refund receipt / prep ticket (deliver-to-office
  marked) / Z-report, 80 mm print stylesheet, reprints marked **DUPLICATE** and audited.
  No tax line — Q3 answered **no VAT** (doc 00 §6).
- **Central (PHP)**: migration 0003 both dialects (`users.pin_pbkdf2`, branch variance
  columns), `Auth::setPin` dual-hash (additive per doc 16 freeze), pull feed carries
  `pin_pbkdf2`; 154 PHPUnit tests green on SQLite + MySQL.

## How to run it

```bash
composer test           # PHP suite, both dialects
composer test-pwa       # parity vectors + the offline trading-day drill
php -S localhost:8000 -t public   # then open http://localhost:8000/pos/
# Provision: /admin/devices → issue token → POS setup screen → pull → pick branch.
```

## Decisions taken (recorded in [14 §13](14-conventions.md))

D17 PWA pivot · D18 PBKDF2 PIN sync · D19 vanilla-JS frontend · D20 parity vectors ·
D21 local-first 86 · D22 variance-tolerance columns · D23 OS-print receipts.

## Deferred, with reasons

| Item | Where it lands |
|---|---|
| Sync **push** client, live sync-status data, stale-device alerts | Phase 4 (the outbox is ready; doc 15 §push is frozen) |
| Central twins of POS tables (orders/shifts/…) | Phase 4 `sync:process` — the ingest migration creates them from the doc 15 §1.2.0 payload schemas |
| 86 visibility at the centre | Phase 4 push (D21 — local enforcement is complete at the only selling terminal) |
| Cashier discount limit as a back-office setting | Constant default (2,000 IQD) in the PWA until a branch asks; column lands with the Phase 4/5 settings pass |
| Manager closure-request queue (REQUEST approval, from Phase 2) | The supervisor-PIN pattern now exists in the PWA; the back-office approval screen lands with Phase 4's central work |
| Charge/split slips, account search, `payment.receive` | Phase 5 (schema carries `buyer_type`/`charges[]` already) |
| Variance-tolerance back-office edit UI | When a branch first needs a non-default value (columns + pull live now) |

## Not in Phase 3 (scope traps — stale "(Phase 3)" labels in docs 02/04)

Standing/scheduled orders and combos are **Phase 10** (doc 13 / doc 11 roadmap-Phase-6);
recipe stock deduction is Phase 8 (`recipe_version_id` reserved on lines).

## Notes for Phase 4

The push client reads the IndexedDB `outbox` in `seq` order and POSTs per doc 15 §push
(≤500/batch, delete only on `accepted`); payload schemas are already the local rows.
`sync:process` needs the central POS tables and applies payloads verbatim. The PWA's
sync chip (`status` screen) shows outbox depth and last-pull already — wire push status
into it. Device registration/revocation and the back-office device screen exist since
Phase 2.
