# DALSEEN Dine — Routes Map

**Last verified:** Phase-7 split (commit `d873331`).
**Total dine routes:** 66.
**Outer middleware (every route):** `auth:sanctum` → `tenant` → `tenant_active` → `module:dine`.
**Tenant + branch isolation:** enforced at every controller via `BelongsToTenant` scope on every model + `applyBranchFilter()` helper for branch-scoped lists.

---

## How to read this map

| Column | Meaning |
|---|---|
| **URL** | Path under `/api/v1`. |
| **Method** | HTTP verb. |
| **Controller** | Class in `App\Http\Controllers\Dine\…`. |
| **Action** | Method on the controller. |
| **Middleware** | Route-specific middleware (in addition to the global outer chain). |
| **Step-up** | Permission key the manager-PIN dialog must request before the call. |
| **Concern** | Operational domain. |
| **Side effects** | Database/journal/inventory/audit writes. |

---

## 1 · Menus  (`DineMenusController`)

| URL | Method | Action | Middleware | Step-up | Concern | Side effects |
|---|---|---|---|---|---|---|
| `/dine/menus` | `GET` | `menusIndex` | — | — | menu engineering | read-only |
| `/dine/menus` | `POST` | `menusStore` | — | — | menu engineering | inserts `menus` |
| `/dine/menu-categories` | `POST` | `categoriesStore` | — | — | menu engineering | inserts `menu_categories` |
| `/dine/menu-items` | `POST` | `itemsStore` | — | — | menu engineering | inserts `menu_items` |
| `/dine/menu-items/{menuItem}` | `PATCH` | `menuItemsPatch` | — | — | menu engineering | updates `menu_items`; audit `dine.menu_item.patched` |
| `/dine/ramadan-list` | `GET` | `ramadanList` | — | — | menu engineering | read-only |
| `/dine/modifier-groups` | `GET` | `modifierGroupsIndex` | — | — | menu engineering | read-only |
| `/dine/modifier-groups` | `POST` | `modifierGroupsStore` | — | — | menu engineering | inserts `modifier_groups` |
| `/dine/modifiers` | `POST` | `modifiersStore` | — | — | menu engineering | inserts `modifiers` |
| `/dine/modifier-groups/{modifierGroup}/applies-to` | `POST` | `modifierGroupAppliesTo` | — | — | menu engineering | upserts `menu_item_modifier_groups` pivot |
| `/dine/recipes` | `POST` | `recipesUpsert` | — | — | recipes / BOM | upserts `recipes` + `recipe_components` |
| `/dine/combos` | `GET` | `combosIndex` | — | — | bundles | read-only |
| `/dine/combos` | `POST` | `combosStore` | — | — | bundles | inserts `combos` + `combo_components` |
| `/dine/combos/{combo}` | `PATCH` | `combosPatch` | — | — | bundles | updates `combos`/`combo_components` |
| `/dine/recipes` | `GET` | `recipesIndex` | — | — | recipes / BOM | read-only |
| `/dine/ingredients` | `GET` | `ingredientsIndex` | — | — | recipes / BOM | read-only |
| `/dine/ramadan-profiles` | `GET` | `ramadanProfiles` | — | — | menu mode | read-only |
| `/dine/ramadan-mode/toggle` | `POST` | `ramadanToggle` | — | — | menu mode | updates `branches.ramadan_active` |
| `/dine/ramadan-profiles/{branch}/activate` | `POST` | `ramadanActivate` | — | — | menu mode | updates `ramadan_profiles` |

---

## 2 · Tables, Seating, Reservations & Waitlist  (`DineTablesController`)

| URL | Method | Action | Middleware | Step-up | Concern | Side effects |
|---|---|---|---|---|---|---|
| `/dine/tables` | `GET` | `tablesIndex` | — | — | floor | read-only |
| `/dine/tables` | `POST` | `tablesStore` | — | — | floor | inserts `tables` |
| `/dine/tables/{table}` | `PATCH` | `tablesPatch` | — | — | floor | updates `tables` |
| `/dine/tables/{table}/seat` | `POST` | `tablesSeat` | — | — | floor | inserts `dine_orders` (status=open); flips `tables.status=occupied` |
| `/dine/tables/{table}/clear` | `POST` | `tablesClear` | — | — | floor | refuses if order open; flips `tables.status=available` |
| `/dine/reservations` | `GET` | `reservationsIndex` | — | — | guests | read-only |
| `/dine/reservations` | `POST` | `reservationsStore` | — | — | guests | inserts `reservations` |
| `/dine/reservations/{reservation}/confirm` | `POST` | `reservationsConfirm` | — | — | guests | flips `reservations.status` |
| `/dine/reservations/{reservation}/cancel` | `POST` | `reservationsCancel` | — | — | guests | flips `reservations.status` |
| `/dine/waitlist` | `GET` | `waitlistIndex` | — | — | guests | read-only |
| `/dine/waitlist` | `POST` | `waitlistStore` | — | — | guests | inserts `waitlist_entries` |
| `/dine/waitlist/{entry}/notify` | `POST` | `waitlistNotify` | — | — | guests | flips `waitlist_entries.notified_at` |
| `/dine/waitlist/{entry}/seat` | `POST` | `waitlistSeat` | — | — | guests | flips `waitlist_entries.status=seated` |

---

## 3 · Orders & Lines  (`DineOrdersController`)

| URL | Method | Action | Middleware | Step-up | Concern | Side effects |
|---|---|---|---|---|---|---|
| `/dine/orders` | `GET` | `ordersIndex` | — | — | order ops | read-only |
| `/dine/orders` | `POST` | `ordersStore` | — | — | order ops | inserts `dine_orders` (+ `dine_order_lines` if `lines[]` provided); recipe-consume stock movements |
| `/dine/orders/{order}/status` | `PATCH` | `ordersStatus` | — | `dine.orders.refund` *(only when paid → cancelled)* | order ops | drives the status state machine; on `paid` runs `DineOrderPoster`; on `cancelled` may run `DineRefundPoster` mirror; on cancel-from-non-paid reverses recipe stock; audit `dine.order.cancelled` (high if was-paid) |
| `/dine/orders/{order}/fire` | `POST` | `ordersFire` | — | — | order ops | thin wrapper → status=`sent_to_kitchen`; emits KDS tickets if not yet emitted |
| `/dine/orders/{order}/serve` | `POST` | `ordersServe` | — | — | order ops | thin wrapper → status=`served`; stamps `served_at` (Phase-1) |
| `/dine/orders/{order}/items` | `POST` | `ordersAddItem` | — | — | order ops | inserts `dine_order_lines`; reverse-checks MenuResolver availability (Phase-5); recipe-consume stock movement; appends `kds_ticket_lines` if order already fired |
| `/dine/orders/{order}/items/{lineId}/void` | `POST` | `ordersVoidItem` | `stepup:dine.orders.void` | `dine.orders.void` | order ops | flips line `status=voided`; reverses recipe stock (Phase-1, ref_type=`recipe_void`); recomputes order totals; audit `dine.order.line_voided` |
| `/dine/orders/{order}/items/{lineId}/hold` | `POST` | `ordersHoldItem` | — | — | order ops | flips line `status=held`; excluded from KDS ticket emission |
| `/dine/orders/{order}/items/{lineId}/recall` | `POST` | `ordersRecallItem` | — | — | order ops | flips line `status=queued` from `held` |

---

## 4 · Payments  (`DinePaymentsController`)

| URL | Method | Action | Middleware | Step-up | Concern | Side effects |
|---|---|---|---|---|---|---|
| `/dine/orders/{order}/print-check` | `POST` | `ordersPrintCheck` | — | — | check & pay | stamps `dine_orders.check_printed_at` (idempotent) |
| `/dine/orders/{order}/pay` | `POST` | `ordersPay` | — | — | check & pay | flips status to `paid`; runs `DineOrderPoster` (revenue/VAT/COGS/tender legs, channel-routed); idempotent on `(source='dine_order', source_ref=order.id)`; raises `PAYMENT_INSUFFICIENT` on partial pay |

---

## 5 · KDS  (`DineKitchenController`)

| URL | Method | Action | Middleware | Step-up | Concern | Side effects |
|---|---|---|---|---|---|---|
| `/dine/kds` | `GET` | `kdsAggregate` | — | — | kitchen | read-only — joins tickets + order + table + waiter; menu lookup; KPIs (active/ready/late/avg_ticket_min) |
| `/dine/kds/stations` | `GET` | `kdsStations` | — | — | kitchen | read-only |
| `/dine/kds/tickets` | `GET` | `kdsTickets` | — | — | kitchen | read-only |
| `/dine/kds/tickets/{ticket}` | `PATCH` | `kdsTicketAdvance` | — | — | kitchen | flips `kds_tickets.status` (`queued→cooking→ready→served|bumped`); cascades to `kds_ticket_lines`; can mark order `ready` once all tickets ready |
| `/dine/kds/lines/{line}` | `PATCH` | `kdsLineAdvance` | — | — | kitchen | flips a single `kds_ticket_lines.status` |

---

## 6 · Reports  (`DineReportsController`)

| URL | Method | Action | Middleware | Step-up | Concern | Side effects |
|---|---|---|---|---|---|---|
| `/dine/reports/sales-by-hour` | `GET` | `reportsSalesByHour` | — | — | reporting | read-only |
| `/dine/reports/overview` | `GET` | `reportsOverview` | — | — | reporting | read-only — daily KPIs, by_hour, top_items, station prep stats; `avg_table_min` reads `served_at` (Phase-1) |

---

## 7 · Inventory / Production  (`DineInventoryController`)

| URL | Method | Action | Middleware | Step-up | Concern | Side effects |
|---|---|---|---|---|---|---|
| `/dine/recipes` *(see Menus row)* | — | — | — | — | recipes | read-only |
| `/dine/production` | `GET` | `productionIndex` | — | — | prep batches | read-only |
| `/dine/production/{id}/complete` | `POST` | `productionComplete` | `stepup:dine.production.approve` | `dine.production.approve` | prep batches | upserts `production_batches` (status=done, completed_at, completed_by) |

> Note: `recipes`/`ingredients` GET reads also live in `DineMenusController` since they're menu-engineering views; the production write flow lives here. There is no overlap — each URL maps to exactly one controller.

---

## 8 · Channels (residual `DineController`)

These haven't been split out — they're partner-system intake/output and don't fit any of the seven operational concerns cleanly. If a future phase grows them, a `DineChannelsController` would split this further.

| URL | Method | Action | Middleware | Step-up | Concern | Side effects |
|---|---|---|---|---|---|---|
| `/dine/aggregators` | `GET` | `aggregatorsIndex` | — | — | partner config | read-only |
| `/dine/aggregators` | `POST` | `aggregatorsStore` | — | — | partner config | inserts `aggregators` |
| `/dine/aggregator-inbox` | `GET` | `aggregatorInboxIndex` | — | — | partner intake | read-only |
| `/dine/aggregator-inbox/{order}/accept` | `POST` | `aggregatorInboxAccept` | — | — | partner intake | inserts `dine_orders` (channel=`aggregator`); flips `aggregator_orders.status=accepted`; calls partner ack via `AggregatorProvider` |
| `/dine/aggregator-inbox/{order}/reject` | `POST` | `aggregatorInboxReject` | — | — | partner intake | flips `aggregator_orders.status=rejected`; partner reject |
| `/dine/aggregator-inbox/stub-arrival` | `POST` | `aggregatorInboxStubArrival` | — | — | partner intake | inserts test `aggregator_orders` row (stub provider only) |
| `/dine/couriers` | `GET` | `couriersIndex` | — | — | delivery | read-only |
| `/dine/deliveries` | `GET` | `deliveriesIndex` | — | — | delivery | read-only |
| `/dine/deliveries/{delivery}/assign` | `POST` | `deliveriesAssign` | — | — | delivery | flips `delivery_orders.rider_*` fields |
| `/dine/deliveries/{delivery}/delivered` | `POST` | `deliveriesDelivered` | — | — | delivery | flips `delivery_orders.status=delivered` |
| `/dine/kiosk-sessions` | `GET` | `kioskSessions` | — | — | self-order | read-only |
| `/dine/kiosk-sessions` | `POST` | `kioskSessionUpsert` | — | — | self-order | upserts `kiosk_sessions` (stage tracking) |
| `/dine/qr-flows` | `GET` | `qrFlows` | — | — | self-order | read-only |
| `/dine/qr-flows/scan` | `POST` | `qrScan` | — | — | self-order | inserts `qr_flow_events` (scan event) |

---

## Step-up keys catalogue

| Key | Used by | Default role grants |
|---|---|---|
| `dine.orders.void` | `POST /dine/orders/{order}/items/{lineId}/void` | `manager`, `business-owner`, `superadmin` |
| `dine.orders.refund` | `PATCH /dine/orders/{order}/status` *(only on paid → cancelled)* | `manager`, `business-owner`, `superadmin` |
| `dine.orders.discount` | *(reserved for future order-level discount endpoint)* | `manager`, `business-owner`, `superadmin` |
| `dine.orders.price_override` | *(reserved for future line-level override endpoint)* | `manager`, `business-owner`, `superadmin` |
| `dine.production.approve` | `POST /dine/production/{id}/complete` | `manager`, `business-owner`, `superadmin` |

The dev bypass header `X-Skip-Step-Up: 1` is only honored when `APP_DEBUG === true` (i.e. `local` and `testing` environments). Production builds always require a real `X-Step-Up-Ticket`.

---

## Cross-tenant + cross-branch isolation invariants

- Every controller delegates branch filtering through `applyBranchFilter()` (in `Concerns\DineControllerHelpers`), which interprets `?branch_id=all` as "no filter" and the absence of the param the same way.
- Every model that scopes by tenant uses the `BelongsToTenant` concern, applied as a global scope on `auth()->user()->company_id`.
- Cross-tenant write tests (e.g. `cross_tenant_aggregator_accept_returns_404`, `cross_tenant_kds_line_returns_404`) assert that endpoints addressed at another tenant's resource ULID return `404`, not `403` (no information leak about whether the resource exists).

---

## Operational state machines referenced by these routes

### `DineOrder.status` (canonical)

```
open ─→ sent_to_kitchen ─→ ready ─→ served ─→ paid
   │                                    │
   └────────────────────────────────────┴─→ cancelled
                                              ▲
                                        (paid → cancelled  =  refund)
```

- **Operational** (no GL): `open`, `sent_to_kitchen`, `ready`, `served`.
- **Financial close**: `paid` (posts `dine_order` journal) and `cancelled` (mirrors as `dine_order_refund` if previously paid).
- **Locks**: `paid` may only transition to `cancelled` (Phase-3); `cancelled` is terminal (Phase-3).

### `DineOrderLine.status`

```
queued → firing → ready
  ├──→ held    ←─ recall
  └──→ voided  (Phase-1: emits +recipe_void inventory movement)
```
