# R-SWEEP · Step (a) — Mock Inventory

> **Document type:** frozen snapshot from 2026-04-30.
> **Verified 2026-05-02:** the 67-mock count and group breakdown remain accurate for `front/api/api-namespace.js` itself, but the prototype has since added 8 additional batch-namespace files (`api-namespace-batch3.js` … `api-namespace-batch8.js`) that this inventory does **not** cover. For the full current API surface across all namespace files, see `API-USAGE-MAP.md` and `front/api/api-namespace-*.js`.
> **Kept for context** as the snapshot that triggered the per-module SCREENS-INVENTORY reconciliation pass.

> **Status:** complete · 2026-04-30
> **Source:** `front/api/api-namespace.js` (1,166 lines, single IIFE)
> **Count:** 67 mock endpoints across 15 logical groups
> **Next step:** **(b)** classify each row against `POSTMAN-SURFACE.md` →
> ✅ matches · ⚠️ shape-drift · ❌ invented · 🆕 real-but-unmocked

This is the raw inventory — every `API.mock.register(...)` call in the
file, with its method, path, source-line, body shape, response shape, and
brief notes. Step (b) adds a verdict column; step (c) audits ID format
(ULID vs integer); step (d) reflects everything into `BACKEND-MAPPING.md`.

**How to read the tables.** The "body" column lists the keys the handler
reads from the request body (or `—` for GETs). The "response" column
describes the envelope shape the mock returns. Anything ambiguous (`...body`
spread, defaulted fields) is called out in notes.

**Conventions used by every mock** (from the file header):
- IDs: `API.id()` → ULID
- Money: `{ amount_minor, currency_code }` (via `M.sar(int)`)
- Bilingual: `name_en` / `name_ar` siblings or `{ ar, en }` JSON
- Lists: accept `{ q, page, per_page, sort, ...filters }`,
  return `{ data: [...], meta: { total, page, per_page, last_page } }`
- Errors: `throw new API.ApiError({ code, http_status, message_en, message_ar, fields? })`

---

## Summary by group

| Group | Count | Lines |
|---|---:|---|
| 1 · Auth                       | 2  | 76–94 |
| 2 · Me                         | 2  | 96–117 |
| 3 · Catalog · Products         | 13 | 145–369 |
| 4 · Catalog · Categories       | 1  | 371–375 |
| 5 · Inventory                  | 2  | 380–401 |
| 6 · CRM · Customers            | 3  | 430–447 |
| 7 · CRM · Suppliers            | 2  | 465–495 |
| 8 · Purchasing · POs           | 7  | 503–559 |
| 9 · Purchasing · RFQs          | 4  | 562–594 |
| 10 · Sales (POS)               | 2  | 598–620 |
| 11 · Shifts                    | 3  | 623–646 |
| 12 · Accounting · Reports + JE | 4  | 652–663 |
| 13 · Accounting · VAT Returns  | 4  | 677–765 |
| 14 · Owner                     | 4  | 868–909 |
| 15 · Platform                  | 14 | 939–1140 |
| **TOTAL**                      | **67** | |

---

## 1 · Auth (2)

| # | M | Path | Line | Body | Response | Notes |
|---|---|---|---:|---|---|---|
| 1.1 | POST | `/auth/login` | 76 | `email`, `password` | `{ data: { token, user, current_company, current_branch } }` | Throws `VALIDATION_FAILED` 422 if either field missing. Token = `'mock-token-' + ULID`. |
| 1.2 | POST | `/auth/logout` | 91 | — | `{ data: { success: true } }` | No-op. |

## 2 · Me (2)

| # | M | Path | Line | Body | Response | Notes |
|---|---|---|---:|---|---|---|
| 2.1 | GET | `/me` | 96 | — | `{ data: { user, tenant, branch, branches, permissions } }` | Reads from `API.session`. |
| 2.2 | GET | `/me/setup-progress` | 105 | — | `{ data: { kyc_complete, company_complete, branches_complete, coa_complete, opening_balance, vat_registered, first_invoice_sent, pct_complete } }` | Static fixture; `pct_complete: 86`. |

## 3 · Catalog · Products (13)

Largest group. Includes the **R1 extensions** (rows 3.6–3.13) added to give `RetailHooks.products` enough surface to become a thin sync façade. These are the most likely candidates for "invented" status in step (b) since `POSTMAN-SURFACE.md` shows the real backend uses `/products` (not `/catalog/products`) and lists no archive/restore/bulk-* actions.

| # | M | Path | Line | Body keys | Response | Notes |
|---|---|---|---:|---|---|---|
| 3.1 | GET | `/catalog/products` | 145 | — (`q`, `category`, `active`, pagination) | paginated list | Seeds 48 products on first call. Fuzzy search over `name_en/name_ar/sku/barcode`. |
| 3.2 | GET | `/catalog/products/:id` | 152 | — | `{ data: row }` | 404 `NOT_FOUND` if missing. |
| 3.3 | POST | `/catalog/products` | 157 | `name_en`, `name_ar`, `price`, plus `...body` | `{ data: row }` | Requires perm `catalog.products.create`; defaults `active:true`, `vat_rate:0.15`. |
| 3.4 | PATCH | `/catalog/products/:id` | 171 | partial product fields (`...body`) | `{ data: row }` | No permission gate. |
| 3.5 | DELETE | `/catalog/products/:id` | 176 | — | `{ data: { success:true } }` | Hard delete from store. |
| 3.6 | GET | `/catalog/products/:id/branch-stock` | 219 | — | `{ data: { product_id, by_branch: { [branch_id]: qty } } }` | **R1.** Deterministic split via `_branchStockFor`. Cached on `row._meta.branchStock`. |
| 3.7 | GET | `/catalog/products/:id/history` | 225 | — | `{ data: { product_id, entries: [{ at, actor, kind, detail }] } }` | **R1.** History stored in `row._meta.history`. |
| 3.8 | POST | `/catalog/products/:id/archive` | 231 | — | `{ data: row }` | **R1.** Sets `active:false`, `_meta.status:'archived'`. Perms: `catalog.products.archive` OR `.update`. |
| 3.9 | POST | `/catalog/products/:id/restore` | 243 | — | `{ data: row }` | **R1.** Inverse of archive. Same perm gate. |
| 3.10 | POST | `/catalog/products/bulk-reprice` | 256 | `ids: string[]`, `pct: number` | `{ data: { updated: row[], pct } }` | **R1.** Multiplies `price.amount_minor` by `1+pct/100`. Logs history per row. |
| 3.11 | POST | `/catalog/products/bulk-tag` | 280 | `ids: string[]`, `tag: string` | `{ data: { updated: row[], tag } }` | **R1.** Appends to `_meta.tags[]` if not already present. |
| 3.12 | POST | `/catalog/products/import` | 303 | `rows: [{ sku, name_en, name_ar, price, ... }]` | `{ data: { created_count, updated_count, created: row[], updated: row[] } }` | **R1.** Upsert by SKU. |
| 3.13 | POST | `/catalog/products/:id/adjust` | 337 | `branch_id?`, `qty`, `reason?`, `note?`, `photo?`, `approved_by?` | `{ data: { adjustment, product } }` | **R1.** Convenience over `/inventory/adjustments`; updates branch-stock cache + writes adjustment row. Perm: `inventory.adjust`. |

## 4 · Catalog · Categories (1)

| # | M | Path | Line | Body | Response | Notes |
|---|---|---|---:|---|---|---|
| 4.1 | GET | `/catalog/categories` | 371 | — | `{ data: [{ id, name_en, name_ar }] }` | Static array of 7 names. **No pagination.** Real backend has POST/PATCH/DELETE here too — they're not mocked. |

## 5 · Inventory (2)

| # | M | Path | Line | Body | Response | Notes |
|---|---|---|---:|---|---|---|
| 5.1 | GET | `/inventory/levels` | 380 | — (`q`, `low_stock`) | paginated list of `{ product_id, sku, name_en, name_ar, on_hand, reserved, available, reorder_level, low_stock }` | Computed from `catalog.products` seed. `reorder_level` hard-coded to 20. |
| 5.2 | POST | `/inventory/adjustments` | 393 | `branch_id?`, plus `...body` | `{ data: row }` | Branch defaults to current session. Perm: `inventory.adjust`. |

## 6 · CRM · Customers (3)

| # | M | Path | Line | Body | Response | Notes |
|---|---|---|---:|---|---|---|
| 6.1 | GET | `/crm/customers` | 430 | — (`q`) | paginated list | Seeds 10 named customers. Fuzzy search over `name_en/name_ar/vat_number/phone`. |
| 6.2 | GET | `/crm/customers/:id` | 434 | — | `{ data: row }` | 404 if missing. |
| 6.3 | POST | `/crm/customers` | 439 | `...body` | `{ data: row }` | Defaults `balance: M.sar(0)`. Perm: `crm.customers.manage`. |

## 7 · CRM · Suppliers (2)

| # | M | Path | Line | Body | Response | Notes |
|---|---|---|---:|---|---|---|
| 7.1 | GET | `/crm/suppliers` | 465 | — (`q`) | paginated list | Seeds 7 suppliers (Almarai, Nadec, etc.). |
| 7.2 | POST | `/crm/suppliers` | 469 | `name_en`/`name`, `name_ar`/`nameAr`, `vat_number?`, `payment_terms?`/`terms?`, `lead_time_days?`/`leadTime?`, `contact_name?`/`contact?`, `phone?`, `email?`, `legacy_id?` | `{ data: row }` | Permission check is permissive in mock (lines 471–477). Accepts both camelCase and snake_case keys (façade-friendly). |
