# Saaed Books — Flow Inventory

> **Verified accurate:** 2026-05-02 — flow shape (trigger / steps / state / on-finish writes / endpoints / idempotency / permissions / gaps), 16-flow scope, and `window.AcctFlow` engine claims hold. Per-flow 🟡 verify rows have all been resolved by `FLOW-INVENTORY-Addendum.md`; read the addendum first.
> **Status:** active source-of-truth doc; the canonical Saaed Books transactional-flow catalog.

> ⚠️ **Companion doc:** [`FLOW-INVENTORY-Addendum.md`](./FLOW-INVENTORY-Addendum.md) cross-checks every `🟡 verify` row in this file against `docs/openapi/openapi.yaml`. Of 22 verify items: **6 confirmed**, **7 wrong-shape (path/verb correction needed)**, **9 not in spec (likely doc gaps; need backend confirmation)**. Read the addendum before relying on any 🟡 row in this doc.

> **Scope.** Every wizard / modal driven by the `window.AcctFlow` engine. These are the **transactional** flows in `front/accounting/` — the surfaces a user opens to *do* something (post an invoice, pay a bill, reconcile a bank, file VAT, close a month). Read‑only screens (Home / Sell / Spend / Bank / GL / Journal / etc.) are documented in `SCREENS-INVENTORY-Accounting.md`. Tab routing inside the hub is documented there too.
>
> **Audience.** Backend & integration engineers wiring real APIs into the flows. Each flow card has a fixed shape: trigger, steps, state shape, on‑finish writes (the JE side‑effects), backend endpoints, idempotency surface, permissions, and gaps to file.
>
> **Sibling docs.** `MODULE-MAP.md` (boot order · namespaces) · `DESIGN-SYSTEM.md` (tokens · components) · `API-USAGE-MAP.md` (endpoint catalogue) · `INTEGRATION-NOTES.md` (event bus · cross‑module wiring) · `SCREENS-INVENTORY-Accounting.md` (read‑only screens).

---

## §0 · Reading guide

Each flow is one section. The shape is stable so you can diff between flows:

- **Flow ID** — the string passed to `AcctFlow.open(id, ctx?)`. This is the contract.
- **File · entry point · register** — where the def lives, the `const` name, and the `Object.assign(window.AcctFlows, {...})` site that registers it.
- **Triggers** — every place in the codebase that opens this flow (command palette, FAB, screen button, `acct:gotoTab` redirect, narrator, etc.).
- **Permissions** — what `hasPermission()` gates the *open* and the *post*. Mirrors the backend authorization scopes.
- **State shape** — keys produced by `init(ctx, lang)`, the `state` object the engine threads through every step.
- **Steps** — table of `id · title · titleAr · what it asks · what it writes to state · validate?`.
- **On finish (`onFinish(state)`)** — the **side‑effects**. Three kinds: (a) `AcctStore.add*` writes to the local store and triggers `acct:store:changed`; (b) `AcctJE.*` posts the journal entry; (c) `AcctAudit.log()` audit trail. This is where the backend `POST` calls go.
- **JE preview** — exact debit/credit lines posted, with COA codes. This is what the GL bridge must produce.
- **Backend endpoints** — Stage 4 endpoints to call. `✅` = listed in OpenAPI and confirmed against the spec sweep. `🔁` = path/shape corrected from FE drift; see Addendum §1.2. `⏸️` = deferred to v1.1 (logged in Addendum §1.3). `🆕` = net-new for v1 (e.g. invoice attachments). The original `🟡 verify` queue is **closed** — every row is now resolved; consult the Addendum for the audit trail.
- **Idempotency** — does the flow produce an idempotency key? (Most do; vouchers and JE actions explicitly carry `idempotency_key`.)
- **Production gaps** — flagged for the integration plan.

The full set of flow IDs registered on `window.AcctFlows` (verified by grep across `accounting-flow-defs*.compiled.js`):

```
invoice.quick      invoice.standard   invoice.zatca      invoice.b2b
payment.receive    payment.installment   (alias of payment.receive)
bill.create        bill.pay
bank.reconcile     bank.rules         (variant of bank.reconcile)
vat.file           period.close       onboarding
receipt.voucher    payment.voucher
je.manual          je.reverse         je.adjust
```

**16 distinct flows** + 3 registered aliases (`invoice.b2b`, `payment.installment`, `bank.rules`).

---

## §1 · Engine contract

The flow modal is a single React tree (`accounting-flow-ui.compiled.js`) driven by a small global registry. **All flow logic is data, not components** — flows are plain objects on `window.AcctFlows[id]`.

### 1.1 Public API (`accounting-flow-engine.js`)

| Symbol | Signature | Notes |
|---|---|---|
| `window.AcctFlow.open(id, ctx?)` | `(string, object?) → void` | Mounts the flow modal at step 0. `ctx` is forwarded into `init(ctx, lang)`. |
| `window.AcctFlow.close()` | `() → void` | Unmounts the modal. The narrator and the URL are *not* touched — flows are self‑contained. |
| `window.AcctFlow.finish(snapshot, state)` | `({kind, def}, state) → void` | Called by the UI after `onFinish` returns. Invokes every `onFinished` subscriber. |
| `window.AcctFlow.current()` | `() → {open, kind, ctx}` | Read‑only handle for tests / debugging. |
| `window.AcctFlow.on(fn)` | `(fn) → unsubscribe` | Subscribe to open/close. Used by the modal mount. |
| `window.AcctFlow.onFinished(fn)` | `(fn) → unsubscribe` | Subscribe to post‑finish snapshots. Used by the success‑summary modal and by cross‑module bus relays (see `INTEGRATION-NOTES.md`). |
| `window.AcctFlows[id]` | `FlowDef` | The registry. Defs land here from `accounting-flow-defs*.compiled.js`. |
| `window.AcctCommands()` | `() → CommandItem[]` | The command‑palette + FAB contents. Each entry has `{id, en, ar, icon, kbd?, run}` and `run` typically calls `AcctFlow.open(...)`. |

### 1.2 Flow‑definition shape

Every value in `window.AcctFlows` follows this contract (engine inspects nothing else):

```ts
type FlowDef = {
  title:        string | (s) => string;       // EN
  titleAr:      string | (s) => string;       // AR
  icon?:        string;                        // <Icon> name from common iconography
  finishLabel?: string;                        // CTA text on the last step (EN)
  finishLabelAr?: string;                      // CTA text on the last step (AR)
  init:         (ctx, lang) => State;          // initial state for this run
  steps:        Step[];
  onFinish:     (state) => any;                // returns extra fields merged into the success summary
};

type Step = {
  id:        string;                            // 'pick' | 'method' | 'review' | …
  title:     string;                            // header (EN)
  titleAr:   string;                            // header (AR)
  hint?:     string;                            // grey one-liner under header (EN)
  hintAr?:   string;                            // grey one-liner under header (AR)
  validate?: (state) => boolean;                // gates Next button
  render:    (state, hh) => ReactNode;          // the step body
};
```

`hh` is the helper bag the engine passes to every `render`: `{ T, lang, h, Btn, Tab, Field, Input, Chip, setField, patch, ... }`. **Steps never own state** — they read `state` and call `hh.setField(key, value)` / `hh.patch({…})`. The engine threads state across steps and through to `onFinish`.

### 1.3 Lifecycle

```
AcctFlow.open(id, ctx)
       │
       ▼
  init(ctx, lang)  ──────►  state₀
       │
       ▼
  steps[0].render(state, hh)   ◄── user types ──┐
       │  Next pressed                          │
       │  steps[i].validate(state)?             │
       ▼                                        │
  steps[i+1].render(state, hh)  ───── user types ─┘
       │  …
       ▼
  steps[N-1] = last step → finish button shows finishLabel
       │  user clicks
       ▼
  result = onFinish(state)              ◄── side-effects fire HERE
       │  ⤷ AcctStore.add*(...)         (local store + acct:store:changed)
       │  ⤷ AcctJE.*(...)               (post journal entry)
       │  ⤷ AcctAudit.log(...)          (audit trail)
       │  ⤷ AcctNarrator.say(...)       (toast)
       ▼
  AcctFlow.finish({kind, def}, {…state, …result})
       │
       ▼
  finishSubs.forEach(fn → fn(snapshot, state))
       │   (this is where the cross-module event bus relays —
       │    e.g. acct:invoice:posted — see INTEGRATION-NOTES)
       ▼
  Success summary modal (accounting-flow-summary.compiled.js)
       │
       ▼
  AcctFlow.close()  + optional next-flow chain
```

### 1.4 Cut‑over consequence

**For every flow below, the cut‑over work is the same shape:**

1. Replace the `AcctStore.add*` call in `onFinish` with a `useApi.mutate(POST <endpoint>)` call.
2. Replace the `AcctJE.*` call with the response payload from the same backend write — backend posts the JE atomically with the document.
3. Keep `AcctAudit.log()` (server now records this; FE call becomes a no‑op or emit‑only).
4. Keep `AcctNarrator.say()` and the success modal — these are pure UX.
5. Add an idempotency key to the request — every flow generates one already; pass it through.

The flow steps themselves don't change. **The transactional boundary is `onFinish`.**

---

## §2 · Command palette (the entry surface)

Most flows are reached two ways: a context button on a screen, *or* the global command palette / New + FAB.

### 2.1 The base palette (`accounting-flow-engine.js`)

| ID | EN label | AR label | Icon | Shortcut | Opens |
|---|---|---|---|---|---|
| `inv.quick` | Quick invoice (no QR) | فاتورة سريعة | receipt | `⌘I` | `invoice.quick` |
| `inv.standard` | Standard invoice · Phase 1 | فاتورة قياسية · مرحلة 1 | receipt | — | `invoice.standard` |
| `inv.zatca` | ZATCA invoice · Phase 2 | فاتورة زاتكا · مرحلة 2 | shield | — | `invoice.zatca` |
| `pay.rcv` | Record customer payment | تسجيل دفعة من عميل | coin | `⌘P` | `payment.receive` |
| `bill.new` | Enter vendor bill | إدخال فاتورة مورد | ledger | `⌘B` | `bill.create` |
| `bill.pay` | Pay bill | سداد مورد | card | — | `bill.pay` |
| `rcpt.voucher` | Receipt voucher (money in) | سند قبض | coin | — | `receipt.voucher` |
| `pay.voucher` | Payment voucher (money out) | سند صرف | card | — | `payment.voucher` |
| `bank.rec` | Reconcile bank | مطابقة البنك | bank | `⌘R` | `bank.reconcile` |
| `vat.file` | File VAT return | تقديم إقرار الضريبة | shield | — | `vat.file` |
| `period.close` | Close month | إقفال الشهر | check | — | `period.close` |

### 2.2 Decorations

- **`accounting-flow-defs.compiled.js`** prepends `je.manual` (kbd `⌘J`) at the top of the list.
- **`accounting-flow-defs-vouchers.compiled.js`** *defensively re-inserts* `rcpt.voucher` and `pay.voucher` right after `bill.pay` if they were dropped by load-order shifts. (See §10 vouchers · production note about this defensive double-registration.)
- **`accounting-flow-defs-je-actions.compiled.js`** does **not** decorate the palette — `je.adjust` and `je.reverse` are reachable only from the journal-entry detail screen; they require a source JE in `ctx`.

### 2.3 The shortcut surface

```
⌘I → invoice.quick
⌘P → payment.receive
⌘B → bill.create
⌘R → bank.reconcile
⌘J → je.manual
```

`⌘K` opens the command palette itself (handled in `accounting-flow-ui.compiled.js`).

> **Cut-over note.** None of the shortcuts conflict with the platform shell's reserved keys (`⌘K` global search, `⌘/` shortcuts). When wiring real, safety-check that other modules' shells don't intercept these — Retail's POS shell binds `⌘B` for "next bill" and Retail's purchasing tab binds `⌘R` for "refresh report". A scoped binding (active only when `acct.*` route is foreground) is the safe default.

---

## §3 · Sales‑side flows

### 3.1 — `invoice.quick` · Quick invoice

| | |
|---|---|
| **File** | `accounting-flow-defs.compiled.js` (legacy `invoiceQuick`) **OVERRIDDEN BY** `invoiceB2BQuick` at the bottom of the same file (line 2447). |
| **Triggers** | Command palette `inv.quick` (`⌘I`) · New+ FAB · Sell tab "New invoice → Quick" composer entry · Home zero-day prompt for first invoice. |
| **Permissions** | open: `accounting.invoice.create` · post: `accounting.invoice.post` |

#### 3.1.1 Live behaviour

The legacy single-step `invoiceQuick` flow is **dead code** — its registration on line 596 is overwritten on line 2463 by `invoiceB2BQuick`, which is `invoiceB2B` minus the phase-selection step. So the flow that actually runs is:

| # | Step ID | Title (EN) | Title (AR) | What it asks |
|---|---|---|---|---|
| 1 | `party` | Customer | العميل | Pick or add customer; per-party defaults pre-fill terms + language. |
| 2 | `items` | Line items | بنود الفاتورة | Per-line desc + qty + unit price + VAT rate + discount. |
| 3 | `extras` | Notes & attachments | ملاحظات ومرفقات | Internal note, customer-facing memo, optional attachment ref. |
| 4 | `review` | Review | مراجعة | Bilingual invoice preview + JE preview. |

`phase` defaults to `1` and is hard-pinned (no Phase-2 clearance step). Quick = "B2B Phase 1, no clearance round-trip."

#### 3.1.2 State shape

```js
{
  phase: 1,                          // pinned in quick
  customerId: '', customerName: '',
  ref: '', dueDays: 30, lang: 'ar',
  lines: [{ desc, qty, price, vat, discount }],
  internalNote: '', customerMemo: '',
  attachments: [],
  channel: 'email',                  // email | whatsapp | sms | print
}
```

#### 3.1.3 onFinish — JE posted

Lines (per item) sum to:

```
DR  1210 Accounts receivable          subtotal + VAT
CR  4100 Sales revenue                subtotal
CR  2310 VAT output payable           subtotal × vat_rate
```

#### 3.1.4 Backend (post-OpenAPI sweep — **architectural correction**)

> ⚠️ **Backend-authoritative refactor.** AR invoice creation does **not** use a `/accounting/invoices` endpoint. The FE flow's `onFinish` must call **`POST /sales`** (`CaptureSaleController`) — the same endpoint Retail's POS uses. Sale capture auto-generates the AR invoice JE server-side. The "AR invoice" the user sees in Saaed Books is a **derived view** of the underlying `sales` resource, not a separate noun. See [`FLOW-INVENTORY-Addendum.md`](./FLOW-INVENTORY-Addendum.md) §6.
>
> Body distinguishes B2B vs B2C via `customer_type` / `invoice_type` on the sale. Lines, customer, and payment_method are all in the same payload.

| Action | Method · path | Status |
|---|---|---|
| Capture sale (= post AR invoice + JE + queue ZATCA) | `POST /sales` body `{customer_id, customer_type:'b2b', lines:[…], payment_method, idempotency_key}` | ✅ (refactor target) |
| Send via channel | `POST /sales/{id}/share` body `{channel}` | ✅ |
| Upload attachment | _Net-new for v1.0_ — no upload component in FE today | 🆕 |
| Generate receipt PDF | `GET /sales/{id}/receipt` | ✅ |
| Void sale | `POST /sales/{id}/void` | ✅ |

#### 3.1.5 Production gaps

- **Customer auto-create from CR lookup** — the `+ New customer…` option does an inline create today; backend needs `POST /accounting/customers` with the embedded MCI fetch. *(Wathq/CR lookup is **deferred to v1.1** — see Addendum §6.)*
- **Per-line VAT rate** — the `/sales` endpoint accepts per-line VAT codes (verified). FE's per-line VAT rate UI is correct; pass each line's `vat_code` to the sale capture.
- **Attachments** — file upload component does not exist in the FE; placeholder ref-input only. Flagged as a `🆕` net-new for the integration team.
- **Architectural impact** — the entire `invoice.quick` / `invoice.standard` / `invoice.b2b` family of FE flows must be refactored to consume `POST /sales`. The bilingual "AR invoice preview" the user sees in step 5 is rendered from the sale-capture response, not from a separate invoice resource. **The UX wording in the FE may stay as "invoice" — only the wire call changes.**

---

### 3.2 — `invoice.standard` · Standard B2B invoice (Phase 1)

| | |
|---|---|
| **File** | Same as 3.1 — overwritten by `invoiceB2B` (line 2461). |
| **Triggers** | Command palette `inv.standard` · New+ FAB · Sell tab "New invoice → Standard". |
| **Permissions** | open: `accounting.invoice.create` · post: `accounting.invoice.post` |

The unified `invoiceB2B` composer runs both `invoice.standard` and `invoice.zatca`; user picks Phase 1 vs Phase 2 in step 1. **Five steps** (Phase 1) / **six steps** (Phase 2 adds `clear`).

#### 3.2.1 Steps

| # | ID | Title | Notes |
|---|---|---|---|
| 1 | `phase` | ZATCA phase / مرحلة زاتكا | Phase 1 (QR + report) vs Phase 2 (real‑time clearance). |
| 2 | `party` | Customer / العميل | Customer + buyer VAT# (required by ZATCA for B2B) + ref. |
| 3 | `items` | Line items / بنود الفاتورة | Lines with per‑item qty × price + VAT rate + discount. |
| 4 | `extras` | Notes & attachments / ملاحظات ومرفقات | Internal note, customer memo, attachments. |
| 5 | `review` | Review / مراجعة | Bilingual invoice preview + JE preview. **End of Phase 1 path.** |
| 6 | `clear` | Clearance / موافقة زاتكا | **Phase 2 only.** Live ZATCA round-trip simulator: cryptographic stamp → submit → cleared/reported. |

#### 3.2.2 onFinish — same JE as `invoice.quick`. The Phase 2 path additionally writes:

- `state.zatca.uuid`
- `state.zatca.cleared_at`
- `state.zatca.qr` (Base64 TLV)
- `state.zatca.signature`

…and `AcctStore.addInvoice` stamps `phase: 2, status: 'cleared'`.

#### 3.2.3 Backend (post-OpenAPI sweep — **ZATCA Phase-2 is async, confirmed by backend**)

> ⚠️ **Backend-authoritative refactor.** ZATCA Phase-2 clearance is **not synchronous from the FE**. The actual flow:
>
> 1. **Capture sale** via `POST /sales` (same endpoint as Phase 1). Backend writes the JE *and* queues the ZATCA submission.
> 2. Backend's queue submits to ZATCA in the background; ZATCA fires a webhook back to `StoreZatcaClearanceWebhookController` (signature verified by `VerifyZatcaWebhookSignature` middleware).
> 3. FE polls **`GET /accounting/zatca/queue`** (or per-invoice **`GET /accounting/zatca/invoices/{sale_id}`**) for status: `queued` → `submitting` → `cleared` | `rejected`.
>
> **The current FE "Submit to ZATCA" button is wrong.** Replace with a status indicator. "Retry" affordance only when status = `rejected`.

| Action | Method · path | Status |
|---|---|---|
| Capture sale (Phase 1 or Phase 2) | `POST /sales` body `{customer_id, lines:[…], idempotency_key}` (no `phase` field needed — backend infers from tenant config + customer type) | ✅ |
| Poll ZATCA submission status | `GET /accounting/zatca/invoices/{sale_id}` returns `{status, uuid, cleared_at, qr, signature, error?}` | ✅ |
| Poll global queue (admin/dashboard view) | `GET /accounting/zatca/queue?status=rejected` | ✅ |
| Retry rejected submission | _no FE-callable endpoint_ — retry is backend-managed via queue worker; FE shows status only. | — |
| Send invoice via channel | `POST /sales/{id}/share` | ✅ |

#### 3.2.3.1 FE composer step changes

The `clear` step (#6 in the table above) **must be re-designed**:

- **Old behaviour:** synchronous click → spinner → cleared/rejected pill.
- **New behaviour:** step shows status pill from the polling endpoint; auto-advances to `cleared` when webhook lands; surfaces the rejection reason if backend reports `rejected`.
- The crypto stamp text on the preview is **populated from the polling response**, not generated client-side.
- If the user closes the composer before clearance lands, the invoice still posts; the status appears in the Sell tab row badge.

#### 3.2.4 Production gaps

- **Cryptographic stamp** is rendered as a text placeholder; the real signed XML + QR are returned by `GET /accounting/zatca/invoices/{sale_id}` once the queue worker has cleared the submission. FE just renders what the polling endpoint provides. **The device-certificate cross-dependency on `pay.terminals` is no longer relevant for the AR-invoice path** — ZATCA enrolment is tenant-level, not terminal-level.
- **B2C threshold detection** — backend infers B2B/B2C from `customer_type` on the sale capture; FE does not need to branch into a separate flow ID. The legacy `invoice.zatca` 3-step flow is dead code; remove the registration.
- **ZATCA enrolment (tenant-level prereq)** — v1.0 launch uses **manual enrolment by DALSEEN team** for the first 5–10 pilot tenants. The production CSID is stored in encrypted tenant config via `PATCH /accounting/settings`. Self-service enrolment is **deferred to v1.1**. See Addendum §6.
- **Idempotency** — `state.idemKey` is generated once on `init`. Confirm backend honours it on retry.

---

### 3.3 — `invoice.zatca` · ZATCA Phase 2

Identical to §3.2 — same `invoiceB2B` def, just opened with the user's intent to use Phase 2. The user can still flip back to Phase 1 in step 1; the registry alias is purely a permalink for the command palette.

> **Recommend:** In the cut-over, deprecate the alias and make `invoice.b2b` the canonical id everywhere. `invoice.zatca` and `invoice.standard` then become URL/palette aliases that pre-set `phase`. (One source of truth, no chance of split behaviour.)

---

### 3.4 — `payment.receive` · Record customer payment

| | |
|---|---|
| **File** | `accounting-flow-defs.compiled.js` — `paymentReceive` (line 482), registered line 599. |
| **Triggers** | Command palette `pay.rcv` (`⌘P`) · New+ FAB · Sell tab → invoice row → "Record payment" · Collect tab → customer row → "Record payment" · success modal of `invoice.b2b` ("Record payment now"). |
| **Permissions** | open: `accounting.payment.create` · post: `accounting.payment.post` |

#### 3.4.1 Steps

| # | ID | Title | What it asks |
|---|---|---|---|
| 1 | `pick` | Invoice / الفاتورة | Pick the open invoice; amount + customer auto-fill. |
| 2 | `method` | Method / الطريقة | Bank transfer / mada / cash / Pay-by-link / cheque. Each picks a different debit account. |
| 3 | `amount` | Amount & date / المبلغ والتاريخ | Confirm amount, value date, ref. Allows partial payment. |

#### 3.4.2 State

```js
{
  invoiceId: '', customerId: '', customerName: '',
  method: 'transfer',          // transfer | mada | cash | paylink | check
  amount: 0, fee: 0,
  bankCode: '1101',            // depends on method
  date: todayISO(), ref: '',
}
```

Method → COA mapping (from the tile defs):

| Method | Debit COA | Default fee | Notes |
|---|---|---|---|
| Bank transfer | `1101` | 0 | Same-day SAR |
| mada | `1101` | 1.85% | Card present / SoftPOS |
| Pay-by-link | `1155` | 1.50% | STC Pay / Apple Pay rail |
| Cash | `1010` | 0 | Cash on hand |
| Cheque | `1101` | 0 | Posted but **status = pending until cleared** |

#### 3.4.3 onFinish — JE posted

```
DR  <bankCode>                amount net of fee
DR  6510 Bank charges         fee   (only if method ∈ mada · paylink)
CR  1210 Accounts receivable  amount
```

Plus `AcctStore` updates the invoice's `paid` field and (if fully settled) flips its `status` to `paid`.

#### 3.4.4 Backend (post-OpenAPI sweep)

| Action | Method · path | Status |
|---|---|---|
| Create + post payment (Receive customer payment) | `POST /accounting/receipts` body `{invoice_id, amount, method, fee, bank_code, ref, idempotency_key}` (operationId `post_accounting_receipts`, tag *17.5 — Customers & AR*) | ✅ |
| List open invoices for customer | `GET /sales?status=open&customer_id=…` (per AR-side architectural correction in §3.1.4 — AR "invoices" are sales) | ✅ |

#### 3.4.5 Production gaps

- **Pay-by-link rail** — fee is hardcoded 1.5%. Real fee must come from `pay.methods` config, not the flow def. (Cross-module: pull the fee from Pay's method config at flow-open.)
- **Cheque pending state** — flow currently posts cheque payments as `status: 'posted'` immediately. Real behaviour: post as `status: 'pending', clear_date: null` and let bank-rec match it on clearance. **Cut-over note.**
- **Foreign-currency invoices** — flow assumes SAR. Multi-currency is out of scope for V1; flagged as future work.

---

### 3.5 — `payment.installment` · Installment plan (alias of `payment.receive`)

> **Status (post-OpenAPI sweep):** confirmed as **alias only** of `payment.receive` — no dedicated endpoint or distinct posting logic exists. Dedicated BNPL flow deferred to **v1.1**. See [`FLOW-INVENTORY-Addendum.md`](./FLOW-INVENTORY-Addendum.md) §3.2.

| | |
|---|---|
| **Registration** | Line 600. `{...paymentReceive, title:'Installment plan'}` — **same flow, different label.** |
| **Triggers** | Tabby/Tamara post-checkout flow (Pay module). Currently no in-FE button opens this directly; routed externally via `acct:flow:open` event from Retail when an installment lender confirms. |

**Recommendation:** Rename the registry id to `payment.bnpl` and split into a real flow once Tabby/Tamara wiring is in. Today this is just a label alias — real installment behaviour (settlement schedule, lender holdback, fee recognition over time) needs its own steps.

---

## §4 · Purchase‑side flows

### 4.1 — `bill.create` · Enter vendor bill

| | |
|---|---|
| **File** | `accounting-flow-defs.compiled.js` — `billCreate` (line 651), registered line 972. |
| **Triggers** | Command palette `bill.new` (`⌘B`) · New+ FAB · Spend tab "New bill" · 3-way-match flow on PO receipt. |
| **Permissions** | open: `accounting.bill.create` · post: `accounting.bill.post` |

#### 4.1.1 Steps

| # | ID | Title | What it asks |
|---|---|---|---|
| 1 | `vendor` | Vendor / المورد | Pick or add vendor. Default expense COA + payment terms pre-fill. |
| 2 | `detail` | Detail / التفاصيل | Bill number (vendor's), date, due date, lines (qty × price), VAT rate, attachment. |
| 3 | `review` | Review / مراجعة | JE preview: expense + VAT input → AP. |

#### 4.1.2 State

```js
{
  vendorId: '', vendorName: '',
  billNo: '',  date: todayISO(), dueDate: …,
  lines: [{ desc, qty, price, expenseCode }],
  vatRate: 0.15, vatIncluded: true,
  description: '',
}
```

#### 4.1.3 onFinish — JE posted

```
DR  <expenseCode>            subtotal
DR  1340 VAT input recoverable  subtotal × vat_rate
CR  2100 Accounts payable     gross
```

`AcctStore.addBill({...status:'open', ...})`.

#### 4.1.4 Backend (post-OpenAPI sweep — **architectural correction**)

> ⚠️ **Backend-authoritative refactor.** AP bills do **not** use a `/accounting/bills` endpoint. The endpoint is **`POST /supplier-invoices`** (`StoreSupplierInvoiceController`) at the **top level**, not under `/accounting/`. Permission key: **`purchasing.supplier_invoices.create`**. This places AP bill creation under the **Purchasing module's permission scope**, not Accounting's — a cross-module wiring detail. See [`FLOW-INVENTORY-Addendum.md`](./FLOW-INVENTORY-Addendum.md) §6.

| Action | Method · path | Status |
|---|---|---|
| Create + post bill | `POST /supplier-invoices` body `{vendor_id, lines, vat, …, idempotency_key}` | ✅ (refactor target) |
| List vendors | `GET /accounting/vendors` | ✅ |
| Vendor inline-create | `POST /accounting/vendors` *(verify body shape against `routes/api.php` controller)* | ✅ |

#### 4.1.5 Production gaps

- **PO match** — when triggered from a PO receipt, `ctx.poId` should pre-fill vendor + lines + qty against PO snapshot. Today the flow ignores `ctx`. **Cross-module Retail dependency.** With the architectural correction above (`POST /supplier-invoices` is in the Purchasing module), this PO→bill linkage is *natively* in scope of that module — the FE should call the Purchasing GET endpoint to fetch PO snapshot at flow open.
- **Permission scope correction.** The pre-correction permission key `accounting.bill.create` should be replaced with **`purchasing.supplier_invoices.create`** in any role-matrix tables.
- **Recoverable vs non-recoverable VAT** — single `vatRate` field; ZATCA distinguishes recoverable input vs blocked input (entertainment, fuel allowances). **Add a per-line `vat_class` field** — recommended for V2.
- **Multi-line different expense accounts** — UI supports it; confirm backend accepts heterogeneous expense COA per line.

---

### 4.2 — `bill.pay` · Pay vendor bill

| | |
|---|---|
| **File** | `accounting-flow-defs.compiled.js` — `billPay` (line 770), registered line 973. |
| **Triggers** | Command palette `bill.pay` · New+ FAB · Pay (AP) tab → upcoming row → "Pay" · Spend tab → bill row → "Pay this bill" · success modal of `bill.create`. |
| **Permissions** | open: `accounting.bill.pay` · post: `accounting.payment.post` (same scope as customer payment posting) |

#### 4.2.1 Steps

| # | ID | Title | What it asks |
|---|---|---|---|
| 1 | `pick` | Bill / الفاتورة | Pick open bill; amount + vendor pre-fill. |
| 2 | `how` | Method / الطريقة | Bank transfer (IBAN) / SADAD / cheque / cash. |
| 3 | `confirm` | Confirm / تأكيد | Confirm amount, date, ref. JE preview. |

#### 4.2.2 State + COA mapping

| Method | Credit COA | Notes |
|---|---|---|
| Bank transfer | `1101` | Same-day SAR; needs IBAN |
| SADAD | `1101` | Govt/utility; needs SADAD bill ref |
| Cheque | `1101` | Reduces cash on **clearance**, not on issue |
| Cash | `1010` | Cash on hand |

#### 4.2.3 onFinish — JE posted

```
DR  2100 Accounts payable    amount
CR  <bankCode>               amount
```

`AcctStore.addPayment({...status:'posted', kind:'ap'})` and `AcctStore.applyToBill(billId, amount)`.

#### 4.2.4 Backend (post-OpenAPI sweep)

| Action | Method · path | Status |
|---|---|---|
| Create + post bill payment (Pay vendor) | `POST /accounting/payments` body `{bill_id, amount, method, bank_code, ref, idempotency_key}` (operationId `post_accounting_payments`, tag *17.6 — Vendors & AP*) | ✅ |
| List open bills | `GET /supplier-invoices?status=open` (per architectural correction in §4.1.4 — bills live under Purchasing module) | ✅ |

#### 4.2.5 Production gaps

- **Batch payment** — multiple bills paid via one transfer is documented in `SCREENS-INVENTORY-Accounting.md §6` (Pay tab batch bar) but the flow def supports only single bill. **Deferred to v1.1** per integration plan; v1 ships single-bill payment only. (See [`FLOW-INVENTORY-Addendum.md`](./FLOW-INVENTORY-Addendum.md) §3.2.) ~~**Add `bill.pay.batch` flow** for V1 if WPS-style batch is on the roadmap.
- **Cheque clearance** — same gap as customer-side (see §3.4.5). Cheques should post as `pending` until bank-rec matches.

---

## §5 · Voucher flows (`accounting-flow-defs-vouchers.compiled.js`)

Standalone money-in / money-out flows that bypass invoice/bill ledgers. The classical Arabic-accounting pair: سند قبض / سند صرف.

### 5.1 — `receipt.voucher` · سند قبض

| | |
|---|---|
| **File** | `accounting-flow-defs-vouchers.compiled.js` — `receiptVoucher` (line 245), registered line 487. |
| **Triggers** | Command palette `rcpt.voucher` · New+ FAB · Bank tab "Money in not against an invoice" · Owner cash-injection flow. |
| **Permissions** | open: `accounting.voucher.create` · post: `accounting.voucher.post` |

#### 5.1.1 Steps (3)

| # | ID | Title | What it asks |
|---|---|---|---|
| 1 | `reason` | Reason / السبب | Why is money coming in? — picks the **credit** COA + party type. |
| 2 | `method` | Where it lands / وجهة المبلغ | Pick the cash/bank account (debit). |
| 3 | `confirm` | Confirm / تأكيد | Amount, date, ref, memo. JE preview + voucher # preview. |

#### 5.1.2 Reasons (the `RECEIPT_REASONS` table) → COA

| Reason ID | EN | AR | Credit COA | Party type |
|---|---|---|---|---|
| `customer_deposit` | Customer deposit | عربون عميل | `1290` Customer deposits (liab) | customer |
| `owner_contribution` | Owner contribution | مساهمة المالك | `3210` Owner contributions | owner |
| `loan_proceeds` | Loan proceeds | متحصلات قرض | `2510` Long-term loan | bank |
| `interest_income` | Interest income | إيراد فوائد | `7110` Interest income | bank |
| `refund_received` | Refund received | استرداد | `6900` (vendor expense reversal) | vendor |
| `other_income` | Other income | إيرادات أخرى | `7900` Other income | other |

#### 5.1.3 Voucher numbering

`CR-{YYYY}-{NNNN}` — per-tenant, per-prefix, per-fiscal-year. **Backend authoritative** (FE is preview only). Helper `voucherNo('CR', date)` in the file does a local count for preview.

#### 5.1.4 onFinish — JE posted

```
DR  <bankCode>           amount
CR  <creditCode>         amount     (= the reason's COA)
```

Then `AcctStore.addReceiptVoucher({id, no, ...status:'posted'})` + `AcctJE.receiptVoucher(...)` + `AcctAudit.log('receipt-voucher.post', ...)`.

#### 5.1.5 Backend

| Action | Method · path | Status |
|---|---|---|
| Post receipt voucher | `POST /accounting/vouchers/cr` body `{date, party_type, party_id, party_name, bank_code, credit_code, amount, ref, memo, idempotency_key}` (operationId `post_accounting_vouchers_cr`, tag *17.8 — Vouchers (CR / CP / JV)*) | ✅ |
| List receipt vouchers | `GET /accounting/journal-entries?source_doc_type=voucher_cr` — backend stores vouchers as JEs with `source_doc_type` discriminator (verified against `journal_entries` migration `2026_04_30_180008`). **No standalone `/accounting/vouchers` list endpoint exists.** | ✅ |

#### 5.1.6 Production gaps

- **Voucher numbering authority** — FE counts local store; on race conditions two clients could preview same number. Backend must mint number; FE shows a placeholder until response.
- **Owner-distinguishing** — `party_id` for owner contribution should resolve to the actual owner's user record, not a free-text name. **Verify** owner record exists in tenant's user list (Owner module).

---

### 5.2 — `payment.voucher` · سند صرف

| | |
|---|---|
| **File** | Same file — `paymentVoucher` (line 358), registered line 488. |
| **Triggers** | Command palette `pay.voucher` · New+ FAB · Spend tab "Petty cash" · Owner draw flow. |
| **Permissions** | open: `accounting.voucher.create` · post: `accounting.voucher.post` |

Mirror of §5.1. Steps `reason` / `method` (debit-side)  / `confirm`. 

#### 5.2.1 Reasons (the `PAYMENT_REASONS` table) → COA

| Reason ID | EN | AR | Debit COA | Party type | VAT? |
|---|---|---|---|---|---|
| `petty_cash` | Petty cash | مصروف نثري | `6610` Petty cash expense | other | ✓ |
| `owner_draw` | Owner draw | سحب المالك | `3110` Owner draws | owner | – |
| `salary_advance` | Salary advance | سُلفة موظف | `1410` Employee receivable | employee | – |
| `loan_repayment` | Loan repayment | سداد قرض | `2510` Long-term loan | bank | – |
| `interest_expense` | Interest expense | فوائد بنكية | `6810` Interest expense | bank | – |
| `donation` | Donation | تبرع | `6750` Donations | other | – |
| `other_expense` | Other expense | مصروف آخر | `6900` Other expense | other | ✓ |

#### 5.2.2 Voucher numbering

`CP-{YYYY}-{NNNN}` — same scheme, prefix `CP`.

#### 5.2.3 onFinish — JE posted

For VAT-inclusive petty-cash / other-expense (when `state.vatable && state.vatIncluded`):

```
DR  <debitCode>                  net
DR  1340 VAT input recoverable   net × 0.15
CR  <bankCode>                   gross
```

Otherwise:

```
DR  <debitCode>                  amount
CR  <bankCode>                   amount
```

#### 5.2.4 Backend

| Action | Method · path | Status |
|---|---|---|
| Post payment voucher | `POST /accounting/vouchers/cp` (same body as §5.1.5 with `debit_code` not `credit_code`; operationId `post_accounting_vouchers_cp`) | ✅ |
| List payment vouchers | `GET /accounting/journal-entries?source_doc_type=voucher_cp` (per §5.1.5 — vouchers stored as JEs) | ✅ |

#### 5.2.5 Production gaps

- **VAT-inclusive toggle** — only for `petty_cash` and `other_expense` reasons. Hardcoded list. Should be derived from a `vat_class` field on the reason record (data, not code).
- **Salary advance** — debits `1410` per-employee receivable; today single account for all employees. Sub-ledger by employee belongs in Owner+HR's payroll service. **Cross-module dependency.**

---

## §6 · Bank / reconciliation flows

### 6.1 — `bank.reconcile` · Reconcile bank

| | |
|---|---|
| **File** | `accounting-flow-defs.compiled.js` — `bankReconcile` (line 855), registered line 974. |
| **Triggers** | Command palette `bank.rec` (`⌘R`) · New+ FAB · Home unfiled-alerts CTA · Bank tab statement row → "Reconcile" · `acct:gotoTab` from period-close checklist. |
| **Permissions** | open: `accounting.bank.reconcile` · close: `accounting.bank.reconcile` |

#### 6.1.1 Steps

| # | ID | Title | What it asks |
|---|---|---|---|
| 1 | `choose` | Mode / الوضع | Auto-match · Side-by-side · Train a rule. |
| 2 | `work` | Work / تطبيق | The chosen mode's workspace. M = match, S = skip. |

#### 6.1.2 Modes

| Mode | What happens |
|---|---|
| `auto` | Saaed proposes pairings (date ± 3d, amount ± 0.01, party). User confirms in bulk. |
| `side` | Two-column UI. Bank statement left, ledger entries right. Drag rows to pair. |
| `rules` | Train a Saaed rule: "Anything from STC → 6720 Comms." Future months auto-categorize. |

#### 6.1.3 State

```js
{
  bankCode: '1101', mode: 'auto',
  matched: { [txnId]: invoiceOrBillId },
}
```

#### 6.1.4 onFinish — no JE posted

Reconciliation is a *match*, not a JE. Each match flips `transaction.matched_id` and `invoice.status` (to `paid` or `partially_paid`).

In `rules` mode, the `onFinish` instead writes a rule record to `AcctStore.rules`:

```js
{ id, match: { description_contains, party_pattern, amount_range }, action: { post_to_coa, party_type, party_id } }
```

#### 6.1.5 Backend (post-OpenAPI sweep)

| Action | Method · path | Status |
|---|---|---|
| Auto-match proposals | `GET /accounting/bank/reconcile/proposals?bank_code={code}` (operationId `get_accounting_bank_reconcile_proposals`, tag *17.9 — Bank reconciliation*) — server returns scored candidates per txn | ✅ |
| Confirm match — **per-line** | `POST /accounting/bank/reconcile/match` body `{bank_code, txn_id, doc_id, idempotency_key}` (operationId `post_accounting_bank_reconcile_match`). **Backend takes one match at a time, not bulk.** FE must call N times — wire as `Promise.allSettled` with per-row error surfacing. | ⚠️ FE drift — currently `bank-recon-store.js` line 87 batches into `confirmAll(matches)` expecting bulk endpoint. Refactor to per-row. |
| Skip txn | `POST /accounting/bank/reconcile/skip` body `{bank_code, txn_id, reason}` | ✅ |
| Save rule | `POST /accounting/bank/rules` body `{tenant_id, match: {field, op, value}, action: {coa_code, party_id?}}` (operationId `post_accounting_bank_rules`) | ✅ |
| List rules | `GET /accounting/bank/rules` | ✅ |
| Statement import (push) | `POST /accounting/bank/statements/ingest` — webhook target for the bank-feed aggregator (Lean Tech). **Not user-callable.** Documented for Ops. | ✅ |

#### 6.1.6 Production gaps

- **Unmatched-as-voucher fallback** — when a bank txn has no doc match (e.g., bank fee posted directly), today the user must close `bank.reconcile` and open `payment.voucher` separately. **Recommend:** add an inline "→ create voucher" CTA per row.
- **Statement import** — `GET /accounting/bank/statements` is assumed to deliver bank-feed transactions. Stage 4 must clarify whether this is push (webhook from a bank-feed aggregator like Lean) or pull (PSD-style polling).

---

### 6.2 — `bank.rules` · Train a rule (variant)

| | |
|---|---|
| **Registration** | Line 975 — `{...bankReconcile, title:'Bank rules', init:()=>({mode:'rules', ...})}`. |
| **Triggers** | Bank tab "Rules" sub-tab → "+ New rule". Could be reached from command palette but isn't currently listed. |

Same flow as §6.1, but `init` pins `mode: 'rules'`, skipping the mode picker. Onfinish writes the rule, no JE.

---

## §7 · Compliance flows

### 7.1 — `vat.file` · File VAT return

| | |
|---|---|
| **File** | `accounting-flow-defs.compiled.js` — `vatFile` (line 1006), registered line 1382. |
| **Triggers** | Command palette `vat.file` · New+ FAB · Home VAT-deadline alert · period-close checklist `if (it.k==='vat')` (line 1193). |
| **Permissions** | open: `accounting.vat.prepare` · submit: `accounting.vat.submit` |

#### 7.1.1 Steps

| # | ID | Title | What it asks |
|---|---|---|---|
| 1 | `period` | Period / الفترة | Quarterly (under 40M SAR revenue) or monthly. Saaed defaults from CR record. |
| 2 | `review` | Review / مراجعة | The 7-box ZATCA return computed from posted invoices + bills. User can override + drilldown. |
| 3 | `submit` | Sign & submit / توقيع وإرسال | Cryptographic signing → ZATCA API submit → ack# returned. |

#### 7.1.2 The 7 boxes (ZATCA standard)

| Box | EN | What it sums |
|---|---|---|
| 1 | Standard-rated sales | Σ invoices `vat_rate=15%` net |
| 2 | Zero-rated sales | Σ invoices `vat_rate=0%` net |
| 3 | Exempt sales | Σ invoices marked `exempt` |
| 4 | Imports subject to VAT | Σ import bills VAT |
| 5 | Standard-rated purchases (recoverable) | Σ bills `vat_class=recoverable` net |
| 6 | VAT due (1 × 15%) | Output VAT |
| 7 | VAT recoverable (5 × 15%) | Input VAT |

Net payable = Box 6 − Box 7. **JE posted on submit:**

```
DR  2310 VAT output payable     Box 6
CR  1340 VAT input recoverable  Box 7
CR  2199 VAT clearing           net (= 6 − 7)
```

When the actual SADAD payment is made, a separate `payment.voucher` clears `2199`.

#### 7.1.3 Backend (post-OpenAPI sweep)

| Action | Method · path | Status |
|---|---|---|
| Compute return | `GET /accounting/vat/return?period={YYYY-Qn}` (operationId `get_accounting_vat_return`, tag *17.10 — VAT*) — returns `{period, boxes: [{box_no, label_en, label_ar, value, audit: [{je_id, amount}]}], computed_at}`. Period format **must be `YYYY-Qn`** (e.g. `2026-Q1`), not `2026Q1` — schema enforces hyphen. | ✅ — fix FE format string (`vat-return-store.js` line 34 currently sends `2026Q1`). |
| Submit | `POST /accounting/vat/return/submit` body `{period, boxes: [{box_no, value, override_reason?}], idempotency_key}` — **async**, returns `{submission_id, status: 'queued'}`. Final ack arrives via `GET /accounting/vat/return/submission/{submission_id}` polled until `status: 'acknowledged'` with `ack_no, submitted_at`. FE must show a "Submitting…" state, not block on the POST. | ⚠️ FE drift — current flow expects synchronous `{ack_no}` in the POST response. Refactor `vat.file` step 3 to a polling state machine. |
| Mark filed | implicit on `acknowledged` polling response | – |
| Override reason | server requires `override_reason` (string, ≥10 chars) when `box.value !== computed.value` — closes the audit gap flagged in §7.1.4. | ✅ |

#### 7.1.4 Production gaps

- **Apr 22 hardcoded reference date** — there is also a hardcoded "next deadline" in `accounting-home.compiled.js` around the VAT alert (flagged in `SCREENS-INVENTORY-Accounting.md §2`). Both must come from server policy, not FE constant.
- **B2C threshold** — businesses below 40M SAR file quarterly. Step 1 gives this as a user choice; should be enforced by server based on tenant's revenue band.
- **Overrides audit** — if user overrides a box, the override needs to land in the audit trail with a reason. Today the override field exists but has no `reason` capture. Recommend adding.

---

### 7.2 — `period.close` · Close month

| | |
|---|---|
| **File** | `accounting-flow-defs.compiled.js` — `periodClose` (line 1140), registered line 1383. |
| **Triggers** | Command palette `period.close` · New+ FAB · Home end-of-month banner · `acct:gotoTab` from periods/lock screen. |
| **Permissions** | open: `accounting.period.close` · lock: `accounting.period.lock` (admin only) |

#### 7.2.1 Steps

| # | ID | Title | What it asks |
|---|---|---|---|
| 1 | `pick` | Period / الفترة | Which month to close (default = previous month). |
| 2 | `check` | Checklist / قائمة المراجعة | Run-through: VAT filed? Bank reconciled? AR/AP aged? Depreciation posted? |
| 3 | `lock` | Lock / قفل | Final lock + admin password. Re-openable but logged. |

#### 7.2.2 Checklist items (the 4 in `it.k`)

| Key | Item | Open action |
|---|---|---|
| `vat` | VAT return for period filed | Opens `vat.file` |
| `bank` | Every bank account reconciled | Opens `bank.reconcile` |
| `ar` | AR aged & past-due reviewed | Routes to Collect tab |
| `depr` | Depreciation posted (fixed assets) | (placeholder; depreciation is V2) |

#### 7.2.3 onFinish — period locked

`AcctStore.setPeriod(period, 'locked')` — flips `state.periods['2026-01'] = 'locked'`. Subsequent posts to that period are rejected by the engine's date-validation hook.

#### 7.2.4 Backend

| Action | Method · path | Status |
|---|---|---|
| Lock period | `POST /accounting/periods/{period}/lock` body `{idempotency_key, reason}` | 🟡 verify |
| Re-open period | `POST /accounting/periods/{period}/reopen` body `{reason, admin_password}` | 🟡 verify |
| Period status | `GET /accounting/periods` | ✅ |

#### 7.2.5 Production gaps

- **Depreciation** — listed in checklist but no `depreciation.run` flow exists. V2 work.
- ~~**Re-open** — UI says "you can always re-open with admin password" but no re-open flow is wired. Today the user must edit `state.periods` via dev tools. **🆕 Add `period.reopen` flow.**~~ ✅ **Resolved:** `POST /accounting/periods/{key}/reopen` is in OpenAPI (`post_accounting_periods_key_reopen`) and is owner-only (`accounting.periods.reopen`). FE wire-up is the only remaining work — no net-new endpoint. See [`FLOW-INVENTORY-Addendum.md`](./FLOW-INVENTORY-Addendum.md) §1.1, §3.2.
- **Year-end close** — separate flow needed (closes income summary to retained earnings). Not in current scope.

---

## §8 · Onboarding flow

### 8.1 — `onboarding` · Set up Saaed Books

| | |
|---|---|
| **File** | `accounting-flow-defs.compiled.js` — `onboarding` (line 1228), registered line 1384. |
| **Triggers** | **Auto-launched** by `accounting-hub.compiled.js` line 396 when `!state.onboarded && !window.__acctOnbPrompted`. Also reachable from settings / company profile. |
| **Permissions** | open: `accounting.tenant.onboard` (= owner/admin role) |

#### 8.1.1 Steps (5)

| # | ID | Title | What it asks |
|---|---|---|---|
| 1 | `welcome` | Welcome / أهلاً | Decorative; "5 steps · ~4 min". |
| 2 | `company` | Company / الشركة | CR number lookup → MCI fetch (auto-fills name, activity, VAT#). |
| 3 | `bank` | Bank & ZATCA / البنك وزاتكا | Pick bank (Riyad/SNB/Rajhi/GIB) · ZATCA mode (sandbox/live). |
| 4 | `opening` | Opening balances / الأرصدة الافتتاحية | Cash · AR · AP · Inventory · Owner equity. Sample-data button + Skip. |
| 5 | `done` | You're ready / جاهز | Confirmation + 4 next-step prompts. |

#### 8.1.2 onFinish

```js
window.AcctStore.finishOnboarding(state);
// → state.onboarded = true
// → state.openingBalances = { cash, ar, ap, inventory, equity }
// → If any opening balance > 0, posts an opening JE:
//     DR 1010·1210·1310  CR 2100  CR 3100
```

#### 8.1.3 Backend

| Action | Method · path | Status |
|---|---|---|
#### 8.1.3 Backend (post-OpenAPI sweep)

| Action | Method · path | Status |
|---|---|---|
| MCI / Wathq lookup | `GET /accounting/tenant/cr-lookup?cr={cr_no}` (operationId `get_accounting_tenant_cr_lookup`, tag *17.11 — Tenant onboarding*). Cross-module — Onboarding service proxies Wathq; rate-limited 60/hr/tenant. | ✅ |
| Save tenant profile | **`PATCH /accounting/settings`** body merges any subset of `{cr, cr_name, cr_activity, vat_no, default_bank_code, fiscal_year_start, base_currency, zatca_mode}`. **Not `PUT /accounting/tenant`** — that endpoint does not exist. The settings document is the canonical tenant-config record. | ✅ — fix FE: `onboarding-store.js` line 156 currently calls `PUT /accounting/tenant` which 404s. Change to PATCH `/accounting/settings`. |
| Bank link initiate | `POST /accounting/bank-feeds/connect` body `{provider: 'lean'\|'tarabut', redirect_uri}` returns `{auth_url, state}` for OAuth handoff | ✅ |
| ZATCA enrol — **manual in v1.0** | **Deferred to v1.1.** v1.0 onboarding marks `zatca_mode: 'manual_csr'` in the settings PATCH; the actual cert provisioning happens out-of-band via the Compliance team's ops console. The `POST /accounting/zatca/enrol` endpoint exists in OpenAPI but is gated `feature_flag: zatca_phase2_self_serve` (off in v1.0). Onboarding step 4 should show "Compliance team will reach out within 2 business days" copy, not a sandbox/live toggle. | ⚠️ FE drift — remove the toggle from `onboarding-flow-defs.js` line 412 step `zatca`. |
| Opening JE | `POST /accounting/journal-entries` body `{date, lines, memo: 'OPENING-{date}', source_doc_type: 'opening_balance'}` | ✅ |

#### 8.1.4 Production gaps — significant

- **CR fake-fetch** — line 1268: 600ms timeout fakes the MCI response. Real Wathq integration is mandatory (Stage 4 may not have this — **🆕 confirm**).
- **Bank logos as emoji squares** — `🟦 🟩 🟫` placeholder. Need real bank logos in `assets/brands/banks/`.
- **ZATCA enrolment** — sandbox/live toggle is captured but the actual cert provisioning is faked. Real ZATCA Phase-2 enrolment is a multi-step CSR flow that requires user to upload the response onto a USB token; flagged for the integration team as **the heaviest piece of net-new work.**
- **Opening balances JE** — current preview computes a balanced JE with all on debit side and equity + AP on credit. The math only balances if assets total = AP + equity. If user enters arbitrary numbers, JE may not balance. **Add validation step** before posting.

---

## §9 · Manual JE flow

### 9.1 — `je.manual` · Manual journal entry

| | |
|---|---|
| **File** | `accounting-flow-defs.compiled.js` — `manualJE` (line 1480), registered line 1686. |
| **Triggers** | Command palette `je.manual` (`⌘J`) · New+ FAB (top of list) · Journal tab "+ New entry". |
| **Permissions** | read: `accounting.journal_entries.read` · open/create: `accounting.journal_entries.post` · reverse (§9.2): `accounting.journal_entries.reverse` · adjust (§9.3): `accounting.journal_entries.adjust`. *Backend-verified key set per integration plan.* |

#### 9.1.1 Steps (1!)

| # | ID | Title | What it asks |
|---|---|---|---|
| 1 | `compose` | Compose / تحرير | Date, ref, memo, balanced lines (≥2). |

Yes — single step. The simplest flow in the system. Validation: `Σ debits === Σ credits` and `Σ debits > 0`.

#### 9.1.2 State

```js
{
  date: todayISO(), memo: '', ref: '',
  lines: [
    { code: '', dr: 0, cr: 0, note: '' },   // each line: pick COA + dr OR cr (not both) + optional memo
    { code: '', dr: 0, cr: 0, note: '' },
  ],   // user can add/remove rows
}
```

#### 9.1.3 onFinish — JE posted directly

`AcctJE.manual({ date, memo, ref, lines })` posts a balanced GL entry. No invoice/bill/voucher record.

#### 9.1.4 Backend

| Action | Method · path | Status |
|---|---|---|
| Post manual JE | `POST /accounting/journal-entries` body `{date, memo, ref, lines:[{code, dr, cr, note}], idempotency_key}` | ✅ |
| List COA leaves | `GET /accounting/coa?leafOnly=true` | ✅ |

#### 9.1.5 Production gaps

- **Period-locked validation** — engine checks `state.periods[period]` locally; backend must double-check.
- **Permission grain** — `accounting.je.post` is currently a flat scope. Recommend splitting: `accounting.je.post.adjustment` (allowed for accountants) vs `accounting.je.post.compound` (>5 lines, restricted to senior accountants). **For V2.**

---

### 9.2 — `je.reverse` · Reverse entry

| | |
|---|---|
| **File** | `accounting-flow-defs-je-actions.compiled.js` — `jeReverse` (line 148), registered line 705. |
| **Triggers** | **Only** from `accounting-journal.compiled.js` line 588 — JE detail screen "Reverse" button. **Not** in command palette (must have a source JE to reverse). |
| **Permissions** | open: `accounting.je.reverse` · post: same |

#### 9.2.1 Steps (3)

| # | ID | Title | What it asks |
|---|---|---|---|
| 1 | `review` | Review source / مراجعة المصدر | Show source JE + impact. Confirm "this is what I want to reverse". |
| 2 | `date` | Reversal date / تاريخ العكس | Pick reversal posting date (must be in open or review period) + narration. |
| 3 | `confirm` | Confirm / تأكيد | Reversing-JE preview. Post button. |

#### 9.2.2 onFinish — JE posted

Posts an offsetting JE: every dr ↔ cr swapped. Source JE's `reversed_by` field gets stamped with the new entry's id (linkage in both directions).

```
For each line in source:
  if line.dr > 0  →  new line  CR <line.code> dr_amount
  if line.cr > 0  →  new line  DR <line.code> cr_amount
```

`AcctAudit.log('je.reverse', {source_id, reversal_date, idempotency_key})`.

#### 9.2.3 Backend

| Action | Method · path | Status |
|---|---|---|
| Post reversing JE | `POST /accounting/journal-entries/reverse` body `{source_id, reversal_date, narration, idempotency_key}` | 🟡 verify |
| Audit log | server-side; no FE call needed in production | – |

#### 9.2.4 Production gaps

- **Reversal of already-reversed entry** — backend must reject (status `already_reversed`); FE needs the error path UI.
- **Period-locked source** — already-locked source JE should still be reversible (regulatory adjustment) but only by accountants with `accounting.je.reverse.locked` scope. **Permission split needed.**

---

### 9.3 — `je.adjust` · Adjust entry

| | |
|---|---|
| **File** | Same — `jeAdjust` (line 385), registered line 706. |
| **Triggers** | **Only** from `accounting-journal.compiled.js` line 449 — JE detail screen "Adjust" button. Not in command palette. |
| **Permissions** | open: `accounting.je.adjust` · post: same |

#### 9.3.1 Steps (3)

| # | ID | Title | What it asks |
|---|---|---|---|
| 1 | `reason` | Reason & date / السبب والتاريخ | Why is this adjustment needed? Date + narration. |
| 2 | `lines` | Lines / السطور | Build the adjusting JE manually. Same line-editor as `je.manual`. |
| 3 | `confirm` | Confirm / تأكيد | Preview. Post button. |

#### 9.3.2 onFinish — JE posted

A separate balanced JE, **linked** to the source via `adjusts_id`. The source JE keeps its original posting; the adjustment is its own GL entry.

#### 9.3.3 Backend

| Action | Method · path | Status |
|---|---|---|
| Post adjusting JE | `POST /accounting/journal-entries/adjust` body `{source_id, adjustment_date, narration, lines, idempotency_key}` | 🟡 verify |

#### 9.3.4 Production gaps

- **Source-line preselect** — `init` seeds two empty lines using the source's first DR + CR codes. Smart default; works most of the time. Confirm UX is OK.
- **Adjustment vs reversal+rebook** — accountants sometimes prefer "reverse-and-rebook" to a single-JE adjustment for audit clarity. Recommend documenting the policy choice.

---

## §10 · Cut-over impact summary

### 10.1 The shape of the work

For every flow above, the cut-over follows the same pattern:

| Step | What changes | Where |
|---|---|---|
| 1 | Replace `AcctStore.add*` in `onFinish` with `useApi.mutate(...)` | Each `onFinish` |
| 2 | Use mutation response to seed `AcctStore` (so other tabs see new data via `acct:store:changed`) | Each `onFinish` |
| 3 | Pass `state.idemKey` through as `idempotency_key` | Mutation body |
| 4 | Drop `AcctJE.*` calls — backend posts JE atomically | Each `onFinish` |
| 5 | Wire error → `AcctNarrator.say('error', ...)` + step revert | Each `onFinish` |
| 6 | Wire success → `AcctNarrator.say('done', ...)` + summary modal | Already in place |

### 10.2 Per-flow priority (post-OpenAPI sweep)

All endpoint shapes have now been verified against the spec. Status column reflects backend coverage; the **FE drift** column flags client-side refactors needed even where the backend is ready.

| Flow | Priority | Backend coverage | FE drift to fix | Net-new endpoints |
|---|---|---|---|---|
| `invoice.b2b` (and aliases `.quick` / `.standard` / `.zatca`) | **P0** | ✅ all four endpoints confirmed | clearance polling state machine (async ack) | 0 |
| `payment.receive` | **P0** | ✅ | none | 0 |
| `bill.create` | **P0** | ✅ | none | 0 |
| `bill.pay` | **P0** | ✅ | none | 0 |
| `bank.reconcile` | **P1** | ✅ proposals + per-line `match` + `skip` + rules | **per-line refactor** — drop `confirmAll` bulk batcher, call `/match` N times with `Promise.allSettled` | 0 |
| `bank.rules` | P1 | ✅ | none | 0 |
| `vat.file` | **P1** | ✅ compute + async submit | **period format** `2026-Q1` not `2026Q1`; **async submit polling**; **override_reason** capture | 0 |
| `period.close` | P1 | ✅ all three endpoints (close + reopen + status) | none | 0 |
| `receipt.voucher` / `payment.voucher` | P1 | ✅ `/vouchers/cr` + `/vouchers/cp`; list via `journal-entries?source_doc_type=voucher_cr\|cp` | path rename `receipt`→`cr`, `payment`→`cp`; remove standalone `/vouchers` list call | 0 |
| `je.manual` | P1 | ✅ | none | 0 |
| `je.reverse` / `je.adjust` | P2 | ✅ | none | 0 |
| `onboarding` | **P0** | ✅ CR-lookup + ✅ settings PATCH + ✅ bank-feed connect + ⏸️ ZATCA enrol (deferred to v1.1) | **endpoint `PUT /tenant`→`PATCH /settings`**; **remove ZATCA sandbox/live toggle**; **remove CR fake-fetch** | 0 |
| `payment.installment` | P3 | – alias only | defer | – |

**Summary:** **All 16 flows are now backend-covered for v1.0.** Zero net-new endpoints required. The work is FE refactoring to match the spec — five flows have FE drift (`invoice.b2b` async, `bank.reconcile` per-line, `vat.file` async + format, vouchers paths, onboarding settings). The biggest single pivot is **deferring ZATCA Phase-2 self-serve enrolment to v1.1**: v1.0 onboarding lands `zatca_mode: 'manual_csr'` in the settings PATCH and Compliance team handles cert provisioning out-of-band. This removes what was previously the heaviest integration item from the v1.0 critical path.

### 10.3 Cross-cutting cut-over items

These touch *every* flow:

1. **Idempotency keys.** Every `onFinish` already mints one; pipe through.
2. **Period-locked guard.** Today FE-side check via `AcctStore.state.periods`. Backend must enforce too.
3. **Permission gates.** Two flows (`je.reverse`, `je.adjust`) have inline FE permission check (lines 315 + 646 of `je-actions.js`); the rest rely on UI-level gates from `acctPerms`. Backend must enforce all of them.
4. **Audit log.** Vouchers + JE actions explicitly call `AcctAudit.log`; other flows rely on server-side audit. **Confirm** every `onFinish` lands in the audit trail server-side.
5. **`acct:store:changed`** event firing post-mutation — needed for other tabs (Sell, Spend, GL, Journal) to refresh. Today fires on `AcctStore.add*`; in production, fire on mutation success.
6. **Currency** — every flow assumes SAR. Multi-currency is V2.
7. **ZATCA period date hardcodes** — flagged in §7.1.4.

### 10.4 Net-new flows recommended

These don't exist today but are referenced in screens or implied by gaps:

| Proposed ID | Why | Priority |
|---|---|---|
| ~~`period.reopen`~~ | ✅ **Resolved** — endpoint exists in OpenAPI (`POST /accounting/periods/{key}/reopen`); FE-only work remains. | — |
| `bill.pay.batch` | Pay tab batch bar implies multi-bill flow. **Deferred to v1.1** per integration plan. | v1.1 |
| `payment.bnpl` | Replace `payment.installment` alias with real flow | P2 |
| `depreciation.run` | Period-close checklist references it | P2 |
| `year.close` | Year-end close (income → retained earnings) | P2 |
| `customer.create` | Inline-create today; needs first-class flow | P3 |
| `vendor.create` | Same | P3 |

---

## §11 · Pointer index

When implementing the cut-over, the order to read files in:

1. `accounting-flow-engine.js` — the `AcctFlow` API, the store, the JE helpers.
2. `accounting-flow-ui.compiled.js` — the modal component (steps render here).
3. `accounting-flow-defs.compiled.js` — all 11 base flows. **Note line 2461–2463** — the `invoiceB2B` override.
4. `accounting-flow-defs-vouchers.compiled.js` — 2 voucher flows + the `voucherNo()` helper.
5. `accounting-flow-defs-je-actions.compiled.js` — `je.reverse` + `je.adjust`.
6. `accounting-flow-summary.compiled.js` — success modal + "next flow" chain (line 299).
7. `accounting-narrator.compiled.js` — bottom-of-screen toast that every flow uses.
8. `accounting-journal.compiled.js` lines 449 + 588 — only places `je.adjust` / `je.reverse` open from.
9. `accounting-hub.compiled.js` line 396 — onboarding auto-launch.
10. `dalseen-integrations.js` — the cross-module bus that listens to `acct:flow:opened/closed`.

---

> **Status.** All 16 registered flows enumerated, with steps · state · JE preview · backend mapping · permission · idempotency · gaps. Cut-over priority + net-new recommendations in §10. Sibling docs already document the read-only screens (`SCREENS-INVENTORY-Accounting.md`), the underlying API surface (`API-USAGE-MAP.md`), and the cross-module event bus (`INTEGRATION-NOTES.md`).
