# 18 — Phase 2 Checklist & Status

Working tracker for [13 — Implementation Plan, Phase 2](13-implementation-plan.md) —
master data, access & catalog. Status date: **2026-08-05**.

---

## Exit criteria (doc 13)

| Criterion | Status | Evidence |
|---|---|---|
| The full real menu of the pilot branch is entered **by a non-developer** | 🔲 **External** — screens ready | Arabic-first RTL back office at `/admin`; catalog + modifier screens; hand the owner/manager a login and the pilot menu |
| A price change is **versioned and audited** | ✅ Done | `item_prices` version rows with `effective_from`; `catalog.price_set` audit with before/after; `AccessCatalogTest::testPriceVersioningAnswersWhatDidALatteCostInMarch` + end-to-end smoke |
| A closure declared centrally **appears in the branch's pulled data** | ✅ Done | `DevicePullTest::testClosureDeclaredCentrallyAppearsInPulledData` + live smoke: declare in back office → `GET /api/sync/pull` returns the `calendar_exception` with the Arabic note intact |

## Delivered

- **Migrations 0002** (central MySQL + branch SQLite): buildings/offices, branches
  (cutoff, cash-rounding increment, currencies, one-Main rule), calendar exceptions,
  categories/items/modifiers, versioned `item_prices` + `modifier_option_prices`,
  branch availability + 86 settings, recipes schema (entry optional until Phase 8),
  FX rates, devices, `sync_changes` feed. `role_assignments.branch_id` FK added
  centrally (decision D6 closed).
- **Domain services** — every mutation transactional + audited + change-logged:
  `BranchService` (code immutable, never deleted, exactly one Main),
  `BuildingService` (office list reconciliation, soft deactivate),
  `CalendarService` (doc 10 §3.1 rules: org/branch scope, retroactive flag, annul
  keeps the record, `isClosed`), `UserService` (manager-LIMITED enforcement:
  non-financial roles, own branch), `CatalogService` + `PriceResolver` (resolution
  order, three availability switches, 86 auto-clear on business-day roll),
  `FxService` (branch override), `DeviceService` (token issue/verify/revoke),
  `PullService` (cursor feed, in-page dedupe, `password_hash` stripped).
- **Back office** `/admin` — server-rendered, Arabic-first, full RTL (doc 12 §5,
  doc 10 §6): login (password mode), dashboard, branches, buildings, calendar,
  users+roles (PIN/password/status/assignments), catalog, item editor (global
  fields, versioned prices, per-branch price/availability/86, modifier attach,
  price history), modifiers, FX, devices (token shown once). CSRF on every POST;
  capability checks server-side on every action.
- **Sync pull** `GET /api/sync/pull` per contract v1.1.0: device bearer auth,
  opaque cursor, 200-change pages, `X-Sync-Contract: 1`; device `last_pull_*`
  observability. Verified live: bad token → 401, incremental cursor → empty page.
- **Bootstrap** `bin/create-owner.php` (refuses when an active Owner exists).
- **Tests**: 153 green across both dialects, including the two automatable exit
  criteria.

## How to run it

```bash
php bin/migrate.php central
php bin/create-owner.php --phone="+964..." --name-ar="..." --name-en="..." --password="..."
# then sign in at https://<host>/admin/login
```

## Deferred, with reasons

| Item | Where it lands |
|---|---|
| Manager closure *requests* (REQUEST mode approval queue) | The approval mechanism ships with the POS supervisor-PIN pattern (Phase 3); until then the calendar screen is read-only for managers |
| Recipe entry UI | Phase 8 (doc 13: "recipes schema, entry optional for now") — schema shipped |
| Per-item modifier default overrides UI (`default_option_ids`) | Phase 3, when the POS consumes them; column shipped |
| Branch-versioned modifier option deltas UI | Table + resolver shipped (`modifier_option_prices`); screen added when a branch first needs it |
| Item image upload | `image_ref` field shipped; file handling when the POS needs images |
| Branch-side pull *apply* (SQLite writer) | Phase 3 branch service (consumes this feed) |

## Notes for Phase 3

The POS reads everything through `PriceResolver` (snapshot prices at sale time),
`CalendarService::isClosed`, and the branch tables populated by applying the pull
feed. The 86 switch already auto-clears on business-day roll — the POS only ever
sets it.
