# Frontend API Foundation

> **Verified accurate:** 2026-05-02 — all six files (`api-foundation.js`, `api-session.js`, `api-fetch.js`, `api-namespace.js`, `api-hooks.jsx`, `api-states.jsx`) exist in `front/api/` and the load-order contract holds. The foundation has since been extended by 8 batch-namespace files (`api-namespace-batch1.js` … `batch8.js`) which add modules without changing the contract documented here.
> **Status:** shipped foundation doc; this is still the canonical contract for the API layer. See `API-USAGE-MAP.md` for the current full namespace map (post-batch additions).

> **Status:** Shipped 2026-04-22 · Task **#14**.
> **Scope:** The single chokepoint every screen uses to talk to the backend, with a mock execution mode for prototyping.
> **Audience:** Cursor, frontend devs, anyone migrating a screen from inline data to API calls.

This is the written record of what landed under **#14 window.API foundation**. It is the contract: anything new must be built on top of it, nothing should bypass it.

---

## 0 · TL;DR

Six files under `front/api/` provide:

| File | Loads | Responsibility |
|---|---|---|
| `api-foundation.js` | first | `Money`, `Bilingual`, `ULID`, `idemKey`, `ApiError`, `MOCK_MODE` flag |
| `api-session.js` | 2nd | Token, tenant, branch, locale, permissions, branch-switcher, sign-out |
| `api-fetch.js` | 3rd | `API.call(method, path, opts)` — single chokepoint, mock + live modes |
| `api-namespace.js` | 4th | `API.{module}.{resource}.{verb}()` — typed helpers mirroring Postman |
| `api-hooks.jsx` | after React | `useApi`, `useApiMutation`, `useSession`, `usePermission`, `useLocale` |
| `api-states.jsx` | after hooks | `<Skeleton>`, `<Spinner>`, `<EmptyState>`, `<ErrorBanner>`, `<Forbidden>`, `<ApiBoundary>` |

All wired in `index.html` between common bootstrap and module scripts.

```html
<!-- ════ window.API foundation (production-shaped frontend) ════ -->
<script src="front/api/api-foundation.js?v=1"></script>
<script src="front/api/api-session.js?v=1"></script>
<script src="front/api/api-fetch.js?v=1"></script>
<script src="front/api/api-namespace.js?v=1"></script>
<script type="text/babel" src="front/api/api-hooks.jsx?v=1"></script>
<script type="text/babel" src="front/api/api-states.jsx?v=1"></script>
```

---

## 1 · Hard contracts (the seven non-negotiables)

Every API call obeys these, mock or live:

| # | Rule | How it's enforced |
|---|---|---|
| 1 | **IDs are ULIDs** (26-char, sortable) | `API.id()` mints them; backend uses them too |
| 2 | **Money is `{ amount_minor, currency_code }`** — never floats | `API.Money.sar(halalas)` only API |
| 3 | **Bilingual fields**: `name_en`/`name_ar` *or* `{ ar, en }` JSON | `API.Bilingual.t(rec, 'name', locale)` reads either pattern with fallback |
| 4 | **Idempotency-Key on every write** | `apiCall` auto-injects UUID v4 on POST/PATCH/DELETE |
| 5 | **Errors are `ApiError` instances** with stable `code` + localised messages | `apiCall` always rejects with `ApiError`, never raw |
| 6 | **Branch context** (`X-Branch-Id`) on branch-scoped calls | Pulled from `API.session.branchId()` automatically |
| 7 | **List endpoints**: `{ data: [], meta: { total, page, per_page, last_page } }` | `paginate()` helper in mock; backend already conforms |

---

## 2 · The envelope

### Success
```json
{
  "data": { /* resource */ } | [/* list */],
  "meta": { "total": 48, "page": 1, "per_page": 25, "last_page": 2 }
}
```

### Error → throws `API.ApiError`
```js
class ApiError extends Error {
  code: 'VALIDATION_FAILED' | 'PERMISSION_DENIED' | 'NOT_FOUND' | …
  http_status: 422 | 403 | 404 | 409 | 500 | 0
  message_en: string
  message_ar: string
  fields: { fieldName: ['error1', …] } | null     // 422 only
  correlation_id: string | null                    // for support
  // Helpers:
  msg(locale)         → message_<locale>
  isValidation()      → http_status === 422
  isConflict()        → 409
  isForbidden()       → 403
  isNotFound()        → 404
  isServerError()     → >= 500
}
```

### Stable error codes (UI must branch on these, NOT message text)
| Code | Status | Use for |
|---|---|---|
| `VALIDATION_FAILED` | 422 | Show field errors inline |
| `PERMISSION_DENIED` | 403 | Render `<Forbidden perm={...}>` |
| `IDEMPOTENCY_CONFLICT` | 409 | Tell user "already submitted" |
| `STATE_CONFLICT` | 409 | Tell user record state changed (refresh) |
| `NOT_FOUND` | 404 | Empty state with "go back" |
| `VAT_PERIOD_LOCKED` | 409 | Surface VAT period locked banner |
| `PERIOD_LOCKED` | 409 | Period close banner |
| `BRANCH_REQUIRED` | 400 | Open branch switcher |
| `TENANT_QUOTA_EXCEEDED` | 429 | Upsell prompt |
| `INTERNAL_SERVER_ERROR` | 500 | Generic retry banner |
| `NETWORK_ERROR` | 0 | Offline banner |

---

## 3 · Public API surface

Available at `window.API` after foundation loads.

### 3.1 — Atomic primitives (foundation)

```js
API.id()                        // → ULID string (26 chars)
API.idemKey()                   // → UUID v4 for Idempotency-Key
API.now()                       // → ISO date — uses 2026-04-22 if AcctData loaded
API.nowTs()                     // → ISO timestamp

API.Money.sar(halalas)          // → { amount_minor: halalas, currency_code: 'SAR' }
API.Money.fromMajor(12.34)      // → { amount_minor: 1234, currency_code: 'SAR' }
API.Money.add(a, b)             // throws if currency mismatch
API.Money.sub(a, b)
API.Money.mul(a, n)
API.Money.fmt(money, 'en')      // → 'SAR 1,234.50'
API.Money.fmt(money, 'ar')      // → '1,234.50 ر.س'
API.Money.toMajor(money)        // → 12.34 (float, only for inputs)
API.Money.isZero(m)
API.Money.isNegative(m)
API.Money.eq(a, b)

API.Bilingual.t(rec, 'name', 'en')        // reads name_en, falls back to name_ar
API.Bilingual.t(rec, 'description', 'ar') // reads description.ar (nested) or description_ar
API.Bilingual.flat('name', 'Hi', 'مرحبا') // → { name_en: 'Hi', name_ar: 'مرحبا' }
API.Bilingual.pair('Hi', 'مرحبا')         // → { en: 'Hi', ar: 'مرحبا' }

API.ApiError                    // class — see §2
API.MOCK_MODE                   // boolean — current execution mode
API.setMockMode(true|false)     // persists to localStorage
```

### 3.2 — Session

```js
API.session.get()                  // full snapshot (read-only, frozen)
API.session.set(patch)             // merge + persist + notify
API.session.subscribe(fn)          // → unsubscribe
API.session.signOut()              // wipes session + tenant data
API.session.reset()                // session only

API.session.token()                // 'mock-token-…' or real Sanctum token
API.session.user()                 // { id, name_en, name_ar, email, role }
API.session.tenant()               // { id, name_en, name_ar, vat_number, cr_number }
API.session.tenantId()             // ULID
API.session.branch()               // current branch object
API.session.branchId()             // ULID | null
API.session.branches()             // available branches []
API.session.switchBranch(id)       // → boolean (true if found)

API.session.locale()               // 'en' | 'ar'
API.session.setLocale('ar')        // also flips <html dir="rtl">

API.session.can('catalog.products.create')      // boolean
API.session.canAny(['p1','p2'])    // boolean
API.session.canAll(['p1','p2'])    // boolean
API.session.grant(perm)            // dev/test
API.session.revoke(perm)           // dev/test
```

### 3.3 — Resource namespace (Postman folder tree)

| Surface | Method | Returns |
|---|---|---|
| `API.auth.login({email, password})` | POST | `{ data: { token, user, current_company, current_branch } }` |
| `API.auth.logout()` | POST | `{ data: { success } }` |
| `API.me.get()` | GET | `{ data: { user, tenant, branch, branches, permissions } }` |
| `API.me.setupProgress()` | GET | `{ data: { kyc_complete, …, pct_complete } }` |
| `API.catalog.products.list(params)` | GET | paginated `{ data, meta }` |
| `API.catalog.products.get(id)` | GET | `{ data: product }` |
| `API.catalog.products.create(body)` | POST | `{ data: product }` |
| `API.catalog.products.update(id, body)` | PATCH | `{ data: product }` |
| `API.catalog.products.remove(id)` | DELETE | `{ data: { success } }` |
| `API.catalog.categories.list()` | GET | `{ data: [] }` |
| `API.inventory.levels(params)` | GET | paginated levels |
| `API.inventory.adjust(body)` | POST | `{ data: adjustment }` |
| `API.crm.customers.list(params)` | GET | paginated |
| `API.crm.customers.get(id)` | GET | `{ data }` |
| `API.crm.customers.create(body)` | POST | `{ data }` |
| `API.crm.suppliers.list(params)` | GET | paginated |
| `API.sales.list(params)` | GET | paginated |
| `API.sales.create(body)` | POST | `{ data: sale }` |
| `API.shifts.current()` | GET | `{ data: shift | null }` |
| `API.shifts.open(body)` | POST | `{ data: shift }` |
| `API.shifts.close(id, body)` | POST | `{ data: shift }` |
| `API.accounting.reports.trialBalance()` | GET | `{ data: { rows } }` |
| `API.accounting.reports.profitLoss()` | GET | `{ data: { rows } }` |
| `API.accounting.reports.balanceSheet()` | GET | `{ data: { rows } }` |
| `API.accounting.journalEntries.list(params)` | GET | paginated |

> **Accounting writes** still go through the existing `AcctStore` engine — see §6.
> **Direct call** for endpoints not yet in the namespace: `API.call(method, path, opts)`.

### 3.4 — `API.call()` — the chokepoint

```js
API.call(method, path, {
  params:        { q, page, per_page, … },   // querystring (GET) or extra body (writes)
  body:          { … },                       // request payload
  branchId:      'override-ulid',             // overrides session.branchId()
  idempotencyKey: 'custom-uuid',              // else auto-generated for writes
})
// → resolves { data, meta? }   |   rejects ApiError
```

Auto-injected headers (live mode):
- `Authorization: Bearer {token}`
- `Accept: application/json`
- `Content-Type: application/json`
- `Accept-Language: en|ar`
- `X-Tenant-Id: {ulid}`
- `X-Branch-Id: {ulid}` (when present)
- `Idempotency-Key: {uuid}` (writes only)

---

## 4 · React hooks

### `useApi(fn, deps)` — for reads

```jsx
const { data, loading, error, refetch, meta } = useApi(
  () => API.catalog.products.list({ q, category }),
  [q, category]
);
```

**Behaviour:**
- Calls `fn()` on mount + whenever any `dep` changes.
- Race-safe: stale responses are dropped if a newer call has already started.
- Auto-refetches when **session changes** (locale, branch, permissions) — so screens never show stale data after a branch switch.
- `data` unwraps the `{ data: … }` envelope for you.

### `useApiMutation(fn)` — for writes

```jsx
const { mutate, loading, error, data, reset } = useApiMutation(
  (body) => API.sales.create(body)
);

// Inline error handling:
try {
  const result = await mutate({ items, totals });
  toast.success('Sale recorded');
} catch (err) {
  if (err.isValidation()) showFieldErrors(err.fields);
  else if (err.isConflict()) toast.warning(err.msg(lang));
  else throw err;
}
```

### `useSession()` — reactive snapshot
### `usePermission(perm)` — reactive boolean
### `useLocale()` — `[locale, setLocale]` tuple

---

## 5 · State components

```jsx
<Skeleton rows={6} />              // shimmer rows for tables/lists
<SkeletonCard height={120} />      // single card placeholder
<SkeletonTable rows={8} cols={5} /> // full-table skeleton

<Spinner size={16} />              // inline / button-friendly

<EmptyState icon="📭" title="No products yet" body="…" ctaLabel="Add product" onCta={…} />

<ErrorBanner error={state.error} onRetry={state.refetch} compact />
// branches on error.code, shows correlation_id, lists field errors for 422

<Forbidden perm="catalog.products.create" />

// Convenience wrapper:
<ApiBoundary state={apiState} render={(data) => <ProductList items={data} />} />
```

All bilingual + RTL-aware via `API.session.locale()` (override with `locale` prop).

---

## 6 · Migration pattern (for #16–20)

The migration is mechanical. Per screen:

### Before
```jsx
const products = [
  { id: 1, name: 'Pepsi', price: 5 },
  …
];
return <ProductList items={products} />;
```

### After
```jsx
const { data, loading, error, refetch } = useApi(
  () => API.catalog.products.list({ q }),
  [q]
);

if (loading) return <SkeletonTable rows={8} cols={5} />;
if (error)   return <ErrorBanner error={error} onRetry={refetch} />;
if (!data?.length) return <EmptyState title="No products yet" ctaLabel="Add" onCta={…} />;
return <ProductList items={data} />;
```

Or with the convenience boundary:
```jsx
<ApiBoundary state={{ data, loading, error, refetch }}
  render={(items) => <ProductList items={items} />} />
```

### Permission-gated buttons

```jsx
const canCreate = usePermission('catalog.products.create');
return (
  <button disabled={!canCreate} onClick={…}>Add product</button>
);
// or hide entirely:
{canCreate && <button>…</button>}
```

### Branch-scoped screens

```jsx
const sess = useSession();
return <BranchSwitcher current={sess.branch} options={sess.branches}
  onChange={(id) => API.session.switchBranch(id)} />;
// All API calls automatically pick up the new X-Branch-Id
```

### Accounting writes (special case)

Accounting keeps its existing `AcctStore` write path — flows post via `AcctJE.post(...)`,
new screens read via `API.accounting.*`. Don't try to wrap AcctStore mutations in
`window.API.*` — they're already production-shape internally.

---

## 7 · Mock mode mechanics

When `API.MOCK_MODE === true`:

1. **Storage:** every record persists to `localStorage` keyed `saaed:tenant:{tenantId}:{resource}`.
2. **Latency:** randomised 180–420 ms per call (tweakable via `API.sim.latencyMs`).
3. **Failures:** 0% by default — bump `API.sim.failureRate = 0.05` to exercise error states.
4. **Force-error:** set `API.sim.forceError = 'PERMISSION_DENIED'` to make every call throw that code (great for testing UI branches).
5. **Force-loading:** `API.sim.forceLoading = true` → calls never resolve (skeleton testing).
6. **Reset tenant:** `API.store.wipeTenant()` clears all data for current tenant (fresh signup).

When `API.setMockMode(false)`:
- Same code paths fire.
- `fetch()` goes to `API.BASE_URL` (`localhost:8000/api/v1` on localhost, `api.dalseen.sa/api/v1` otherwise).
- All headers, envelopes, idempotency keys behave identically.
- **No code change needed in any screen.**

---

## 8 · What's NOT in this foundation (yet)

These are deliberate gaps. Add them when first needed, in the same shape.

| Missing | Where it'll go |
|---|---|
| WebSocket / pusher subscriptions (live KDS, terminals) | `API.realtime.subscribe(channel, fn)` — add when Dine KDS migrates |
| File uploads (logos, attachments) | `API.upload(file, kind)` returning `{ url, id }` |
| Polling helper (VAT 202, ZATCA refs) | `API.poll(fn, { until, intervalMs, timeoutMs })` — add when VAT Return migrates |
| Optimistic update wrapper | `useApiMutation(fn, { optimistic })` — add on first request |
| Caching / dedup | none — keep it simple until proven needed |
| Offline queue | none — out of scope until SoftPOS goes offline-first |

---

## 9 · How to verify

In the browser console on `index.html`:

```js
// Surface check
typeof window.API                           // 'object'
Object.keys(window.API).sort()              // includes auth, me, catalog, …

// Live read
await window.API.catalog.products.list({per_page:5})
// → { data: [5 products], meta: { total: 48, page: 1, per_page: 5, last_page: 10 } }

// Live write
await window.API.catalog.products.create({
  name_en: 'Test', name_ar: 'اختبار', price: API.Money.sar(1500)
})
// → { data: { id: '01HQK…', name_en: 'Test', … } }

// Permission gate
window.API.session.can('accounting.je.reverse')   // true (default owner)
window.API.session.revoke('accounting.je.reverse')
window.API.session.can('accounting.je.reverse')   // false

// Forbidden state
window.API.session.grant('accounting.je.reverse')  // restore
window.API.sim.forceError = 'PERMISSION_DENIED'
await window.API.catalog.products.list({})
// → throws ApiError { code:'PERMISSION_DENIED', http_status:403, … }
window.API.sim.forceError = null

// Locale flip
window.API.session.setLocale('ar')   // <html dir="rtl">
window.API.session.setLocale('en')
```

---

## 10 · Files & line counts

| File | Lines | Purpose |
|---|---|---|
| `front/api/api-foundation.js` | 198 | Atomic primitives |
| `front/api/api-session.js` | 220 | Session + permissions + locale |
| `front/api/api-fetch.js` | 280 | `apiCall()` + mock layer + storage |
| `front/api/api-namespace.js` | 365 | Mock handlers + typed helpers |
| `front/api/api-hooks.jsx` | 125 | React hooks |
| `front/api/api-states.jsx` | 215 | UX components |
| **Total** | **~1400** | |

---

## 11 · Next migrations (todo refs)

- **#16** Owner screens → `API.platform.*`, `API.owner.*` namespaces (extend §3.3)
- **#17** Retail screens → already use `API.catalog`, `API.inventory`, `API.crm`, `API.sales`, `API.shifts`
- **#18** Pay screens → add `API.pay.*` namespace
- **#19** Dine screens → add `API.dine.*` namespace
- **#20** Shared screens → AI/BI/CRM/Manufacturing — extend respective namespaces
- **#21** Verifier sweep — confirm all 5 states render per screen

---

*This document is the source of truth for what shipped. If you're tempted to deviate from any of §1, write down why here first.*
