# User Flows — Cross-Screen Journeys

> **Verified accurate:** 2026-05-02 — structure, doc shape, and cross-references hold against current source. Per-flow API call sequences are point-in-time accurate as of the original write-up; for the **current** wiring of any specific flow, cross-check the matching SCREENS-INVENTORY-* doc and `docs/openapi/openapi.yaml`.
> **Status:** active source-of-truth doc; the navigational-journey catalog (companion to FLOW-INVENTORY's modal-wizard catalog).

**Audience:** integration engineers and product reviewers. Use this doc to trace how a real user moves *across* screens — and which API calls fire in what order, with what idempotency, what role gates, and what side-effects.
**Scope:** end-to-end journeys that span ≥ 2 modules or ≥ 3 screens. Single-screen interactions are covered in the per-module `SCREENS-INVENTORY-*.md` files.
**Companion docs:**
- `SCREENS-INVENTORY-{Common,Retail,Pay,Dine,Accounting,Owner+HR,Platform}.md` — per-screen detail (every screen referenced below has a one-page card)
- `FLOW-INVENTORY.md` — every transactional `AcctFlow` wizard (the *modal* flows under `front/accounting/`)
- `API-USAGE-MAP.md` — endpoint catalogue (every route called below has an entry)
- `INTEGRATION-NOTES.md` — boot order, event bus, integrations gate, cut-over plan
- `MODULE-MAP.md` — file/module geography

> **How this doc differs from `FLOW-INVENTORY.md`.** `FLOW-INVENTORY` catalogues the 16 modal wizard flows driven by the `AcctFlow` engine — single transactional acts. **This doc** catalogues the *navigational* journeys that string those wizards, screens, and modals together (e.g. "open shift → sale → settle → close shift → end-of-day"). Some user flows below *contain* a FLOW-INVENTORY entry as a step.

---

## How to read this doc

Every flow has a fixed shape:

> **<Flow id> · <Flow name>**
> **Persona:** the role(s) doing the journey
> **Trigger:** what kicks it off (UI button, schedule, external event)
> **Pre-conditions:** what must be true (auth state, shift state, branch, integrations gate, …)
> **Screens:** ordered list of screen route keys touched
> **API calls:** ordered list of endpoints fired (with idempotency notes)
> **Side-effects:** events posted, JEs written, stock decrements, audit rows
> **Permissions:** the gates the user passes
> **Failure modes:** where the flow can break and how it recovers
> **Cut-over notes:** what to wire / what's still mocked

Routes use the `<system>.<id>` style from `DALSEEN_NAV`. API calls use `METHOD /path` with `IK` annotation when an `Idempotency-Key` is required. Severity legend: 🔴 high (blocks pilot), 🟠 medium (visible in next phase), 🟡 low (edge / can defer).

---

## Catalogue

| § | Flow id | Persona | Title |
|---|---------|---------|-------|
| 1 | `F.AUTH.signin` | any | Sign in (creds → 2FA) |
| 2 | `F.ONBOARD.tenant` | new owner | New tenant onboarding (8 steps) |
| 3 | `F.RETAIL.shift-day` | cashier | Open shift → sale → settle → close shift |
| 4 | `F.RETAIL.refund-highvalue` | cashier + manager | High-value refund (step-up biometric) |
| 5 | `F.DINE.ticket-life` | server / KDS / cashier | Open table → fire → serve → settle |
| 6 | `F.PAY.ondemand-charge` | merchant | Send payment request → customer pays → settle |
| 7 | `F.ACCT.invoice-to-paid` | accountant | Issue invoice → ZATCA clear → record payment → reconcile |
| 8 | `F.ACCT.month-close` | accountant + owner | Month-end close (lock period → JEs → reports) |
| 9 | `F.HR.hire-to-payroll` | HR + owner | Hire → onboard → first payroll cycle |
| 10 | `F.HR.timeoff` | employee + manager | Time-off request → approve → payroll impact |
| 11 | `F.RBAC.role-change` | owner | Change a user's role (with step-up) |
| 12 | `F.PLATFORM.tenant-create` | super-admin | Provision new tenant from signup |
| 13 | `F.OPS.daily-rollup` | owner / manager | Daily roll-up across all systems |
| 14 | `F.OPS.zero-stock` | manager + accountant | Zero-friction stock → first PI count |
| 15 | `F.OPS.integrations-flip` | owner | Switch books mode (day-boundary enforced) |
| 16 | `F.DINE.aggregator-order` | host / manager | Talabat / HungerStation webhook → inbox → Dine ticket |
| 17 | `F.ACCT.vat-return` | accountant + owner | Quarterly VAT return (FE wizard + backend submission) |

> **v1.1 patch (this revision):** added flows 16 (aggregator) & 17 (VAT return); strengthened the day-boundary on flow 15 to a hard server-enforced block; deprecated the multi-tenant pick branch in flow 1. See §18 for the changelog.

---

## 1 · `F.AUTH.signin` — Sign in

**Persona:** any user
**Trigger:** unauthenticated visit to `/`
**Pre-conditions:** none

### Screens
1. `LoginScreen` (creds stage)
2. `LoginScreen` (2fa stage) — only if `user.twofa === true`
3. ~~`LoginScreen` (tenant stage)~~ — **DEPRECATED.** DALSEEN is single-tenant per user; this branch is dead code today and must be stripped at cut-over. See §18 cleanup ticket.
4. `dashboard` (or last-visited route from `localStorage:dalseen:last-route`)
5. `DashboardSplash` overlays first time after `F.ONBOARD.tenant`

### API calls
| # | Call | IK | Notes |
|---|------|----|-------|
| 1 | `POST /auth/login` | — | `{ email, password }` → `{ token, user, requires_2fa, twofa_method }` |
| 2 | `POST /auth/2fa/verify` | — | `{ code }` → `{ token, session }` (only if `requires_2fa`) |
| 3 | `GET /me` | — | tenant + branch + permissions; set `X-Tenant-Id`, `X-Branch-Id` for all subsequent calls |
| 4 | `GET /shifts/current?branchId={…}` | — | only if role ∈ retail roles → drives Topbar shift pill |
| 5 | `GET /branches` | — | cache for Topbar branch picker |

### Side-effects
- `localStorage:saaed:auth` set with bearer token + tenant + branch
- `sessionStorage:dalseen:currentShift` populated if `GET /shifts/current` returns one
- Audit: `login.success` row written with IP / device / geo (server-side)

### Permissions
None — pre-auth.

### Failure modes
- 🔴 Wrong password ×5 → server returns 423 Locked + Retry-After. FE today only counts client-side (Quirk 2 in `SCREENS-INVENTORY-Common.md §2.1`).
- 🔴 Suspended account → server 403 with `code: account_suspended`. FE shows the message; no retry option.
- 🟠 2FA code expired → server 401 `code: 2fa_expired`. FE today resets the OTP input but doesn't request a new code automatically.
- 🟡 Multi-tenant pick (dead branch today) — wire when multi-tenant ships.

### Cut-over notes
- Replace any-6-digit OTP acceptance with real verify (server check).
- Move attempt-count to server-side.
- Strip the `faisal@dalseen.sa` autofill default before pilot.
- Wire `Sign in with OTP to phone` (no handler today).
- **Strip multi-tenant pick stage** (component branch + `requires_tenant_pick` flag handling). Single-tenant-per-user is the product decision. Tracked as `CLEANUP-AUTH-001`.

---

## 2 · `F.ONBOARD.tenant` — New tenant onboarding (8 steps)

**Persona:** new owner (just signed up)
**Trigger:** `setup_token` parameter on first login OR `me.setup_progress.completed === false`
**Pre-conditions:** account created, email verified

### Screens
1. `LoginScreen` (with `setup_token`)
2. `core.onboarding` wizard — 8 steps:
   1. Account created (auto)
   2. Verify CR (commercial registration)
   3. Configure business profile
   4. Pick verticals (retail / pay / dine / hr) → drives sidebar density
   5. Configure ZATCA (Phase II VAT registration)
   6. Configure SAMA / payment provider (Geidea, mada Hayyak)
   7. Invite admins → uses `OnboardingAdminsStep` (Common §5.9)
   8. Choose books mode + stock mode → writes through `DalseenIntegrations`
3. `DashboardSplash` (first time)
4. `dashboard`

### API calls
| # | Call | IK | Notes |
|---|------|----|-------|
| 1 | `GET /me/setup-progress` | — | Drives the wizard's resume point |
| 2 | `PATCH /tenants/me` | ✓ | Profile (legal name, AR/EN names, address) |
| 3 | `POST /tenants/me/cr-verify` | ✓ | Triggers Wathiq lookup |
| 4 | `PATCH /tenants/me/zatca` | ✓ | Onboards onboarding cert via Fatoora |
| 5 | `PATCH /tenants/me/payment-provider` | ✓ | Geidea / mada keys |
| 6 | `POST /users/invite` (×N) | ✓ | One per admin invitee — see `SCREENS-INVENTORY-Common.md §5.9` |
| 7 | `PATCH /tenants/me/integrations` | ✓ | Books + stock mode (this doc §15) |
| 8 | `POST /me/setup-progress/complete` | — | Flips the gate |

### Side-effects
- Tenant verticals (`retail`, `pay`, `dine`, `hr`) flip on → `DALSEEN_NAV` filters reveal those system pills in the sidebar
- ZATCA submission ledger initialised; OTP cert pinned
- `sessionStorage:dalseen:splash-seen` cleared so DashboardSplash shows
- Audit: 8 `tenant.setup.*` rows
- Email: invite emails to admins

### Permissions
- All steps gated to the `owner` role (or whoever holds the setup token).
- Step 6 (invite admins) creates `pending` users.

### Failure modes
- 🔴 CR verify timeout (Wathiq 504) → wizard offers retry; can be skipped and resumed (FE today doesn't gate on this — it should)
- 🟠 ZATCA OTP wrong → step 4 retries inline up to 3 times
- 🟡 User abandons wizard mid-flow → resume from `me.setup_progress.last_step`. FE handles this, but the `OnboardingAdminsStep` invite list is *not* persisted across abandons (Quirk in §5.9).

### Cut-over notes
- See `SCREENS-INVENTORY-Owner+HR.md §3` for the wizard component detail.
- Each step's sub-flow has its own `FLOW-INVENTORY` entry where applicable (CR verify, ZATCA cert exchange).
- Detailed onboarding-step → endpoint mapping lives in `docs/handoff/onboarding-chain-endpoint-mapping.md`.

---

## 3 · `F.RETAIL.shift-day` — Open shift → sale → settle → close shift

The **single most-walked** flow in the system. Most cashiers run this 1×/day.

**Persona:** cashier (and indirectly: customer at counter)
**Trigger:** cashier signs in to a register
**Pre-conditions:** authed, role ∈ retail, no active shift on this terminal

### Screens
1. `dashboard` → `ShiftLauncher` (Common §5.3) appears
2. `StartShiftModal` (Common §5.2) — pick terminal, count opening float, biometric
3. `retail.pos` — the POS itself
4. `PayModal` (Common §5.4) — settle a sale
5. … repeat 3–4 N times for N sales …
6. `retail.shift-close` — count closing float, declare variance
7. `retail.day-close` — end-of-day rollup (only if last shift on register today)

### API calls (per shift)
| # | Call | IK | Notes |
|---|------|----|-------|
| 1 | `POST /shifts/open` | ✓ | `{ branchId, terminalId, openingFloat, biometricToken }` → `{ shiftId, openedAt, expectedFloat }` |
| 2 | `GET /catalog/products?branchId=…` | — | warm POS product grid (cached per shift) |
| **per sale** | | | |
| 3a | `POST /sales` | ✓ | `{ shiftId, lines, payments, customerId? }` → `{ saleId, total, vat }` |
| 3b | `POST /accounting/journal-entries` | ✓ | Only if `DalseenIntegrations.shouldPostGL(branchId)` (Common §6.1) |
| 3c | `POST /inventory/adjustments` | ✓ | Only if `DalseenIntegrations.shouldDecrementStock(branchId)` |
| 3d | `POST /zatca/submissions` | ✓ | Phase II e-invoice clearance (B2B) or reporting (B2C) |
| 3e | `POST /payments/charge` | ✓ | When method ≠ cash; provider integration (Geidea / mada) |
| **per shift close** | | | |
| 4 | `POST /shifts/{id}/close` | ✓ | `{ closingFloat, variance, varianceReason? }` |
| 5 | `POST /sessions/day-close` | ✓ | Only if last shift; aggregates Z-report |

### Side-effects
- `dalseen:shift-opened` event → Topbar shift pill, dashboard live tab refresh
- Per sale: `dalseen:sale-completed` event → BI tile, AI Copilot context, dashboard live
- Per sale (if `shouldPostCOGS`): COGS leg appended to JE
- Per sale: receipt printed via `printer-01` adapter (Common §6.2), drawer kicked via `drawer-01`
- Per sale: ZATCA QR code on receipt
- On shift close: variance row in `audit.shift.variance` if non-zero
- On day close: Z-report PDF generated; `dalseen:day-closed` event

### Permissions
- `shifts.open` (cashier+)
- `sales.create` (cashier+)
- `shifts.close.own` (cashier — own shift only) or `shifts.close.any` (manager+)
- High-value sale (> threshold from `DALSEEN_ROLE_POLICIES[role].highValueThreshold`) gates `BiometricPrompt` step-up

### Failure modes
- 🔴 Network drop mid-sale → idempotent retry on `POST /sales` with same `IK`. FE today queues to `localStorage` and resyncs (see retail offline mode in `SCREENS-INVENTORY-Retail.md §POS`).
- 🔴 ZATCA submission fails → sale is **still recorded** (cannot block customer) but submission queued for retry (per FE behaviour today; spec see `BACKEND-MAPPING.md §ZATCA`).
- 🟠 Drawer open variance → forces variance reason at close.
- 🟠 Printer offline → receipt held in queue; cashier can re-print from `retail.receipts`.
- 🟡 Two tabs opened by same cashier → second `POST /shifts/open` rejected with 409 (server-side). FE today doesn't dedupe (Quirk 1 in Common §5.2).

### Cut-over notes
- Detailed mapping per step in `docs/handoff/shifts-day-close-endpoint-mapping.md`.
- Detailed POS-flow mapping in `docs/handoff/POS-flow-endpoint-mapping.md`.
- This flow gates on the **integrations gate** (Common §6.1). When `acct=off`, skip 3b. When `stock=off`, skip 3c. When both off, the sale is just a receipt + payment record.

---

## 4 · `F.RETAIL.refund-highvalue` — High-value refund

**Persona:** cashier initiates, manager approves
**Trigger:** customer return at counter, original sale total ≥ SAR 500 (or whatever `DALSEEN_HIGH_RISK['void']` says)
**Pre-conditions:** active shift, original sale within return window

### Screens
1. `retail.pos` → `Returns` quick action
2. `retail.returns` — search original sale, pick lines, reason
3. `BiometricPrompt` (Common §5.1) — for the cashier
4. **If above cashier's `highValueThreshold`:** Manager approval modal — manager taps in
5. `BiometricPrompt` again — for the manager
6. `PayModal` — issue refund (cash / mada reversal / store credit)
7. Receipt printed

### API calls
| # | Call | IK | Notes |
|---|------|----|-------|
| 1 | `GET /sales/{id}` | — | Pull original |
| 2 | `POST /auth/webauthn/challenge` | — | Cashier step-up |
| 3 | `POST /auth/webauthn/verify` | — | → step-up token |
| 4 | `POST /sales/{id}/refund-request` | ✓ | `{ lines, reason, cashierStepupToken }` → `{ requestId, requiresApproval }` |
| 5 (if approval) | `POST /auth/webauthn/challenge` (manager) | — | Manager step-up |
| 6 | `POST /sales/{id}/refund-approve` | ✓ | `{ requestId, managerStepupToken }` |
| 7 | `POST /payments/refund` | ✓ | provider call |
| 8 | `POST /accounting/journal-entries` | ✓ | reversal JE if `shouldPostGL` |
| 9 | `POST /inventory/adjustments` | ✓ | restock if `shouldDecrementStock` and item is restockable |

### Side-effects
- Audit: `refund.requested`, `refund.approved`, `refund.executed` rows
- Original sale's `refunded_amount` updated
- ZATCA credit-note submission

### Permissions
- `sales.refund.request` (cashier)
- `sales.refund.approve.high` (manager+) — gated on threshold
- Both require **fresh** WebAuthn step-up (within last 60s)

### Failure modes
- 🔴 Step-up token reuse → server 401 (token must be fresh per request). FE today does **not** thread the token through — see Quirk 1 in Common §5.1.
- 🔴 Manager unavailable → request held in `audit.refund.pending` for next manager session.
- 🟠 Original sale older than return window → 422 with `code: out_of_window`.

### Cut-over notes
- This is the canonical **high-risk action** flow — every other gated action (role change, payroll approval, large discount override) follows the same shape.
- Token plumbing is the 🔴 outstanding work. Today the FE lets the action proceed after a successful `BiometricPrompt` resolve without carrying any proof.

---

## 5 · `F.DINE.ticket-life` — Open table → fire → serve → settle

**Persona:** server (or host), KDS, cashier
**Trigger:** customer seated
**Pre-conditions:** dine vertical enabled, table free

### Screens
1. `dine.tables` (host) — pick free table, mark seated
2. `dine.order` (server, counter) **or** `dine.waiter` (server, handheld) — add items
3. (Items appear on) `dine.kds` — kitchen sees ticket
4. `dine.kds` — kitchen taps "fired" → "serving" → "ready"
5. `dine.tables` — server picks up, marks "served"
6. `dine.order` (recall ticket) → `PayModal` — settle
7. Table flips to `dirty`, then `free` after busser

### API calls
| # | Call | IK | Notes |
|---|------|----|-------|
| 1 | `POST /tables/{id}/seat` | ✓ | `{ partySize, reservationId? }` |
| 2 | `POST /orders` | ✓ | `{ tableId, lines }` → orderId |
| 3 | `POST /orders/{id}/items` (×N) | ✓ | each course/round |
| 4 | `POST /orders/{id}/fire` | ✓ | pushes to KDS |
| 5 | `PATCH /kds/items/{id}` (×N) | — | KDS state transitions |
| 6 | `POST /orders/{id}/serve` | — | server marks served |
| 7 | `POST /orders/{id}/close` | ✓ | settles ticket → triggers `POST /sales` (this is the seam between Dine and Retail) |
| 8 | `POST /payments/charge` | ✓ | as in §3 |
| 9 | (post-settle) JE / inventory / ZATCA — per §3.3b–e |

### Side-effects
- `dine.tables` state machine: `free → busy → busy(served) → dirty → free`
- KDS audio chime on each fire
- Recipe-cost decrement (when stock integration is on; see Dine §recipes)
- ZATCA: settle generates the same Phase II receipt as a retail sale

### Permissions
- `dine.tables.seat` (server, host)
- `dine.orders.create`, `dine.orders.modify` (server)
- `kds.act` (kitchen)
- `dine.orders.close` (server, manager)

### Failure modes
- 🔴 The "Send to Kitchen" button has **no handler** today — see Quirk 1 in `SCREENS-INVENTORY-Dine.md §2.1`.
- 🟠 Server transfers a table mid-meal → `POST /tables/{id}/move` (Dine §2.2)
- 🟡 Aggregator order (Talabat / HungerStation) lands directly on `dine.delivery` — out of scope here.

### Cut-over notes
- Dine has **no live wrapper** today (`SCREENS-INVENTORY-Dine.md §1.2`). All 9 screens read fixtures directly. Step 1 of cut-over: write `front/dine/dine-live.jsx` and point at `useApi`.
- The **settle-to-sale seam** is the most subtle bit: closing a Dine ticket fires a Retail `POST /sales` — Dine never has its own payment endpoint.

---

## 5b · `F.DINE.aggregator-order` — Talabat / HungerStation webhook → inbox → Dine ticket

**Persona:** host or floor manager
**Trigger:** **External** — Talabat, HungerStation, Jahez, or Mrsool POSTs an order to the DALSEEN webhook endpoint. The FE never initiates this flow; it polls the resulting inbox.
**Pre-conditions:** Dine vertical enabled; aggregator integration configured (provider keys set in `shared.integrations`); branch is `accepting_orders`.

### How it joins F.DINE.ticket-life
This flow is the **ingest half** of a Dine ticket that didn't start at a table. Once accepted, it joins `F.DINE.ticket-life` (§5) at the **"fired" stage** — KDS sees the items and prepares them. There is no settle step (already paid through the aggregator).

### Screens
1. (External) Talabat / HungerStation webhook → backend `IngestAggregatorOrderController` (server-only)
2. `dine.aggregator.inbox` — host sees pending orders. FE polls every 10s (no SSE today; see cut-over notes).
3. Order detail card — items, customer, ETA, courier status
4. **Accept** → order becomes a Dine ticket; deep-link to `dine.kds` (joins `F.DINE.ticket-life` at "fired")
5. **Reject** → reason picker (e.g. `out_of_stock`, `branch_closed`, `cant_meet_eta`); webhook back to aggregator

### API calls
| # | Call | IK | Notes |
|---|------|----|-------|
| 0 (server) | `POST /webhooks/aggregator/{provider}` | — | server-only; backend ingests, signs verifies, persists |
| 1 | `GET /aggregator/inbox?branchId=…&status=pending` | — | FE poll (10s); returns `{ orders: [...], lastPollCursor }` |
| 2 | `POST /aggregator/{id}/accept` | ✓ | `{ etaMinutes? }` → creates Dine ticket; returns `{ orderId, ticketId }` |
| 3 | `POST /aggregator/{id}/reject` | ✓ | `{ reason, note? }` → webhook back to aggregator |
| 4 (auto, on accept) | internal: `POST /orders` (server-side) | — | server creates the Dine ticket; FE doesn't call this directly |
| 5 (auto, on accept) | internal: `POST /orders/{id}/fire` (server-side) | — | auto-fires — no "send to kitchen" step needed for aggregator orders |

### Side-effects
- On accept: Dine ticket appears on `dine.kds` immediately (skips `dine.tables` — no table assigned)
- On accept: aggregator gets a `confirmed` callback with our ETA
- On reject: aggregator gets a `rejected` callback with reason; aggregator may re-route to another vendor
- Audit: `aggregator.order.{ingested,accepted,rejected}` rows
- **No ZATCA submission on our side** — the aggregator owns the customer receipt; we file VAT against the aggregator (B2B) at month-end
- **No `POST /sales`** at settle time — payment is already with the aggregator; we book revenue when the aggregator settles to us (separate flow, not in scope here)

### Permissions
- `dine.aggregator.inbox` (host, manager)
- `dine.aggregator.accept` (host, manager)
- `dine.aggregator.reject` (manager only — rejecting impacts our aggregator rating)

### Failure modes
- 🔴 Webhook signature fails — server rejects ingest with 401; aggregator retries with exponential backoff. FE never sees these.
- 🔴 Branch not configured for this provider — webhook rejected with 422; observability ticket.
- 🟠 Auto-rejection at SLA — if no host accepts within the aggregator's window (default 90s), the order auto-rejects with reason `timeout`. FE inbox highlights orders > 60s old in red as a heads-up.
- 🟠 Item out-of-stock at accept time — host accepts then realises out-of-stock; FE today doesn't have a "partial accept" path. Mitigation: reject with `out_of_stock` and let aggregator re-place.
- 🟡 Duplicate webhook (aggregator retries while we're processing) — server-side dedupe by `provider_order_id`; FE never sees duplicates.

### Cut-over notes
- Confirmed canonical — backend webhooks at `POST /webhooks/aggregator/{provider}`; FE consumes `GET /aggregator/inbox`, `POST /aggregator/{id}/accept`, `POST /aggregator/{id}/reject` (verified in `routes/api.php` — `ListAggregatorInboxController`, `AcceptAggregatorOrderController`, `RejectAggregatorOrderController`).
- **FE polling → SSE upgrade** is the v1.1 ask. 10s polling is fine for pilot; replace with SSE when `dashboard.live` SSE lands (Common §4.1 Quirk 1).
- **No FE wizard.** This is an inbox + accept/reject UI — not a multi-step modal flow. It does **not** belong in `FLOW-INVENTORY.md`.
- **Screens to add (not in `front/dine/` today):** `dine.aggregator.inbox`, `dine.aggregator.detail`. Tracked as `DINE-AGG-001`. The settle-time linkage to `F.DINE.ticket-life` only differs in: no settle step.

---

## 6 · `F.PAY.ondemand-charge` — Send payment request → customer pays → settle

**Persona:** merchant (any role with `pay.create`)
**Trigger:** merchant clicks "Charge" on `pay.requests`
**Pre-conditions:** Pay vertical enabled, valid SAMA-licensed account

### Screens
1. `pay.requests` — new charge form (amount, customer, method)
2. Send link via SMS / WhatsApp / email
3. (External) customer opens link → SAMA-hosted page → pays
4. `pay.requests` — webhook flips request to `paid`
5. (Auto) `accounting.invoices` row created if `shouldPostGL`

### API calls
| # | Call | IK | Notes |
|---|------|----|-------|
| 1 | `POST /pay/requests` | ✓ | `{ amount, currency, customer, methods, ttl }` → `{ requestId, payUrl }` |
| 2 | `POST /pay/notifications/send` | ✓ | SMS or email delivery |
| **webhook** | | | |
| 3 | `POST /pay/webhook` (server-receive) | — | provider callback; flips state |
| 4 (auto) | `POST /accounting/journal-entries` | ✓ | if `shouldPostGL` |

### Side-effects
- Audit: `pay.request.created`, `pay.request.paid`
- Real-time tile on `pay.dashboard` updates
- ZATCA: B2B receipt clearance auto-fires

### Permissions
- `pay.create` (manager+)
- `pay.refund` (manager+)

### Failure modes
- 🔴 Webhook lost → reconcile job runs every 5 min against provider. Manual reconcile on `pay.reconcile` screen.
- 🟠 Customer abandons (TTL expires) → request flips to `expired`.

### Cut-over notes
- Pay is a thin module — see `SCREENS-INVENTORY-Pay.md`. Most heavy lifting is in the SAMA-hosted payment page (out of FE scope).

---

## 7 · `F.ACCT.invoice-to-paid` — Issue invoice → ZATCA clear → record payment → reconcile

**Persona:** accountant
**Trigger:** owner invoices a B2B customer (not POS)
**Pre-conditions:** accounting vertical enabled, customer has CR + VAT number

### Screens
1. `accounting.invoices.create` — quick-invoice flow (`FLOW-INVENTORY.md §3.1`)
2. `shared.zatca` — submission ticks in
3. `accounting.invoices.{id}` — detail page
4. `accounting.payments.record` — record customer payment (`FLOW-INVENTORY.md §3.2`)
5. `accounting.bank-rec` — match against bank statement (`FLOW-INVENTORY.md §3.3`)

### API calls
| # | Call | IK | Notes |
|---|------|----|-------|
| 1 | `POST /accounting/invoices` | ✓ | `{ customerId, lines, currency, dueDate }` |
| 2 | `POST /zatca/submissions` | ✓ | auto-fired on invoice issue |
| 3 | `POST /accounting/journal-entries` | ✓ | issue JE — receivable + revenue + VAT |
| **on payment** | | | |
| 4 | `POST /accounting/payments` | ✓ | `{ invoiceId, amount, method, reference }` |
| 5 | `POST /accounting/journal-entries` | ✓ | payment JE — bank/cash + receivable contra |
| **on bank-rec** | | | |
| 6 | `POST /accounting/bank-rec/match` | ✓ | `{ statementLineId, paymentId }` |

### Side-effects
- Customer's AR aging updates
- ZATCA cleared QR on invoice PDF
- Income recognised on issue (accrual)
- Bank-rec status: `unmatched → matched → reconciled`

### Permissions
- `accounting.invoices.create` (accountant+)
- `accounting.payments.record` (accountant+)
- `accounting.bank-rec` (accountant+)

### Failure modes
- 🔴 ZATCA reject (bad VAT format) → invoice flagged; cannot send to customer until fixed.
- 🟠 Cheque payment → posts as `pending` until bank-rec match (see `FLOW-INVENTORY.md §3.4.5` gap).
- 🟡 Bank-rec mismatch (FX, fee deduction) → suggested match shown but operator confirms.

### Cut-over notes
- This entire flow is well-covered by `FLOW-INVENTORY.md §3` — see there for the JE preview tables.
- Cheque clearance is a **known gap** — same gap as supplier-side (`FLOW-INVENTORY.md §4.2.5`).

---

## 8 · `F.ACCT.month-close` — Month-end close

**Persona:** accountant + owner sign-off
**Trigger:** end of fiscal month
**Pre-conditions:** all branches' day-closes done; bank-recs current

### Screens
1. `accounting.month-close` — checklist
2. `accounting.adjusting-je` — accrual / depreciation / FX revaluation entries (`FLOW-INVENTORY.md §5.x`)
3. `accounting.reports.trial-balance` — review
4. `accounting.reports.profit-loss` — review
5. `accounting.reports.balance-sheet` — review
6. `accounting.month-close` — owner sign-off (BiometricPrompt)
7. `accounting.period-lock` — lock period (no further posts to closed month)

### API calls
| # | Call | IK | Notes |
|---|------|----|-------|
| 1 | `GET /accounting/month-close/checklist?period=YYYY-MM` | — | drives the screen |
| 2 | `POST /accounting/journal-entries` (×N) | ✓ | adjusting entries |
| 3 | `GET /accounting/reports/trial-balance?period=…` | — | |
| 4 | `GET /accounting/reports/profit-loss?period=…` | — | |
| 5 | `GET /accounting/reports/balance-sheet?period=…` | — | |
| 6 | `POST /accounting/period-lock` | ✓ | `{ period, ownerStepupToken }` |

### Side-effects
- All JEs in the period become read-only
- `audit.period.closed` row
- Reports cached snapshot

### Permissions
- `accounting.month-close.run` (accountant)
- `accounting.period-lock` (owner) + WebAuthn step-up

### Failure modes
- 🔴 In-flight transaction at lock → server rejects with `code: period_inflight`. FE today doesn't surface this — should show "wait 5s for in-flight to settle".
- 🟠 Negative bank-rec balance → checklist blocks lock until resolved.

### Cut-over notes
- See `FLOW-INVENTORY.md §5` for the adjusting-JE flows that feed this.
- Period-lock is **server-enforced** — FE just shows the gate.

---

## 8b · `F.ACCT.vat-return` — Quarterly VAT return (FE wizard + backend submission)

**Persona:** accountant prepares; owner signs off
**Trigger:** end of fiscal quarter; ZATCA portal opens for filing.
**Pre-conditions:** all branches' day-closes done for the quarter; bank-recs current; §8 month-close run for each month in the quarter.

### Architecture (hybrid — FE wizard + backend submission)
- **FE owns:** period selection, review of the 7 boxes, declarant identity, confirm-and-file action.
- **Backend owns:** the 7-box computation from period JEs, ZATCA submission, digital signature (Phase II cert), webhook callback, status update.

### Screens
1. `accounting.vat.list` — list of past returns + the open period for this quarter
2. `accounting.vat.compose` — wizard:
   1. **Period select** — quarter picker (defaults to current open quarter)
   2. **Review boxes** — server-computed 7-box VAT return (output VAT, input VAT, zero-rated, exempt, exports, imports, net payable). FE displays read-only; if the accountant disagrees, they go fix the underlying JEs and re-run.
   3. **Declarant** — pick which authorised user files (their CR-bound identity goes on the submission); biometric step-up
   4. **Confirm & file**
3. `accounting.vat.detail.{id}` — post-submission status (pending, accepted, rejected, paid)
4. (Async) ZATCA webhook callback flips status; `accounting.vat.list` reflects on next refresh

### API calls
| # | Call | IK | Notes |
|---|------|----|-------|
| 1 | `GET /accounting/vat?period=Q1-2026` | — | server returns the **computed** 7 boxes from period JEs + supporting drill-downs (which JEs feed each box) |
| 2 | `GET /accounting/vat/declarants` | — | list of users authorised to file (their `cr_role` must be `declarant`) |
| 3 | `POST /auth/webauthn/challenge` | — | declarant step-up |
| 4 | `POST /auth/webauthn/verify` | — | → step-up token |
| 5 | `POST /accounting/vat/file` | ✓ | `{ period, confirm: true, declarant_id, stepupToken }` → server queues ZATCA submission, returns `{ submissionId, status: 'pending' }` |
| 6 (server) | ZATCA webhook → `POST /webhooks/zatca/vat-return` | — | flips submission to `accepted` / `rejected` |
| 7 | `GET /accounting/vat/{id}` | — | FE polls or reloads to see status; could upgrade to SSE later |

### Side-effects
- On file: VAT-payable JE auto-posts (output VAT → ZATCA payable account)
- Period covered by return becomes **read-only** for VAT-affecting JEs (server-enforced)
- `audit.vat.return.{computed,filed,accepted,rejected}` rows
- On `accepted`: payable invoice generated against ZATCA; flows into `F.ACCT.invoice-to-paid` (§7) when paid
- On `rejected`: detailed reason from ZATCA shown; accountant can fix and re-file

### Permissions
- `accounting.vat.review` (accountant)
- `accounting.vat.file` (declarant only — specific role gate; not all accountants are declarants)
- WebAuthn step-up required at file step (high-risk action)

### Failure modes
- 🔴 Box computation contradicts accountant's expectation — they cannot edit boxes directly. Workflow: cancel wizard, fix underlying JEs in `accounting.adjusting-je`, re-open wizard. The 7-box computation is the **single source of truth**; manual override is intentionally not provided.
- 🔴 ZATCA submission rejected (bad cert, format error, late filing) — status flips to `rejected` with detail; FE shows on `accounting.vat.detail` with a "Re-file" CTA. Re-file uses a fresh `Idempotency-Key`.
- 🔴 Period not closed — server returns 409 `code: period_open` if any month in the quarter isn't through `F.ACCT.month-close` (§8). FE pre-checks via `GET /accounting/vat?period=…` (which includes a `readiness` envelope) and gates the "File" button.
- 🟠 ZATCA portal down — submission queues server-side; status stays `pending` until portal returns. FE shows "Submission queued (ZATCA portal unavailable, retrying)".
- 🟡 Declarant step-up token expires before submit — FE re-prompts; submission re-queued with same `Idempotency-Key` on retry (server dedupes).

### Cut-over notes
- Confirmed canonical — `POST /accounting/vat/file` exists in `routes/api.php` with body `{ period, confirm, declarant_id }`; backend computes the 7 boxes server-side from period JEs.
- **FE wizard scope is narrow:** period select → review (read-only) → declarant + step-up → confirm. No box editing. No manual JE adjustment from inside the wizard — that's a separate flow.
- **Status polling → SSE upgrade** — same v1.1 ask as aggregator inbox. Polling at 30s is fine for pilot.
- **Screen work to add:** `accounting.vat.{list,compose,detail}`. Tracked as `ACCT-VAT-001`. Layout shares the `accounting.month-close` checklist visual language.
- **Idempotency-Key strategy:** generated by FE on entry to step 4 of the wizard; reused on every retry of `POST /accounting/vat/file` until the wizard is dismissed or the submission lands. New wizard session = new key.

---

## 9 · `F.HR.hire-to-payroll` — Hire → onboard → first payroll

**Persona:** HR officer + owner sign-off
**Trigger:** new hire
**Pre-conditions:** HR vertical enabled

### Screens
1. `hr.candidates` — pipeline; advance candidate to "offer accepted"
2. `hr.hire` — wizard: personal info, employment terms, banking, GOSI, Mol
3. `hr.contracts` — generate Saudi-compliant contract; e-sign
4. `hr.onboarding` — checklist (badge issue, system access, training)
5. `hr.employees.{id}` — full record live
6. `hr.payroll.cycle.{period}` — appears in next cycle
7. `BiometricPrompt` — owner approves payroll
8. `accounting.journal-entries` — payroll JE auto-posted

### API calls
| # | Call | IK | Notes |
|---|------|----|-------|
| 1 | `POST /hr/candidates/{id}/advance` | ✓ | → "offer accepted" |
| 2 | `POST /hr/employees` | ✓ | hire transaction |
| 3 | `POST /hr/contracts` | ✓ | generate contract |
| 4 | `POST /hr/contracts/{id}/sign` | ✓ | e-sign (Mol-compliant) |
| 5 | `POST /hr/gosi/register` | ✓ | GOSI registration |
| 6 | `POST /hr/badges/issue` | ✓ | uses `badge-01` adapter |
| 7 | `POST /hr/payroll/{period}/run` | ✓ | computes cycle |
| 8 | `POST /hr/payroll/{period}/approve` | ✓ | owner step-up |
| 9 | `POST /hr/payroll/{period}/disburse` | ✓ | bank file generation |
| 10 | `POST /accounting/journal-entries` | ✓ | payroll JE |

### Side-effects
- Mol notification of new employee
- GOSI contribution starts
- WPS (Wage Protection System) inclusion
- Audit trail for entire onboarding

### Permissions
- `hr.candidates.advance` (HR)
- `hr.employees.create` (HR)
- `hr.payroll.run` (HR)
- `hr.payroll.approve` (owner) + step-up

### Failure modes
- 🔴 GOSI registration fails (invalid Iqama) → onboarding paused, escalation to HR officer
- 🟠 Bank account validation fails → re-collect
- 🟡 Contract template mismatch → fall back to manual upload

### Cut-over notes
- See `SCREENS-INVENTORY-Owner+HR.md §HR` for screen detail.
- WPS file generation is a **batch job** — runs nightly, not on-demand.
- Employee record lifecycle is the longest in the system; see HR module's data model in `BACKEND-MAPPING.md §HR`.

---

## 10 · `F.HR.timeoff` — Time-off request → approve → payroll impact

**Persona:** employee + manager + (auto) payroll
**Trigger:** employee taps "Request leave" on `hr.me.timeoff`

### Screens
1. `hr.me.timeoff` — employee submits
2. (Notification) manager sees in `hr.team.timeoff`
3. `hr.team.timeoff` — manager approves / rejects
4. `hr.attendance` — auto-populated for the period
5. `hr.payroll.cycle.{period}` — auto-deducts unpaid leave or counts paid

### API calls
| # | Call | IK | Notes |
|---|------|----|-------|
| 1 | `POST /hr/timeoff/requests` | ✓ | employee |
| 2 | `POST /hr/timeoff/requests/{id}/approve` | ✓ | manager |
| 3 | `GET /hr/attendance?employeeId=…&period=…` | — | refresh |
| 4 (auto) | payroll cycle pulls `GET /hr/timeoff/balance` | — | applied at run time |

### Side-effects
- Calendar entry on `hr.team.calendar`
- Annual leave balance decremented
- Payroll line item: paid / unpaid

### Permissions
- `hr.me.timeoff.request` (employee)
- `hr.team.timeoff.approve` (direct manager only — gated by `me.report_chain`)

### Failure modes
- 🟠 Insufficient balance → server 422 `code: balance_insufficient`. FE today shows generic error — should show balance details.
- 🟡 Overlapping critical period (month-end close) → soft warning, not block.

### Cut-over notes
- Manager scope (`me.report_chain`) is **server-enforced** — FE today just lists all team requests for any "manager" role. Tighten before pilot.

---

## 11 · `F.RBAC.role-change` — Change a user's role

**Persona:** owner (or super-admin against any tenant)
**Trigger:** User clicks "Change role" on `retail.users` row

### Screens
1. `retail.users` → `EditUserModal` (Common §5.5)
2. `BiometricPrompt` — owner step-up
3. (List update) `retail.users`
4. (Audit) `admin.audit` — `perm.change` row

### API calls
| # | Call | IK | Notes |
|---|------|----|-------|
| 1 | `POST /auth/webauthn/challenge` | — | owner step-up |
| 2 | `POST /auth/webauthn/verify` | — | → step-up token |
| 3 | `PATCH /users/{id}` | ✓ | `{ role, stepupToken }` |
| 4 (server) | side: `audit.access` row, target user's session forced refresh on next request | | |

### Side-effects
- Target user's `me.permissions` reload on next request → sidebar reshapes
- `audit.access` `perm.change` row with before/after
- If demoted: target's open shifts continue (don't kill mid-shift); but high-value action access cuts off immediately

### Permissions
- `users.role.change` (owner / super-admin)
- WebAuthn step-up required (Common §5.1)

### Failure modes
- 🔴 Step-up token reuse — see §4 failure modes (same root cause)
- 🟠 Demoting the only owner → server 422 `code: last_owner` (server-enforced; FE today doesn't pre-check)

### Cut-over notes
- This flow is the canonical pattern for **any RBAC mutation** (assign branches, suspend, activate). Same shape.
- See `FLOW-INVENTORY.md` if a future flow wraps this in a wizard.

---

## 12 · `F.PLATFORM.tenant-create` — Provision new tenant from signup

**Persona:** super-admin (Dalseen staff)
**Trigger:** new signup lands in `platform.signups`

### Screens
1. `platform.signups` — incoming signups list
2. `platform.signups.{id}` — detail / verify
3. `platform.tenants.create` — provision wizard
4. `platform.tenants.{id}` — live record
5. (Email out) — owner gets activation email → flows into §2

### API calls
| # | Call | IK | Notes |
|---|------|----|-------|
| 1 | `GET /platform/signups` | — | super-admin list |
| 2 | `POST /platform/signups/{id}/verify` | ✓ | CR / Wathiq lookup |
| 3 | `POST /platform/tenants` | ✓ | creates tenant + owner user + setup_token |
| 4 | `POST /platform/tenants/{id}/activation-email` | ✓ | dispatch |

### Side-effects
- Tenant record created with `status: 'provisioned'`
- Owner user created with `status: 'invited'` + setup token
- Audit (platform-level): `tenant.provisioned`

### Permissions
- `platform.signups.verify` (super-admin)
- `platform.tenants.create` (super-admin)

### Failure modes
- 🟠 CR lookup fails → manual override allowed (with audit reason)

### Cut-over notes
- See `SCREENS-INVENTORY-Platform.md`.
- This is one half of the new-tenant lifecycle; the other half is `F.ONBOARD.tenant` (§2), driven by the owner.

---

## 13 · `F.OPS.daily-rollup` — Daily roll-up across all systems

**Persona:** owner / area manager
**Trigger:** opens dashboard at start of day

### Screens
1. `dashboard` (Today tab)
2. `dashboard` (Live tab) — watch
3. `dashboard` (Act tab) — work down outstanding tasks
4. `dashboard` (Roll-up tab) — review last 7d / 30d / 90d
5. Drill-through: any KPI tile → source screen (e.g. low-stock → `retail.inventory`)

### API calls
| # | Call | IK | Notes |
|---|------|----|-------|
| 1 | `GET /dashboard/today?branchId=…` | — | system tiles |
| 2 | `GET /dashboard/live?branchId=…&since=…` | — | SSE / long-poll |
| 3 | `GET /dashboard/act?role=…` | — | task list |
| 4 | `GET /dashboard/rollup?range=7d` | — | series |

### Side-effects
- Read-only — no writes from dashboard itself

### Permissions
- Every role sees the dashboard; tiles are filtered by `canSee` of underlying nav id (Common §3.1)

### Failure modes
- 🔴 Aggregator endpoints **missing** today (Common §6.3) — FE composes client-side. Won't scale past 50 branches.

### Cut-over notes
- Block pilot on the 5 aggregator endpoints.
- Live tab needs SSE or WebSocket — long-polling is the fallback (Common §4.1 Quirk 1).

---

## 14 · `F.OPS.zero-stock` — Zero-friction → first PI count

**Persona:** branch manager (transitioning a branch from zero-friction stock to periodic-inventory)
**Trigger:** owner flips `stock=both` (or `stock=pi`) for a branch on `shared.integrations`

### Screens
1. `shared.integrations` — owner flips mode (this doc §15 / Common §4.6)
2. (Notification) branch manager sees "PI count required" task on `dashboard` (Act tab)
3. `retail.inventory.count.start` — wizard: pick branch, pick categories, freeze sales (optional)
4. `retail.inventory.count.execute` — handheld scan-in OR manual count
5. `retail.inventory.count.review` — variance vs system
6. `retail.inventory.count.post` — adjust on hand, post variance JE

### API calls
| # | Call | IK | Notes |
|---|------|----|-------|
| 1 | `POST /inventory/count-sessions` | ✓ | start session |
| 2 | `POST /inventory/count-sessions/{id}/lines` | ✓ (per scan) | bulk-able |
| 3 | `GET /inventory/count-sessions/{id}/variance` | — | computed |
| 4 | `POST /inventory/count-sessions/{id}/post` | ✓ | adjust + JE |
| 5 | `POST /accounting/journal-entries` | ✓ | inventory adjustment JE |

### Side-effects
- On-hand levels reset to counted
- Variance JE: `inventory adjustment` line
- `audit.inventory.count` row
- Branch's `stock_mode` flips effective from this date forward

### Permissions
- `inventory.count.run` (manager)
- `inventory.count.post` (manager + owner sign-off if variance > threshold)

### Failure modes
- 🔴 Sales during count → either freeze sales (optional) OR post all sales as `count-pending` and reconcile after.
- 🟠 Bulk-line endpoint not in OpenAPI today — see `R-SWEEP-CLASSIFICATION.md` for status.

### Cut-over notes
- Zero-friction → PI is the most-asked migration in pilot tenants. This flow is **the** answer.
- The integrations gate's `stockModeFor(branchId)` reads must update on flip — `DalseenIntegrations.onChange` subscriber bus already handles this.

---

## 15 · `F.OPS.integrations-flip` — Switch books mode (day-boundary enforced)

**Persona:** owner
**Trigger:** opens `shared.integrations` and flips books mode
**Pre-conditions (HARD — server-enforced):**
- **No active shifts** anywhere in the tenant (`SELECT count(*) FROM shifts WHERE status='open' AND tenant_id=?` must be 0)
- **Today's sales count = 0** since last day-close (i.e. flip is only legal between Z-out and the next day's first sale)
- Both checks live server-side; FE shows the gate but cannot bypass

### Screens
1. `shared.integrations` (Common §4.6)
2. **Pre-flight check banner** — FE calls `GET /tenants/me/integrations/flip-readiness` on screen mount and surfaces the result:
   - ✅ "Ready — no active shifts, no sales since last close"
   - ⛔ "Cannot flip: 3 active shifts (B-RUH, B-JED, B-DMM) — close all shifts first"
   - ⛔ "Cannot flip: 47 sales today — run end-of-day close first"
3. (If ready) Confirm modal with diff preview (`books: ZF → books`)
4. (Auto) tenant-wide setting persists; subscriber bus broadcasts

### API calls
| # | Call | IK | Notes |
|---|------|----|-------|
| 1 | `GET /tenants/me/integrations/flip-readiness` | — | `{ ready: bool, openShifts: [...], todaysSales: N, lastDayClose: ISO }` |
| 2 | `PATCH /tenants/me/integrations` | ✓ | `{ acct, stock, stockMode, externalProvider? }` — server **re-checks** readiness atomically and **rejects with 409 `code: flip_blocked`** if the window has closed since step 1 |

### Side-effects
- `dalseen-integrations` event fires; every screen reading `shouldPostGL/shouldDecrementStock` re-renders
- New transactions from this point post per **new** mode
- Audit: `tenant.integrations.changed` row with full diff + `actor`, `prev_mode`, `new_mode`, `effective_at`

### Permissions
- `tenant.settings.edit` (owner only) + WebAuthn step-up (Common §5.1) — same pattern as §11

### Failure modes
- 🔴 Race — step 1 reports ready, but a sale lands before step 2 commits. Server's atomic re-check catches this and returns 409. FE re-runs step 1, shows fresh status, asks owner to retry.
- 🔴 Active shift exists — server returns 409 with `openShifts: [{branchId, terminalId, openedAt}]`. FE shows the list with a deep-link to each `retail.shift-close` so the owner can chase them.
- 🔴 Sales since last day-close > 0 — server returns 409 with `todaysSales: N`. FE deep-links to `retail.day-close`.
- 🟠 External provider selected but credentials missing — separate validation; flip allowed but outbound queue pauses with `code: provider_unconfigured`.

### Why hard-block (rationale)
Flipping books mode mid-day causes **data drift** in accounting: the two modes use different reconciliation logic (zero-friction batches sales into a single end-of-day JE; books mode posts per-sale). A flip mid-day produces a partial day with mixed posting strategies, which cannot be reconciled cleanly without manual JE surgery. Hard-blocking at the day boundary is cheaper than fixing it after the fact.

### Cut-over notes
- Persistence today is via `AcctStore.state.settings.dalseenIntegrations` (an `AcctStore.save()` call on every change) — replace with `PATCH /tenants/me/integrations`.
- The integrations gate read API (`shouldPostGL` etc.) is **already in production-final shape** — see Common §6.1.
- **New screen work:** the pre-flight banner (step 2) does not exist today — add it as a sub-component of `shared.integrations`.
- **New endpoint:** `GET /tenants/me/integrations/flip-readiness` is new in v1.1; tracked in `API-USAGE-MAP.md` cut-over backlog.

---

## 16 · How flows compose

The 15 flows above are not independent — many *contain* each other. The tree of containment:

```
F.PLATFORM.tenant-create  ─┐
                           ├─ produces → setup_token →
F.AUTH.signin (with token) ┘                    │
                                                ▼
                                          F.ONBOARD.tenant  (8 steps)
                                          │  step 7: invite admins ─┐
                                          │                          │
                                          │  step 8: integrations ───┼→ F.OPS.integrations-flip
                                          ▼                          │
                                     dashboard                       │
                                          │                          │
              ┌───────────────────────────┼──────────────────────────┘
              ▼                           ▼
    F.RETAIL.shift-day              F.HR.hire-to-payroll
        │                                │
        │  any sale ≥ threshold ─→ F.RETAIL.refund-highvalue
        │                                │
        │                                └──→ F.HR.timeoff (per employee, recurring)
        │
        ├──→ F.DINE.ticket-life (if dine vertical)
        │        ▲
        │        └── F.DINE.aggregator-order (webhook → inbox → joins ticket-life at "fired")
        │
        ▼
    F.ACCT.invoice-to-paid (parallel — non-POS sales)
        │
        ▼
    F.ACCT.month-close (end of month, owner sign-off)
        │
        ├──→ F.ACCT.vat-return (every quarter; depends on §8 month-close per month)
        │
        ▼
    F.OPS.zero-stock (when branch transitions PI)
        │
        ▼
    F.OPS.daily-rollup (every morning)
        │
        ▼
    F.RBAC.role-change (ad-hoc)
```

**The single shared atom across every retail-side flow** is `BiometricPrompt`'s step-up. Today the FE doesn't carry the resulting token through — that's the **single largest cut-over gap** for security-sensitive flows. Wire it once and 5 flows above (3, 4, 8, 9, 11) become production-ready.

**The single shared gate across every transactional flow** is `DalseenIntegrations`. When `acct=off`, flows 3/4/5/6/7/9/14 skip their `POST /accounting/journal-entries` step but still complete. When `stock=off`, they skip `POST /inventory/adjustments`. The gate's read API needs no cut-over — only the screen that flips it (§15), which now hard-blocks until day-boundary.

---

## 18 · v1.1 changelog (this revision)

Four decisions folded into v1.0 of this doc:

| # | Decision | Where applied |
|---|----------|---------------|
| 1 | **Aggregator orders are backend-webhook ingest, not FE wizard.** Confirmed against `routes/api.php` (`ListAggregatorInboxController`, `AcceptAggregatorOrderController`, `RejectAggregatorOrderController`). | Added §5b `F.DINE.aggregator-order`. §5 (`F.DINE.ticket-life`) keeps its shape; aggregator orders join at "fired" stage. |
| 2 | **VAT return is hybrid** — FE wizard owns period select / review / declarant / confirm; backend computes the 7 boxes + submits to ZATCA. Confirmed against `POST /accounting/vat/file` body `{ period, confirm, declarant_id }`. | Added §8b `F.ACCT.vat-return`. |
| 3 | **Single-tenant per user; multi-tenant pick is dead code.** | §1 `F.AUTH.signin` — multi-tenant stage marked `DEPRECATED` with cleanup ticket `CLEANUP-AUTH-001`. §1 cut-over notes updated. |
| 4 | **Day-boundary on integrations flip is a HARD requirement, server-enforced.** Two pre-conditions (no active shifts, no sales since last day-close); FE pre-checks via `GET /tenants/me/integrations/flip-readiness`; server atomically re-checks at PATCH time. | §15 `F.OPS.integrations-flip` rewritten end-to-end with new endpoint, new pre-flight banner screen, race-condition handling, and rationale. |

### Backlog tickets opened in v1.1

| Ticket | Owner | Description |
|--------|-------|-------------|
| `CLEANUP-AUTH-001` | FE | Strip multi-tenant pick stage from `LoginScreen` (component branch + `requires_tenant_pick` flag handling) |
| `DINE-AGG-001` | FE | New screens: `dine.aggregator.inbox`, `dine.aggregator.detail`. Polling at 10s for pilot; SSE upgrade later |
| `ACCT-VAT-001` | FE | New screens: `accounting.vat.{list,compose,detail}`. Wizard scope: select → review (read-only) → declarant + step-up → confirm |
| `OPS-FLIP-001` | BE | New endpoint: `GET /tenants/me/integrations/flip-readiness`. Atomic re-check inside `PATCH /tenants/me/integrations` returning 409 `code: flip_blocked` |
| `OPS-FLIP-002` | FE | New pre-flight banner inside `shared.integrations`; deep-links to `retail.shift-close` and `retail.day-close` |

The catalogue table (top of doc) and §16 composition tree have been updated to reflect flows 16 and 17.

---

## 19 · References

- **Per-screen detail:** `SCREENS-INVENTORY-Common.md`, `SCREENS-INVENTORY-Retail.md`, `SCREENS-INVENTORY-Pay.md`, `SCREENS-INVENTORY-Dine.md`, `SCREENS-INVENTORY-Accounting.md`, `SCREENS-INVENTORY-Owner+HR.md`, `SCREENS-INVENTORY-Platform.md`
- **Per-wizard detail:** `FLOW-INVENTORY.md` (16 modal flows under `front/accounting/`)
- **Endpoint catalogue:** `API-USAGE-MAP.md`
- **Boot order, event bus, integrations gate:** `INTEGRATION-NOTES.md`
- **File geography:** `MODULE-MAP.md`
- **Step-by-step endpoint mapping for the 3 most-walked flows:**
  - `docs/handoff/onboarding-chain-endpoint-mapping.md` (§2)
  - `docs/handoff/POS-flow-endpoint-mapping.md` (§3)
  - `docs/handoff/shifts-day-close-endpoint-mapping.md` (§3)
- **Routes:** `docs/handoff/routes-api.php` (canonical excerpt) · `docs/openapi/openapi.yaml` (schema)

---

**End of USER-FLOWS.** With this in place the handoff package is complete:

| Doc | What it gives you |
|-----|-------------------|
| `MODULE-MAP.md` | File / module geography |
| `DESIGN-SYSTEM.md` | Visual tokens |
| `API-USAGE-MAP.md` | Endpoint catalogue |
| `INTEGRATION-NOTES.md` | Boot order, event bus, cut-over plan |
| `SCREENS-INVENTORY-*.md` (×7) | Per-screen specs across all modules |
| `FLOW-INVENTORY.md` + Addendum | Per-wizard transactional flows |
| **`USER-FLOWS.md` (this doc)** | **Cross-screen journeys — how it all chains together** |
