# Dine Hardening — Phases 1 → 10 Summary

> **Branch:** `claude/intelligent-bhabha-4c11b0`
> **Head:** `741cd12`
> **Scope:** operational hardening of the Dine module across 10 numbered phases — no new features, no UI redesign, no AI / loyalty / messaging. Every change is additive, read-only-or-symmetric, tenant + branch scoped, and reversible.

---

## Executive Summary

Before this branch, the Dine module was a collection of POS-style screens — menus, KDS, floor map, cashier flow — wired to a single fat controller and validated mostly via the happy path. Refunds, voids, ramadan menus, and KDS routing all had subtle correctness gaps that only show up in busy service. The dashboard was a single Reports tab. There was no operational situational awareness for managers and no actionable warnings.

Across ten phases this branch:

1. **Hardened correctness** — every state transition is now idempotent, every reversal symmetric, every audit trail honest.
2. **Locked down access** — granular dine permissions, manager-PIN step-up gates on the dangerous endpoints, route-level entitlement.
3. **Split the fat controller** into per-concern controllers without changing a single URL.
4. **Built an operational dashboard** at `/app/dine/ops` polling `/dine/dashboard` every 30s with six honest sections (null where the system genuinely doesn't track yet — never a fake zero).
5. **Layered a small operational alerts engine** that turns the dashboard signals into actionable warnings with explainable thresholds.

The cashier flow runs on the same code path it always did — every change is structural, additive, or read-only. The dashboard and alerts ride on the same 30-second cache, so the POS keeps the same latency profile under load.

---

## Completed Phases

| # | Title | What it did |
|---|---|---|
| 1 | `served_at` / `cancelled_at` + inventory reversal | Stamped lifecycle timestamps on serve / cancel; ensured every voided line and every non-paid cancellation symmetrically reverses recipe stock via `recipe_void` movements; added `dine.order.line_voided` audit row. |
| 2 | KDS routing + idempotency | Per-`menu_categories` station routing (catch-all when null/empty); ticket emit is idempotent on `(order_id, station_id)` — no duplicate / empty tickets on retries. |
| 3 | Cancelled-state lock + refund idempotency | Made `cancelled` symmetric to `paid`: terminal status, no out-bound transitions; refund poster is idempotent on `(company_id, source='dine_order_refund', source_ref=order_id)`. |
| 4 | `module:dine` route entitlement | Wrapped every `/dine/*` route under the same `module:dine` middleware so tenants without the Dine plan get a clean 403 instead of partial behaviour. |
| 5 | Active menus runtime | Branch-aware menu resolver respecting daypart windows + Ramadan profile + per-item availability windows; `addLine()` rejects items outside their window with a typed error. |
| 6 | Operational permissions + step-up | Added the granular `dine.orders.{void,refund,discount,price_override}` and `dine.production.approve` permissions; void / refund / production-complete endpoints require `X-Step-Up-Ticket`. |
| 7 | DineController decomposition | Split the ~3 300-line `DineController` into seven domain controllers (Menus / Tables / Orders / Kitchen / Payments / Inventory / Reports) without changing a single URL. Phase-7 parity test pins this. |
| 8 | Backend operational dashboard | New read-only `GET /dine/dashboard` returning six aggregated sections in one round-trip with a 30s per-(tenant, branch) cache. Honest null reporting for un-tracked infra signals. |
| 9 | Frontend operational dashboard | New `/app/dine/ops` page (tab now first under Dine) polling the dashboard every 30s, paused in background. Tablet-friendly, no charting libs. |
| 10 | Operational alerts layer | New `DineOperationalAlertsService` computing alerts from the dashboard payload and surfacing them inside the same response (`alerts: []`). Frontend strip on `/app/dine/ops` with severity-driven cards and a "No active alerts" green-card empty state. |

---

## Commit Map

The user-supplied list checked against `git log main..HEAD` — every hash verified.

| Phase | Commit | Subject |
|---|---|---|
| 1 | `53fb5c9` | fix(dine): phase-1 hardening — served_at + void/cancel inventory reversal |
| 2 | `4ce6dbb` | fix(dine): phase-2 hardening — KDS per-category routing + idempotent emit |
| 3 | `f63bebd` | fix(dine): phase-3 hardening — cancelled-state lock symmetric to paid |
| 4 | `6ebc0d1` | fix(dine): phase-4 hardening — wrap dine routes in module:dine entitlement gate |
| 5 | `b5cd3e2` | feat(dine): phase-5 hardening — active menus runtime + item availability windows |
| 6 | `332aa9a` | fix(dine): phase-6 hardening — operational step-up gates + audit |
| 6.x | `ad5a508` | fix(dine-pos): restore polished server POS lost during phase-6 file sync |
| 7 | `d873331` | Phase 7: split DineController into operational controllers |
| 7.x | `90a0f75` | chore(dine): stabilization pass — fix flaky reservations test + add docs |
| 8 | `6d33403` | Phase 8: read-only operational dashboard for Dine |
| 9 | `5266204` | Phase 9: add dine operational dashboard frontend |
| 9.x | `ecb67ba` | Phase 9 follow-up: fix by-hour bar chart height collapse |
| 10 | `741cd12` | Phase 10: add operational alerts to dine dashboard |

---

## Backend Changes

### Migrations added (additive, all with safe down methods)

- `2026_05_10_010000_dine_harden_served_at_and_void_audit` — adds `served_at` / `cancelled_at` to `dine_orders`; reverse-safe.
- `2026_05_10_020000_dine_menu_runtime_scheduling` — adds `is_available`, `available_from`, `available_to`, `available_days` to `menu_items`; `daypart_*` columns to `menus`; reverse-safe.

No destructive migrations. No journal rewrite. No inventory rewrite.

### Models changed

- `App\Models\DineOrder` — `served_at` and `cancelled_at` casts added.
- `App\Models\MenuItem` — availability window casts (`available_from`, `available_to`, `available_days`).
- `App\Models\Menu` — daypart window casts.

### Services added

- `App\Services\Dine\Dashboard\DineDashboardService` (Phase 8) — six section aggregators, all read-only.
- `App\Services\Dine\Dashboard\DineOperationalAlertsService` (Phase 10) — pure function over the assembled dashboard payload; no DB queries of its own.
- `App\Services\Dine\Menu\ActiveMenuResolver` (Phase 5) — branch + daypart + Ramadan-aware menu resolution.

### Controllers added / split (Phase 7)

The fat `App\Http\Controllers\Dine\DineController` now hosts only the residual surfaces (aggregator inbox, deliveries, kiosk, QR, couriers, ramadan toggles). Concerns split into:

- `DineMenusController` — menus, categories, items, modifiers, combos, recipes, ingredients.
- `DineTablesController` — tables, reservations, waitlist.
- `DineOrdersController` — orders + per-line ops (fire / serve / hold / recall / void / status).
- `DineKitchenController` — KDS aggregate + ticket / line advance + stations.
- `DinePaymentsController` — pay + print-check.
- `DineInventoryController` — production batches + complete.
- `DineReportsController` — reports overview + sales-by-hour.
- `DineDashboardController` — Phase-8 dashboard.
- Shared trait: `App\Http\Controllers\Dine\Concerns\DineControllerHelpers` — `applyBranchFilter()` etc.

Every URL is preserved. `DineRouteParityTest` pins this.

### Routes added / changed

- **Added:** `GET /api/v1/dine/dashboard?branch_id={id|all}` (Phase 8). Alerts ride on this same response (Phase 10) — no separate endpoint.
- **No URLs changed.** Phase-7 split moved class names but every public path is identical.
- 67 dine routes today, all under `module:dine`.

### Audit behaviour

- `dine.order.line_voided` (Phase 1) — `medium` severity, payload `{reason, order_id, qty, reversed}`.
- `dine.order.cancelled` (Phase 6) — `high` if `was_paid`, else `medium`; payload `{order_id, ref_no, channel, was_paid, total, cancelled_at}`.
- `dine.menu_item.patched` (existing) — preserved.
- All other domain mutations rely on the `Audited` trait at the row level.

### Inventory behaviour

- `recipe_consume` on `addLine()` — unchanged.
- `recipe_void` on `voidLine()` AND on `transitionStatus(non-paid → cancelled)` — symmetric reversal.
- Paid-cancellation does NOT reverse stock — goods were already consumed (matches accounting reality).

### Permission behaviour

Phase-6 added five granular dine permissions (see *Permissions Added* below). The `module:dine` middleware (Phase 4) wraps every dine route. Step-up gates (Phase 6) on void / refund / production-complete.

---

## Frontend Changes

### `/app/dine/ops` (Phase 9 + 10)

- New `OpsDashboardPage.jsx` — six section components with KPI tiles, tiny native bar chart for hourly revenue, branch picker, manual refresh, "Last updated" stamp.
- Operations tab is now the first entry under the Dine sidebar group; default tab inside Dine.
- Polling: `useDineDashboard(branchId)` with `refetchInterval: 30_000`, `refetchIntervalInBackground: false`, `staleTime: 25_000`.

### Operations tab placement

`DinePage.jsx` GroupedTabNav order:
`Operations · Server POS · Reports · Orders · Floor · KDS`. Default route inside Dine redirects to `ops`.

### Dashboard API hook + polling

- `dineApi.dashboard({ branch_id })` — pure HTTP layer in `dine.api.js`. Omits `branch_id` when value is `'all'` or falsy.
- `useDineDashboard(branchId)` — single source of truth, single polling loop, used only on `OpsDashboardPage`. POS / KDS / Floor / Orders / Reports never touch this hook.

### Alerts panel (Phase 10)

- `OperationalAlertsPanel.jsx` placed at the top of the dashboard, below page header, above Live Operations.
- Severity → Badge tone + card background. `critical` falls back to `danger` (no critical tone in the design system).
- Empty state: green "No active alerts." card — operator always sees the all-clear acknowledgement.
- Optional session-only dismiss with restore. **No persistence**, **no BE call**.
- Tolerates missing / null / non-array `alerts` without crashing.

### Null / untracked handling

- Service skips `null` and non-numeric values — no fake alerts.
- System Health section renders un-tracked slots as `—` with italic "Not tracked yet" note.
- Same rule everywhere the dashboard touches a value: render the gap honestly.

### Tablet / desktop behaviour

- Verified at desktop 1374×900: clean grid, no overflow.
- Verified at tablet 820×1180: KPI grids re-flow cleanly, alerts strip stays compact, all six sections still render.

### Visual verification notes (post-Phase-10)

- Six sections present, alerts panel renders with a real "Delayed orders" warning derived from existing test data.
- Console clean (only standard React Router future-flag warnings, both pre-existing).
- POS, Floor, KDS, Reports siblings still navigate correctly.
- Backward-compat redirects (`/dine/menu` → `/app/dine-menu/items`, etc.) still work.

---

## Endpoints Added or Changed

| URL | Method | Phase | Notes |
|---|---|---|---|
| `/dine/dashboard` | `GET` | 8 (BE) + 10 (alerts in same payload) | Read-only. Cached per (tenant, branch) for 30s. Returns six sections + envelope + `alerts[]`. |
| Active-menu read | (existing endpoints) | 5 | `addLine()` enforces availability window; no new public route — the resolver runs server-side. |

**No URL renamed, no URL removed.** Phase-7 controller split was structural only; `DineRouteParityTest` confirms every dine route still resolves to a real controller method. 67 dine routes today.

---

## Permissions Added

All under guard `sanctum`, all enforced via `manager-PIN step-up tickets`:

- `dine.orders.void` — void an order line.
- `dine.orders.refund` — paid → cancelled (refund flow).
- `dine.orders.discount` — manual order-level discount (key reserved; producer pending).
- `dine.orders.price_override` — manual line price override (key reserved; producer pending).
- `dine.production.approve` — complete a production batch.

Bypass `X-Skip-Step-Up: 1` honoured only when `APP_DEBUG=true` (test env).

---

## Tests Added

| Suite | Phase(s) | Count |
|---|---|---|
| `DineHardeningTest` | 1, 3 | 11 (timestamps, idempotency, cancel locks, refund idempotency) |
| `DineKdsRoutingTest` | 2 | 5 (per-category routing + idempotent emit + catchall) |
| `DineMenuRuntimeTest` | 5 | 8 (availability windows + daypart + branch-specific resolver) |
| `DineStepupTest` | 6 | 4 (void / refund step-up gates) |
| `DineRouteParityTest` | 4, 7 | 4 (every route resolves; void / production routes still have step-up; all routes inside `module:dine`) |
| `DineDashboardTest` | 8 | 6 (shape, empty tenant, branch filter, cache, tenant isolation, module gate) |
| `DineDashboardAlertsTest` | 10 | 17 (threshold ladders, null skip, severity sort, branch isolation, tenant isolation, cache coherence) |
| Frontend `OpsDashboardPage.test.jsx` | 9 | 6 (api shape, hook config, module export) |
| Frontend `OperationalAlertsPanel.test.jsx` | 10 | 12 (severity tone / card / label maps, sort helper, module export) |

**Aggregate:** 112 dine backend tests + 153 frontend tests pass on `741cd12`. See *Testing Summary* in the PR body for exact commands.

---

## Known Limitations

- **System-health signals not yet tracked** (return `null`): `sync_failures_today`, `queue_backlog`, `failed_payments_today`, `failed_printer_jobs_today`. Surfaced as `—` / "Not tracked yet" in the FE; no fake alerts emitted. Documented in `docs/dine/DINE_OPERATIONAL_EVENTS.md` §12.
  - `offline_devices` was on this list at the time of the Phase 1–10 PR; **Phase 11 wires it up** with the `dine_devices` heartbeat table. See `docs/dine/DINE_OPERATIONAL_EVENTS.md` §11a.
- **Alert threshold tuning is global.** Cutoffs are constants in `DineOperationalAlertsService`; no per-tenant overrides. Deliberate — keeps the UX consistent for small/medium tenants. Move to a config table when actually needed.
- **No persistent alert dismissals.** Session-only dismiss in the FE; refresh / re-poll re-surfaces.
- **No notification delivery channels.** No push / SMS / WhatsApp / email. Out of scope.
- **No AI / anomaly detection / ML scoring.** Out of scope.
- **No loyalty / customer engagement / marketing.** Out of scope.
- **No websocket or realtime stream.** Polling is 30s and pauses in background; that's the contract.
- **Some `top_risk_actors` rows show user IDs** (last-8 of UUID) until a user-name resolution surface exists. Intentional — IDs are still actionable when an audit drill-down is just a click away.
- **`manual_discounts_today` and `manual_price_overrides_today`** are exposed as `0` with an italic "not yet wired" hint until the matching producers ship.
- **Dashboard cache TTL is 30 seconds.** Within that window, freshly created delayed orders / new alerts won't appear in the next response. This matches the polling cadence and is the right tradeoff against running 6 aggregations on every page-visible-tick during busy service.

---

## Rollback Notes

- **Per-phase revert.** Each phase landed as a single squash-style commit; `git revert <hash>` is clean. Phase 9 + 9.x can also be reverted as a pair without breaking BE.
- **Migrations are reversible.** Both Phase-1 and Phase-5 migrations carry working `down()` methods. No destructive operations, no data drops.
- **Dashboard route is additive.** Reverting Phase 8 removes `GET /dine/dashboard`; nothing else depends on it.
- **Alerts embedded safely.** Reverting Phase 10 removes `data.alerts` from the response and the FE panel. Frontend tolerates a missing `alerts` field already.
- **No journal rewrite, no inventory rewrite.** Phase-1's stock reversal is symmetric — reverting it stops emitting `recipe_void`, but does not retroactively un-do any movement already written.
- **No POS flow dependency on the dashboard hook.** POS / KDS / Floor / Orders / Reports never call `useDineDashboard` or `dineApi.dashboard` (verified by grep). Reverting the dashboard or alerts has zero cashier-flow impact.
- **Cache expires in 30s.** Any rolled-back dashboard payload self-heals once the cache window passes.

---

## Next Recommended Phases

Operational only — no AI, no loyalty, no marketing, no engagement.

1. **Device heartbeat / offline terminal tracking** — populate `system_health.offline_devices` (today returns `null`).
2. **Sync failure logging** — populate `system_health.sync_failures_today` (today `null`).
3. **Printer job tracking** — populate `system_health.failed_printer_jobs_today` (today `null`).
4. **Failed payment / refund attempt log** — populate `system_health.failed_payments_today` (today `null`).
5. **Production yield → finished-good stock** — currently `production_done_today` only counts batches; tying yield into `inventory_levels` closes the food-cost loop.
6. **Variant-aware low-stock counting** — current `low_stock_count` ignores variant precision (NULL == NULL fallback). Tighten when variants land for inventory.
7. **User-name resolution for `top_risk_actors`** — replace the `…last8` UUID display with `users.name` lookup so the drill-down reads naturally.
8. **SQL / index performance review** — Phase 8 keeps queries tight, but a few `whereHas` paths can grow with order volume; profile under load.
9. **Kitchen course-fire endpoint** — if course firing isn't yet wired, add it; the KDS data is already there.
10. **Async accounting posting** — only if profiling shows the journal poster slowing the cashier flow on `pay()`.

Each of these is a self-contained operational improvement — none requires architectural change.
