# Screens Inventory — Pay Module

> **Verified accurate:** 2026-05-02 — Stage 4 backend pre-verification still holds, screen-card schema and per-screen entries accurate. **One drift:** `front/pay/` is now 3 files (not 2) — added `pay-screens-batch7.js` for the Batch 7 screen additions; net-new screens, original 6 top-level screens still owned by `pay.compiled.js` + `pay-modules.compiled.js` as documented.
> **Status:** active source-of-truth doc; the canonical Pay screen catalog.

**Audience:** integration engineers wiring the Pay surface to the real backend (`pay.*` namespace + `msp.*` rails).
**Scope:** every consumer-visible screen, modal, and drawer under `front/pay/`. 2 source files, 6 top-level screens, ~8 modals/popovers/inline editors.
**Companion docs:** `MODULE-MAP.md`, `DESIGN-SYSTEM.md`, `API-USAGE-MAP.md`, `INTEGRATION-NOTES.md`, `SCREENS-INVENTORY-Retail.md` (POS sale capture happens in Retail and consumes Pay terminals).

> **Backend pre-verification note** (per integration-team direction at start of this module): Pay routes have been confirmed to exist in `routes/api.php` per the Stage 4 backend tag (`ready-for-handoff-v7-stage4`). Specifically: `/pay/transactions`, `/pay/terminals/*` (activate/deactivate/activation-token), `/pay/disputes` (list + transition), `/pay/settlement-exceptions`, the four `/pay/msp/*` config endpoints (contactless-cvm, decline-reasons, pricing), `/msp/softpos/workflow`, and the four `/reports/pay/*` analytics endpoints (mdr-by-scheme, acquirer-mix, chargeback-rate, auth-decline-rate). I have **not** assumed FE-only for any of these; every endpoint reference below is treated as already-existing in the backend unless explicitly flagged otherwise.

---

## How to read this doc

Same shape as the Retail inventory:

> **<Route key> · <Screen name>** — one-line purpose
> **File(s):** source files
> **Mounts at:** route key in `DALSEEN_NAV`
> **Owns / Composes / States / Actions / Data / Permissions / Modals / Quirks**

When the FE today reads from a static demo array (`TERMINALS = […]`, `DISPUTES = […]`, `METHODS = […]`, `PAYOUTS = […]` — all four exist as inline data inside `pay-modules.compiled.js`), I list **both** the demo source and the canonical `window.API.pay.<resource>.list` it will resolve to after cut-over.

---

## 1 · Module overview

### 1.1 File census (2 files)

| File                                  | Lines | Owns                                                      |
|---------------------------------------|------:|-----------------------------------------------------------|
| `pay.compiled.js`                     |   313 | `PaySoftPOS` (dashboard), `PaySettlements` (table)        |
| `pay-modules.compiled.js`             |   345 | `PayTerminals`, `PayDisputes`, `PayMethods`, `PayPayouts` (IIFE-scoped, all globals via `window.PayX = PayX`) |

Compared to Retail (50 files), Pay is intentionally compact — the screens are mostly tabular reports + one config screen. The complexity sits **behind** the boundary in the real backend (acquirer protocols, SAMA filings, PCI scope), not in the FE.

### 1.2 Top-level routes

The Pay module contributes 5 route keys to `DALSEEN_NAV` (sourced from `front/common/tokens.js`):

| Route key            | Sidebar label                  | Component             | File                       |
|----------------------|--------------------------------|-----------------------|----------------------------|
| `pay.softpos`        | SoftPOS · Tap to Phone (badge:Live) | `PaySoftPOS`     | `pay.compiled.js`          |
| `pay.terminals`      | Terminals                      | `PayTerminals`        | `pay-modules.compiled.js`  |
| `pay.settlements`    | Settlements                    | `PaySettlements`      | `pay.compiled.js`          |
| `pay.disputes`       | Disputes & Chargebacks         | `PayDisputes`         | `pay-modules.compiled.js`  |
| `pay.methods`        | Methods · mada · Card          | `PayMethods`          | `pay-modules.compiled.js`  |
| `pay.payouts`        | Payouts                        | `PayPayouts`          | `pay-modules.compiled.js`  |

(6 routes total — `pay.softpos` is the marketing/landing, the other 5 are operational.)

### 1.3 Permissions matrix (from `front/common/roles.js`)

| Route             | owner | manager | accountant | auditor (read-only) | cashier |
|-------------------|:-----:|:-------:|:----------:|:-------------------:|:-------:|
| `pay.softpos`     | ✓     | ✓       | —          | —                   | ✓       |
| `pay.terminals`   | ✓     | ✓       | —          | —                   | —       |
| `pay.settlements` | ✓     | ✓       | ✓          | ✓ (read)            | —       |
| `pay.disputes`    | ✓     | ✓       | ✓          | —                   | —       |
| `pay.methods`     | ✓     | ✓       | —          | —                   | —       |
| `pay.payouts`     | ✓     | —       | ✓          | —                   | —       |

> Cashier sees only `pay.softpos` (so they can transact); accountant sees money trail (settlements + disputes + payouts) but not the device fleet or method config; auditor sees settlements read-only. Match this in the backend's permission grants — the FE only mediates affordance.

### 1.4 Atom library (defined inline in `pay-modules.compiled.js`)

Five reusable primitives. **Do not fork; reuse across any new Pay screens.**

- **`<Page>`** — page wrapper with consistent padding/gap.
- **`<Header>`** — proxy to `window.SectionHeader`, always passes `system="pay"` (drives header tint to `T.pay`).
- **`<StatCard>`** — KPI tile; tones: `ok` / `warn` / `danger` / `info` / `ink` (default).
- **`<Pill>`** — status pill; same tones as StatCard plus `neutral`.
- **`<Btn>`** — button kinds: `primary` / `ghost` / `danger`; sizes: default / `small`.
- **`<TableShell>`** — column-driven table with empty state. Cols spec: `{label, key, w, align, muted, bold, render}`.

### 1.5 Cross-links and integration arrows

```
   ┌────────────────┐ POS sale captures via terminal
   │ Retail · POS   │─────────────────────────────────►  pay.terminals  (online)
   └────────────────┘                                          │
                                                              ▼
                                                       captures aggregated daily
                                                              │
                                                              ▼
                                                       pay.settlements ◄──── pay.disputes
                                                              │                  (Action: Settlement ↗)
                                                              ▼
                                                       pay.payouts (Action: View settlements →)
                                                              │
                                                              ▼
                                                       Accounting · Bank txns (server-side reconciliation)
```

The FE cross-links wire deep-link nav: `PayDisputes` row Action button calls `onNav('pay.settlements')`; `PayPayouts` header Action calls `onNav('pay.settlements')` too. **Preserve `onNav` prop contract** — it's how the host hub routes between Pay screens.

---

## 2 · `pay.softpos` · SoftPOS dashboard

### 2.1 PaySoftPOS — Tap-on-Phone marketing + live KPIs

**File:** `pay.compiled.js`
**Mounts at:** `pay.softpos`
**Purpose:** the Pay module's "home" — dual-purpose as a marketing/onboarding card for SoftPOS *and* an at-a-glance KPI tile for active SoftPOS volume.

**Layout:**

```
┌─ <SectionHeader> · "Tap on Phone" · "SAMA certified · PCI DSS 4.0" · CTA: "Try a transaction" ─┐
├─────────────────────────────────────────────────────────────────────────────────────────────┤
│  KPI strip (4 tiles): Volume today · Txns · Avg · Approval %                                 │
├──────────────────────────────────────────────┬──────────────────────────────────────────────┤
│  How it works (3 steps)                       │  "Last txn" card (inverted/dark, AI panel)   │
│  1. Enter amount                              │  428 SAR · mada · •• 4471 · AUTH 847293     │
│  2. Customer taps card on phone back          │  14:32 · Olaya · APPROVED                    │
│  3. Instant approval · print or WhatsApp      │  (radial gradient accent)                    │
└──────────────────────────────────────────────┴──────────────────────────────────────────────┘
```

**Owns:** `PaySoftPOS` (the entire screen — single component, no children defined here)
**Composes:** `<SectionHeader>`, `<Icon name="phone">`, `window.fmtSAR()`

**States:**
- **Default (no live data)** — fixture KPIs (24,320 SAR / 892 / 148 / 99.6%) and fixture last-txn (428 SAR mada).
- **Live (post cut-over)** — KPIs from `GET /reports/pay/auth-decline-rate?range=today` + `/pay/transactions?limit=1&order=desc` for last txn.

**Actions:**
- **"Try a transaction"** CTA → triggers `onOpenPay` prop. **The host shell decides what this opens** — today it overlays a SoftPOS demo modal (the SoftPOS workflow simulator). Cut-over: keep the same prop; the shell can call `POST /msp/softpos/workflow/start` to launch a real test transaction with the merchant's tenant credentials.

**Data (today → cut-over):**
- Today: hardcoded KPI strip + hardcoded last-txn card.
- Cut-over (suggested):
  - Volume / txns / avg / approval %: `GET /reports/pay/auth-decline-rate?range=today` + `GET /pay/transactions/aggregate?range=today` (or whichever existing analytics endpoint provides these — confirm before wiring; today's KPIs are 4 numbers, an aggregate route would be cleaner than 4 separate calls).
  - Last txn: `GET /pay/transactions?limit=1&order=desc&status=approved`.

**Permissions:** `pay.softpos.view` (implicit — see §1.3 matrix). `pay.softpos.transact` if the "Try a transaction" CTA actually starts a real charge in production (do **not** allow this for cashier role unless behind a sandbox flag).

**Quirks:**
- The **last-txn card is intentionally inverted** — uses `T.aiBg`/`T.aiInk`/`T.pay` accent gradient. This matches the AI-panel inversion documented in `DESIGN-SYSTEM.md §B.1`. Don't "fix" the contrast; it's brand.
- Header uses `<SectionHeader system="pay">` which colours the title accent line in `T.pay` (deep blue). All Pay screens use this same accent — preserves visual identity across the section.

---

## 3 · `pay.terminals` · Terminal fleet management

### 3.1 PayTerminals — fleet table + provisioning

**File:** `pay-modules.compiled.js` (lines 53–126)
**Mounts at:** `pay.terminals`
**Purpose:** the device fleet — every physical PAX/Newland/Ingenico terminal and every SoftPOS-capable phone, with status, battery, last-txn, version, and per-row actions.

**Layout:**

```
Header: "Terminals" · "8 devices · SAMA-certified fleet" · CTA: "+ Request terminal"
KPI strip (4): Online · Offline · Today volume · Uptime · 30d
Tab strip (5): All · Online · Idle · Offline · New (counts after each tab name)
Table: Device (model + SN) | Branch | Status | Battery | SIM | Last txn | Action
```

**Owns:** `PayTerminals`
**Composes:** `<Page>`, `<Header>`, `<StatCard>`, `<Pill>`, `<Btn>`, `<TableShell>`

**States:**
- **All** — full fleet.
- **Online** — `status === 'online'` (active and reachable).
- **Idle** — `status === 'idle'` (online but no recent txn — likely a wired terminal at a closed counter).
- **Offline** — `status === 'offline'` — needs ops attention.
- **New** — `status === 'unprovisioned'` — shipped but not yet activated.

**Battery indicator colour:** `< 25%` → danger red · `< 50%` → warn amber · `>= 50%` → ink (default).

**Actions (per row):**
- **Manage** (any status except `unprovisioned`) — opens device drawer (today: `alert()`; production: terminal detail screen with `GET /pay/terminals/:id` — last 50 txns, version, SIM signal, deactivate option).
- **Provision** (`unprovisioned` only) — kicks off activation flow. Production: `POST /pay/terminals/:id/activation-token` returns a 6-digit code the field tech enters into the terminal; terminal then calls `POST /pay/terminals/activate` with that token plus its serial. After activation, `status` flips to `online`.

**Action bar:**
- **+ Request terminal** — today: `alert()`; production: opens a request modal that creates a procurement ticket (likely `POST /platform/tickets` with category=terminal — confirm against backend ticket schema). Modal captures: model, branch destination, SIM type (STC / Mobily / Zain / wired), shipping address.

**Data (today → cut-over):**
- Today: `TERMINALS` constant array (8 fixtures: 5 PAX A920 Pro, 1 Newland N910, 1 Ingenico iPP350, 2 SoftPOS phones).
- Cut-over: `GET /pay/terminals?status=<filter>` returning `{ data: Terminal[], meta: {…} }`.

**Permissions:**
- `pay.terminals.view` to render screen
- `pay.terminals.request` for the CTA
- `pay.terminals.provision` for the per-row Provision button
- `pay.terminals.deactivate` for the deactivate action inside the manage drawer
- `pay.terminals.openDetail` for any row click drilling into telemetry (often everyone with .view; backend gates the audit endpoints separately)

**Quirks:**
- `battery` is `null` for wired terminals (e.g. Ingenico iPP350) — render as `—`, **not** "0%". Today this is checked with `r.battery == null`.
- SoftPOS rows show `model: "SoftPOS · iPhone"` / `"SoftPOS · Galaxy"` and `sim: "—"`. Same row treatment as physical terminals — they're terminals from a fleet-management point of view; the difference (host phone vs purpose-built device) is communicated by model name only.
- The status pill uses dot-prefix glyphs (`● Online`) which are **part of the label string**, not separate icons — so RTL mirrors them correctly. Don't extract them into icons.

**Modals & drawers (proposed for production):**

#### 3.1.1 Terminal Detail drawer
**Triggered by:** Manage button (any non-unprovisioned row)
**Body tabs:** Telemetry (last 50 txns, signal, battery history) · Settings (CVM limits, decline reasons, scheme support) · Updates (firmware version + push update CTA) · Audit
**Actions:** Push update · Reboot · Deactivate
**Data:** `GET /pay/terminals/:id`, `GET /pay/terminals/:id/transactions?limit=50`, `POST /pay/terminals/:id/firmware-update`, `POST /pay/terminals/:id/reboot`, `POST /pay/terminals/:id/deactivate`. The first one is confirmed; the action endpoints need backend confirmation — **flag in INTEGRATION-NOTES §C2**.

#### 3.1.2 Provisioning wizard
**Triggered by:** Provision button (`unprovisioned` row)
**Steps:** (1) confirm device + branch, (2) generate 6-digit activation code, (3) wait for terminal callback (polled or pushed), (4) success.
**Data:** `POST /pay/terminals/:id/activation-token` (already exists in backend) → `GET /pay/terminals/:id` until `status === 'online'` (poll every 3s, max 60s).

---

## 4 · `pay.settlements` · Settlement batches

### 4.1 PaySettlements — daily settlement table

**File:** `pay.compiled.js`
**Mounts at:** `pay.settlements`
**Purpose:** the daily roll-up of card transactions into settlement batches, broken out by method, with gross/fees/net and status.

**Layout:**

```
Header: "Settlements" · "Daily settlement · Al-Rajhi Bank"
Table (no KPI strip on this screen — minimal by design):
  Date | Method | Gross | Fees | Net | Txns | Status
```

**Owns:** `PaySettlements`
**Composes:** `<SectionHeader>`, `window.fmtSAR()`

**States:**
- **`paid`** — settlement complete, money in bank account (green dot · "Paid")
- **`pending`** — in flight (amber dot · "Pending")

**Actions:** today none — this is a read-only display. **Production needs row-level actions** (see Quirks).

**Data (today → cut-over):**
- Today: `D.settlements` array on `window.DALSEEN_DATA` (set up by the seed layer; rows shaped `{ date, method, gross, fees, net, txns, status }`).
- Cut-over: `GET /pay/transactions/settlements?from=&to=&groupBy=method,date` (confirm exact route — likely under `/pay/settlements` per the registered `pay.settlements` resource; check in `routes/api.php`).

**Permissions:** `pay.settlements.view` (owner / manager / accountant / auditor read-only).

**Quirks:**
- This is the **most-deep-linked screen in the Pay module** — `PayDisputes` rows have a "Settlement ↗" button per row, and `PayPayouts` header has a "View settlements →" CTA. Both pass the **settlement ID** through the host's nav system to filter this table. Today the FE doesn't honour the filter (just navigates); cut-over **MUST** support `?settlementId=STL-2026-04-22` query param to focus the row + scroll to it.
- Production additions needed (flag in INTEGRATION-NOTES):
  - **Date range picker** (today, this week, this month, custom) — header action area
  - **Export** (CSV / PDF) — for accountants reconciling against bank statements
  - **Drill-down on row click** — opens a settlement detail drawer with all txns in the batch, fee breakdown by scheme, exception list (chargebacks / refunds netted out)
  - **Settlement Exceptions tab or filter** — `/pay/settlement-exceptions` exists in backend; surface it (e.g. an "Exceptions" tab next to "All settlements" or a chip filter "Show exceptions only")

#### 4.1.1 Settlement Detail drawer (proposed for production)
**Triggered by:** row click
**Body tabs:** Transactions (all txns in this batch) · Fees breakdown (per scheme) · Exceptions (chargebacks / refunds offsetting this batch) · Bank ref
**Actions:** Export CSV · Match to bank txn (links to Accounting · Bank txns)
**Data:** `GET /pay/settlements/:id`, `GET /pay/transactions?settlementId=:id`, `GET /pay/settlement-exceptions?settlementId=:id`.

---

## 5 · `pay.disputes` · Disputes & Chargebacks

### 5.1 PayDisputes — chargeback workbench

**File:** `pay-modules.compiled.js` (lines 134–198)
**Mounts at:** `pay.disputes`
**Purpose:** the dispute lifecycle — chargebacks, retrievals, fraud — with deadline tracking, scheme breakdown, and per-row evidence-submission action.

**Layout:**

```
Header: "Disputes & chargebacks" · "Manage mada · Visa · Mastercard disputes"
KPI strip (4): Open disputes · Due ≤ 7 days · Win rate · 90d · Fraud loss · 90d
Tab strip (2): Open · Closed (counts after each)
Table: ID | Type | Customer | Reason | Scheme | Amount | Deadline | Status | Action
```

**Owns:** `PayDisputes`
**Composes:** `<Page>`, `<Header>`, `<StatCard>`, `<Pill>`, `<Btn>`, `<TableShell>`

**Tabs:**
- **Open** — `status ∈ {evidence_due, investigating}`
- **Closed** — `status ∈ {won, lost}`

**Status pills:**
- `evidence_due` → warn ("Evidence due")
- `investigating` → info ("Investigating")
- `won` → ok ("Won")
- `lost` → danger ("Lost")

**Type pills (column 2):**
- `chargeback` → neutral ("Chargeback")
- `retrieval` → info ("Retrieval")
- `fraud` → danger ("Fraud")

**Deadline column** — coloured by urgency: `≤ 2 days` red, `≤ 7 days` amber, `> 7 days` muted. Bold weight when ≤ 7.

**Actions (per row):**
- **Settlement ↗** (always shown) — calls `onNav('pay.settlements')` to deep-link to the settlement batch this dispute came from. Cut-over: pass `settlementId=r.settlementId` through the nav.
- **Respond** (only when `status ∈ {evidence_due, investigating}`) — today: `alert()`. Production: opens evidence-submission drawer.

**Data (today → cut-over):**
- Today: `DISPUTES` constant (6 fixtures across mada / Visa / Mastercard, mix of statuses).
- Cut-over: `GET /pay/disputes?status=open|closed` → already exists.

**Permissions:**
- `pay.disputes.view` to render
- `pay.disputes.respond` for the Respond CTA
- `pay.disputes.refund` if the response includes a refund (most disputes are won by refunding to avoid the fee penalty — but that's still a refund mutation against the original sale, so this is also `pos.refund` territory)

**Quirks:**
- Win rate is computed client-side from the closed list. Cut-over: server should provide this in the dashboard aggregate (`GET /reports/pay/chargeback-rate` should include win rate + count).
- The "Fraud loss · 90d" KPI is a **fixed fixture** (11,240 SAR / 0.06%) — needs a real source post cut-over (`/reports/pay/chargeback-rate` should include this segment, or add a dedicated `/reports/pay/fraud-loss?range=90d`).
- A dispute's relationship to its underlying sale is **not currently rendered** — the row only shows customer name and amount. Production should add a "Sale" column (or include sale ID in the detail drawer) so reps can pull up the original receipt + tender info instantly when assembling evidence.

#### 5.1.1 Dispute Evidence drawer (proposed for production)
**Triggered by:** Respond button
**Body sections:**
- **Original sale** — receipt details, tender, customer, line items
- **Evidence checklist** — per scheme rules (Visa / Mastercard / mada all have different required evidence). UI: checkboxes per item with file-upload slots
- **Free-text rebuttal** (max 1000 chars per scheme rules)
- **Submit / Save draft / Concede** actions
**Data:** `GET /pay/disputes/:id` (with full evidence history), `POST /pay/disputes/:id/evidence` (with idempotency key), `POST /pay/disputes/:id/concede`. **Backend confirmation needed** for the evidence-attachment endpoints — flag.
**File upload:** evidence is mostly receipts, shipping confirmations, customer correspondence. The FE has no file upload today; this is a Pay-module-specific gap to spec.

#### 5.1.2 Settlement deep-link
Already covered — `onNav('pay.settlements')` with settlement ID query param.

---

## 6 · `pay.methods` · Payment methods configuration

### 6.1 PayMethods — method config + per-method volume

**File:** `pay-modules.compiled.js` (lines 204–268)
**Mounts at:** `pay.methods`
**Purpose:** which payment methods are enabled, what their fee is, what their settlement schedule is, and how much volume each is taking. Used by owners to enable/disable rails (e.g. enable Tamara BNPL).

**Layout:**

```
Header: "Payment methods" · "Configure mada · cards · wallets · BNPL"
KPI strip (4): Enabled (count) · Today volume · Blended fee · mada %
Method list (one row per method):
  [colored brand chip] | name + type pill | share bar | fee % | volume | settlement schedule | enable toggle
```

**Owns:** `PayMethods`
**Composes:** `<Page>`, `<Header>`, `<StatCard>`, `<Pill>`

**Methods today (9):**
| Method        | Brand    | Type   | Fee   | Schedule | Badge | Default state |
|---------------|----------|--------|-------|----------|-------|---------------|
| mada          | mada     | card   | 0.6%  | T+1      | SAMA  | enabled       |
| Visa          | Visa     | card   | 1.95% | T+2      | —     | enabled       |
| Mastercard    | Mastercard | card | 1.95% | T+2      | —     | enabled       |
| Amex          | Amex     | card   | 2.85% | T+3      | —     | disabled      |
| Apple Pay     | Apple Pay| wallet | 0.6%  | T+1      | —     | enabled       |
| STC Pay       | STC Pay  | wallet | 0.5%  | T+1      | —     | enabled       |
| Tabby         | Tabby    | bnpl   | 5.5%  | T+1      | BNPL  | enabled       |
| Tamara        | Tamara   | bnpl   | 5.5%  | T+1      | BNPL  | disabled      |
| Cash          | Cash     | cash   | 0%    | EOD      | —     | enabled       |

**Type pills:** card / wallet / BNPL / cash (colour from `T` palette + brand colour for the chip).

**Actions:**
- **Toggle switch** per method — today: local React state via `useState`. Production: `PATCH /pay/methods/:id { enabled: true|false }` with idempotency key. Optimistic UI is acceptable here (toggle flips immediately, revert if 422).

**Data (today → cut-over):**
- Today: `METHODS` constant (9 methods); `useState(METHODS)` makes toggles local-only (resets on screen revisit).
- Cut-over: `GET /pay/methods` returns the same shape; `PATCH /pay/methods/:id` flips enabled.

**Permissions:**
- `pay.methods.view` to render
- `pay.methods.toggle` for the switch — owner only by default; restricting this is a SAMA compliance concern (you don't want a manager unilaterally disabling mada)

**Quirks:**
- The "Cash" entry is included as a method — although it's not card-acquired, it's a tender type the merchant might want to disable (e.g. cashless-only branches). Keep it.
- BNPL methods (Tabby/Tamara) have noticeably higher fees (5.5%) — owners often disable them after looking at this screen. The fee column is the killer chart for that decision.
- The blended-fee KPI is computed live from enabled methods and weighted by volume. After cut-over this should still be FE-computed (server returns volume + fee per method, FE blends) so toggling a method updates the blended fee in real time without a server round-trip.

**Production additions needed (flag in INTEGRATION-NOTES):**
- **Fee override per branch** — chains often negotiate different MDR per region. Today there's one fee per method globally; production needs per-branch override. UI: clicking a method opens a config drawer with default + branch overrides.
- **Acquirer routing** — when multiple acquirers are available (e.g. 2 routes for Visa), the merchant chooses which to route to. This is hidden today (one route per scheme); production needs a "Routing" section in the method drawer surfacing `/pay/msp/pricing` data.
- **CVM limits** — the contactless verification limit (e.g. PIN required above 300 SAR) is method+scheme-specific. Surface from `/pay/msp/contactless-cvm`.
- **Decline-reason library** — for cashier training; surface from `/pay/msp/decline-reasons`.

---

## 7 · `pay.payouts` · Bank payouts

### 7.1 PayPayouts — payout schedule + history

**File:** `pay-modules.compiled.js` (lines 274–342)
**Mounts at:** `pay.payouts`
**Purpose:** the daily bank deposit ledger — what's coming next, what's in transit, what's already deposited, and the bank reconciliation reference.

**Layout:**

```
Header: "Payouts" · "Daily bank deposits · T+1 settlement" · CTA: "View settlements →"
KPI strip (4): Next payout · Month-to-date · Fees · MTD · Bank
Section 1 (Upcoming): Table of scheduled + in_transit
Section 2 (History): Table of paid (with bank reference)
```

**Owns:** `PayPayouts`
**Composes:** `<Page>`, `<Header>`, `<StatCard>`, `<Pill>`, `<Btn>`, `<TableShell>`

**Status pills:**
- `paid` → ok ("Paid")
- `in_transit` → info ("In transit")
- `scheduled` → warn ("Scheduled")

**Two tables, same column shape (slight variation):**
| Section  | Columns                                                                |
|----------|------------------------------------------------------------------------|
| Upcoming | ID · Date · Bank (name + IBAN) · Gross · Fees · Net · Status           |
| History  | ID · Date · Bank (name + IBAN) · **Bank ref** · Gross · Fees · Net · Status |

The **Bank ref** column (only on History) is the bank's transaction ID — used by accountants to reconcile against bank statements.

**Actions:**
- **Header: "View settlements →"** — calls `onNav('pay.settlements')`.

**Data (today → cut-over):**
- Today: `PAYOUTS` constant (5 fixtures across Al-Rajhi and SNB).
- Cut-over: `GET /pay/payouts?from=&to=` (confirm route — `pay.payouts` is a registered resource).

**Permissions:**
- `pay.payouts.view` (owner / accountant)
- `pay.payouts.export` for CSV download (production add)

**Quirks:**
- IBANs are masked for display — full IBAN is shown today (8000 0000 6080 1016 7519); production likely wants `SA03 ••• 7519` for security. Owner-bank-detail screen would still show full IBAN. **This is a small but real PCI/data-handling fix to make.**
- The KPI "Bank · Al-Rajhi" is a fixture single-bank assumption; production must support multiple bank accounts per tenant — KPI becomes "Primary bank" or a small list.
- Today there's no "Initiate manual payout" CTA — payouts are auto-scheduled. If product wants a manual-payout flow (e.g. early settlement), that's a new endpoint + modal.

---

## 8 · Cross-cutting: SoftPOS workflow

### 8.1 Browser SoftPOS simulation (cut-over: real device flow)

The "Try a transaction" CTA on `pay.softpos` is the **only place** in the Pay module today that touches a transaction-initiation flow. Today it's a stubbed alert / overlay. In production it triggers SoftPOS, which is a multi-step contactless flow:

1. Enter amount on phone
2. Phone enters reader mode (NFC)
3. Customer taps card / phone / wearable
4. Phone reads chip → routes via acquirer → returns approval/decline
5. Receipt: print / WhatsApp / SMS

**Backend support:** `/msp/softpos/workflow` is confirmed to exist. The FE wiring (which screen actually renders the multi-step UI) is **not in `front/pay/`** — it's a host-shell concern that mounts on top of the Pay route. Likely lives in `front/common/softpos-shell.compiled.js` or similar; **needs to be located and documented in INTEGRATION-NOTES** as a separate FE deliverable.

### 8.2 Reports (analytics)

The four analytics endpoints (`/reports/pay/mdr-by-scheme`, `acquirer-mix`, `chargeback-rate`, `auth-decline-rate`) are **not currently rendered as standalone screens** in the Pay module. Instead, fragments of these reports power KPI tiles across the screens above:

- `auth-decline-rate` → `PaySoftPOS` "Approval %" tile
- `chargeback-rate` → `PayDisputes` "Win rate" + "Fraud loss" tiles
- `mdr-by-scheme` → `PayMethods` per-method fee values (which are tenant-config today, not analytics — see §6 Quirks: production should split "fee charged to merchant" config from "MDR observed by scheme" analytics)
- `acquirer-mix` → not rendered anywhere today

**Recommendation:** add a **`pay.reports`** screen (route key) that renders all four as a dashboard, accessible to owner + accountant. This is a Stage 4 deliverable per the backend tag.

### 8.3 Settlement Exceptions

`/pay/settlement-exceptions` exists in backend but is not surfaced anywhere in the FE today. Recommend adding:
- An "Exceptions" filter chip on `PaySettlements`, OR
- A dedicated `pay.exceptions` route, OR
- A red-banner alert at the top of `PaySettlements` when exceptions exist for today's batch ("3 exceptions in today's settlement — review")

This is the missing piece for accountants doing bank rec — without it, they can't see that the bank deposit doesn't match the gross by the chargeback offset.

---

## 9 · Cut-over impact summary (Pay-specific)

| Priority | Surface                          | Endpoints (status)                                                                   | FE wiring needed |
|---------:|----------------------------------|--------------------------------------------------------------------------------------|------------------|
|        1 | POS sale → terminal              | `GET /pay/terminals` (✅), terminal-side capture (out of FE scope)                  | Already wired in Retail · POS via `API.pay.terminals.list` |
|        1 | Terminal fleet (Pay · Terminals) | `GET /pay/terminals` (✅), `POST /pay/terminals/:id/activation-token` (✅)         | Replace `TERMINALS` const with `useApi(API.pay.terminals.list)` |
|        1 | Settlements list (Pay · Settlements) | `GET /pay/settlements` (✅ — registered)                                          | Replace `D.settlements` with API call |
|        1 | Disputes list (Pay · Disputes)   | `GET /pay/disputes` (✅), `POST /pay/disputes/:id/resolve` (✅)                    | Replace `DISPUTES` const; wire Respond CTA via dispute drawer |
|        1 | Methods list/toggle (Pay · Methods) | `GET /pay/methods` (✅), `PATCH /pay/methods/:id` (✅ via auto-CRUD)             | Replace `METHODS` const + local state with API |
|        1 | Payouts list (Pay · Payouts)     | `GET /pay/payouts` (✅)                                                             | Replace `PAYOUTS` const |
|        2 | SoftPOS dashboard live KPIs      | `GET /reports/pay/auth-decline-rate` + last txn query (✅ analytics endpoints)      | Wire 4-tile strip + last-txn card |
|        2 | Settlement deep-link from disputes | `?settlementId=` query param honoured by `PaySettlements`                          | Add `useEffect` filter on mount |
|        2 | Settlement detail drawer         | `GET /pay/settlements/:id` + `/transactions?settlementId=` + `/settlement-exceptions?settlementId=` | New drawer component |
|        2 | Dispute evidence drawer          | `GET /pay/disputes/:id` (✅), `POST /pay/disputes/:id/evidence` (verify backend)    | New drawer + file upload |
|        3 | Terminal detail drawer           | `GET /pay/terminals/:id` (verify), update/reboot/deactivate actions (verify)         | New drawer |
|        3 | Terminal provisioning wizard     | `POST /pay/terminals/:id/activation-token` (✅), poll `GET /pay/terminals/:id`     | New 4-step wizard |
|        3 | Settlement Exceptions surfacing  | `GET /pay/settlement-exceptions` (✅)                                               | Either tab on PaySettlements or new route `pay.exceptions` |
|        3 | Pay reports dashboard            | All four `/reports/pay/*` (✅)                                                      | New screen `pay.reports` |
|        4 | IBAN masking                     | FE-only — render as `SA03 ••• 7519` outside the bank-detail drawer                  | Display fix |
|        4 | Per-branch fee override          | `GET /pay/methods/:id/branch-overrides` (verify — likely needs new endpoint)         | New drawer section |
|        4 | Manual payout initiation         | `POST /pay/payouts` (verify — likely needs new endpoint)                             | CTA + modal |
|        4 | Method config: routing / CVM / decline-reasons | `GET /pay/msp/pricing` (✅), `/pay/msp/contactless-cvm` (✅), `/pay/msp/decline-reasons` (✅) | Method drawer needs sections |
|        4 | SoftPOS workflow simulator (real flow) | `/msp/softpos/workflow` (✅) — host shell concern                               | Locate and document SoftPOS shell |
|        4 | File upload (dispute evidence)   | New cross-cutting file-upload concern                                                | New component + signed-URL flow |

**Stage 4 backend coverage is excellent for Pay** — almost everything Day-1/Week-1 is already wired on the backend side. The FE work is concentrated in (1) replacing 4 const arrays with `useApi` calls, (2) building the 3 missing drawers (settlement detail, dispute evidence, terminal detail), (3) the proposed `pay.reports` and `pay.exceptions` additions, and (4) the cross-cutting file-upload component for dispute evidence.

---

## 10 · Pointer index (Pay)

When in doubt, read these in order:

| File                                       | Why                                                               |
|--------------------------------------------|-------------------------------------------------------------------|
| `front/pay/pay.compiled.js`                | `PaySoftPOS` (the dashboard) + `PaySettlements` (the table)       |
| `front/pay/pay-modules.compiled.js`        | The remaining 4 screens + the atom library (Page/Header/StatCard/Pill/Btn/TableShell) — **THE** reference for Pay-module patterns |
| `front/common/api-seeds.js` lines 362–367  | The 6 registered Pay resources (terminals/disputes/methods/payouts/settlements/softposDevices) |
| `front/common/tokens.js` lines 147–152     | The 6 `pay.*` route definitions                                    |
| `front/common/roles.js`                    | Pay route → role mapping (cashier sees softpos only, etc.)        |
| `routes/api.php` (backend, Stage 4)        | The actual `/pay/*` and `/msp/*` and `/reports/pay/*` definitions  |
| `front/retail/pos-pay.compiled.js`         | Where Pay terminals are **consumed** during a sale (Retail module) |

---

**End of SCREENS-INVENTORY (Pay).** Next module per the proposed order: **Accounting / Saaed Books**.
