# 20 — Phase 4 Checklist & Status

Working tracker for [13 — Implementation Plan, Phase 4](13-implementation-plan.md) —
sync pipeline & central reporting. Status date: **2026-08-05**. Executed under the
[D17](14-conventions.md) PWA architecture: the "sync client on Task Scheduler" is the
PWA's in-app push loop; the "nightly SQLite backup" is replaced by the push cadence
itself (loss bounded to the last unsynced minute) plus the doc 10 §9.1 recovery flow.

---

## Exit criteria (doc 13)

| Criterion | Status | Evidence |
|---|---|---|
| Branch data reaches the centre reliably, no duplicates | ✅ Engineering | `SyncPipelineTest` (MySQL): staged batch → `sync:process` → every event type applied idempotently in per-device order; re-delivery and re-processing are no-ops; unknown references quarantined, never lost. PWA side: 218-assertion suite incl. the full-day outbox drain — 5xx backoff, 401 keep-trading, per-seq ordering, delete-only-on-accepted |
| Every order visible centrally | ✅ Engineering | Central `orders`/`order_lines`/`order_payments` mirrors + `/admin/reports` (daily, by item, by cashier) reading **rollups rebuildable from source** (`testRollupsRebuildFromSourceAndMatch`) |
| **Pilot: a real branch trades a full week, one day deliberately offline, cash reconciles** | 🔲 **External** | Needs the deployment to cafe.ubarum.com + a real branch. Protocol below |
| Recovery procedure rehearsed end to end | 🔲 **External** (engineering ready) | Drill below — revoke device, re-register, reinstall PWA, pull, resume |

## Delivered

- **Push endpoint** `POST /api/sync/push` (contract [15 §1.3.0](15-sync-contract.md)):
  device bearer auth, fast-and-dumb staging into `sync_staging` (unique `event_id`),
  accept-and-flag — envelope-invalid events quarantined with reasons
  (`invalid_schema`, `unknown_event_type`, `payload_too_large`, `branch_mismatch`),
  stored and reported, never dropped; >500 events → 413; `devices.last_push_*` recorded.
- **`sync:process` cron** (5 min): applies staging in per-device order into central
  `orders` (+lines/payments), `shifts`, `cash_movements`, `day_closes`, `audit_events`
  — idempotent by natural id, chunked (`SYNC_PROCESS_BATCH`), resumable; transient
  failures retry then quarantine as `apply_error`; every applied event marks its
  (branch, day) dirty for rollups. Requeue from `/admin/sync` (Owner, audited).
- **`rollups:build` cron** (hourly): `rollup_daily_sales`, `rollup_item_sales`,
  `rollup_cashier_actions` rebuilt per dirty pair inside one transaction —
  **rebuildable from source, proven byte-identical on full rebuild**; `rollups:build all`
  forces a full re-mark.
- **`alerts:scan` cron** (15 min): stale-device alerts (doc 10 §4) open after
  `STALE_DEVICE_HOURS` (default 6) of silence and auto-resolve on the next
  push/pull; optional `ALERT_WEBHOOK_URL` notification; shown on `/admin/sync`.
- **Reporting v1** `/admin/reports` (Arabic-first, from rollups only): daily sales
  summary (tenders, rounding, variance, late arrivals, frozen-day marker), sales by
  item, and the doc 06 §6 cashier panel (voids/discounts/refunds/no-sale opens).
  Branch scoping enforced via `permittedBranchScope`; the all-branch view requires
  `report.consolidated`.
- **Sync health** `/admin/sync`: device last push/pull, staging backlog, open alerts,
  Sync Exceptions list with quarantine reasons + requeue.
- **PWA push client**: drains the outbox in `seq` order every ≤60 s online (batches
  ≤200), exponential backoff 60 s → 15 min, deletes rows **only** on `accepted`,
  moves quarantined rows to the local `conflicts` store flagged for support, 401 →
  "device deauthorized" banner + keep trading + outbox preserved; live sync status
  screen (last push/pull, backlog, backoff, conflicts) and manual push/pull.
- **Tests**: 160 PHPUnit (5 new pipeline tests, 45 assertions, MySQL per the
  centre-only precedent) + 218 PWA assertions.

## Pilot protocol (the roadmap Phase-1 done-when)

1. Deploy + provision the first branch (doc 19 §How-to-run, cafe.ubarum.com steps).
2. Trade live for a full week; mid-week, **disable the device's connectivity for one
   whole trading day** (airplane mode / cut the Wi-Fi).
3. Each evening: blind close reconciles; day close freezes the Z.
4. Verify centrally: every order visible on `/admin/reports` next morning; the offline
   day arrives complete after reconnection with **zero duplicates** (`sync_staging`
   accepted-idempotent); `/admin/sync` shows the stale-device alert opening during the
   offline day and auto-resolving after.
5. Recovery drill: revoke the device in `/admin/devices` → register a new one →
   reinstall the PWA (or clear site data) → token → pull → resume trading. Target
   under one hour (doc 10 §9.1); local unsynced loss is bounded by the 60 s push cadence.

## Deferred, with reasons

| Item | Where it lands |
|---|---|
| `payment.received` application (`sync:process` applier) | Phase 5 with accounts — the type already stages |
| 86 flowing up centrally | Phase 5 settings pass (D21 — local enforcement complete; central visibility is cosmetic until multi-branch) |
| Owner daily digest (WhatsApp/Telegram) | Phase 7 (doc 13) — `alerts:scan`'s webhook is the transport seed |
| Rollup-fed dashboards beyond tables (charts, comparisons) | Phase 7 branch-comparison report |
| Manager closure-request approval screen | Rides the Phase 5 back-office pass (REQUEST plumbing exists) |

## Notes for Phase 5

Accounts/receivables build on this pipeline unchanged: new event types register in
doc 15, stage through the same endpoint, apply in `ProcessService` with their own
appliers, and report through new rollups. The POS gains account search against pulled
`account`/`authorized_person` entities (new pull entities, minor bump) and
`payment.received` at the counter.
