# Retail Migration Plan — `RetailHooks` → `window.API`

> **Verified accurate:** 2026-05-02 — R1 (Products) shipped wiring is live in `front/retail/retail-hooks.js` (`Hooks.products._api`, all 8 mirror calls + `legacy_id` link) and the 13 product endpoints exist in `front/api/api-namespace.js`. R2 (Suppliers) is also partially shipped — `Hooks.suppliers._api` mirror is live (line 918+).
> **Status:** historical migration plan; R1 ✅ shipped, R2 partial, R3–R5 superseded by the SCREENS-INVENTORY refresh + per-batch shipped work (Batch 5 customers/promotions/loyalty, Batch 8 pricing/vendor-360). Treat as historical context, not active queue.

> **Status:** **R1 shipped ✅** · R2–R5 ready to execute.
> **Owner doc:** This is the planning artifact for todo **#17 — Migrate Retail screens from inline data to API calls**.
> **Companion:** [`FRONTEND-API-FOUNDATION.md`](./FRONTEND-API-FOUNDATION.md) (the API layer being mapped onto), [`BACKEND-MAPPING.md`](./BACKEND-MAPPING.md) (real endpoint catalog).

---

## 0 · What's shipped (R1 — Products)

**Date shipped:** 2026-04-29
**Scope:** `RetailHooks.products.*` (the largest namespace) now mirrors every mutation to `API.catalog.products.*`. Reads remain synchronous. Zero screen-level changes.

**Mocks added** (`front/api/api-namespace.js`):
- `GET  /catalog/products/:id/branch-stock` — per-branch stock split (deterministic)
- `GET  /catalog/products/:id/history` — change log
- `POST /catalog/products/:id/archive` — sets `active=false`, logs entry
- `POST /catalog/products/:id/restore` — sets `active=true`, logs entry
- `POST /catalog/products/:id/adjust` — convenience wrapper over `/inventory/adjustments` with branch-stock cache update
- `POST /catalog/products/bulk-reprice` — body `{ ids, pct }`
- `POST /catalog/products/bulk-tag` — body `{ ids, tag }`
- `POST /catalog/products/import` — body `{ rows }` — upsert by SKU

**Helper namespace extended** (`API.catalog.products`):
```js
list, get, create, update, remove,                    // existing
branchStock, history, archive, restore, adjust,       // R1 added
bulkReprice, bulkTag, import                          // R1 added
```

**Façade wired** (`front/retail/retail-hooks.js`):
- New private `Hooks.products._api(fn, label)` swallow-on-fail wrapper
- `create`, `update`, `archive`, `restore`, `reprice`, `tag`, `adjust`, `import` each fire-and-forget mirror to `API.catalog.products.*`
- Legacy `id` is passed through as `legacy_id` so API records link back to in-memory rows
- Failures log to console as `[RetailHooks.products._api] <label> failed: …` — never throw to the caller

**Verification:**
- 13 product endpoints registered in mock layer
- `RetailHooks.products.create({...})` returns synchronously, mirror lands in `API.store('catalog.products')` after ~500ms
- POS screen (`Cashier`/cart UI) renders without errors
- Inventory screen renders product rows from legacy `DALSEEN_DATA`
- Cache-buster bumped: `api-namespace.js?v=4`, `retail-hooks.js?v=11`

**Pattern proven:** sync façade is the right shape for this codebase. R2–R5 follow mechanically.

---

## 1 · The decision

There are 25+ Retail screens reading from `window.RetailHooks.*` — a 1448-line abstraction layer that wraps an in-memory `mem` object plus `window.DALSEEN_DATA`. The naive migration is **wrap every screen in `useApi()`**. That's wrong here.

**The right move: rewrite `RetailHooks` itself so its methods proxy to `window.API.*` mocks under the hood.** Every screen using `RetailHooks` becomes API-backed for free. Zero screen-level changes for ~80% of Retail.

| Strategy | Cost | Result |
|---|---|---|
| Wrap each of 25 screens with `useApi()` | High (25 files × ~30min ea = 12h) | API-backed, but signature-breaking; lots of refactors |
| **Rewrite `RetailHooks` to call `API.*`** | Medium (1 file × ~3h) | API-backed, same signatures, zero screen changes |
| Skip migration | Zero | `RetailHooks` keeps its own `mem` — when backend lands, ~25 screens still need wiring |

---

## 2 · What's already in place

### `window.API.*` mocks that exist (verified in `front/api/api-namespace.js`)

| Namespace | Endpoints | Backed by |
|---|---|---|
| `API.catalog.products` | `list`, `get`, `create`, `update`, `remove` | `API.store('catalog.products')` (localStorage) |
| `API.catalog.categories` | `list` | static mock |
| `API.inventory` | `levels`, `adjust` | `API.store('inventory.adjustments')` |
| `API.crm.customers` | `list`, `get`, `create` | `API.store('crm.customers')` |
| `API.crm.suppliers` | `list` | seed-only |
| `API.sales` | `list`, `create` | `API.store('sales')` |
| `API.shifts` | `current`, `open`, `close` | `API.store('shifts')` |
| `API.accounting.*` | TB / P&L / BS / JE / VAT-returns / VAT-file (202) | `window.AcctStore` |
| `API.owner.*` | dashboard, approvals, banks, portfolio | `window.DALSEEN_OWNER` |
| `API.platform.*` | dashboard + tenants CRUD + suspend/reactivate/plan/impersonate/message | `window.DALSEEN_PLATFORM` |

### `window.RetailHooks.*` surface (the migration target)

20 namespaces, ~1100 LOC of logic:

| Namespace | Methods | API mock today | Migration work |
|---|---|---|---|
| `audit` | list, log | none | New `API.audit` namespace |
| `giftCards` | list, lookup, redeem | none | New `API.crm.giftCards` |
| `bundles` | list, expand | none | New `API.catalog.bundles` |
| `layaway` | list, create, settle | none | New `API.sales.layaway` |
| `loyalty` | tier, pointsForSale, applyTier | none | New `API.crm.loyalty` |
| `expiry` | list, all | none | New `API.inventory.expiry` |
| **`products`** | 14 methods | ✅ `API.catalog.products` | **R1** — biggest win |
| `transfers` | list, get, create, receive, cancel | none | New `API.inventory.transfers` |
| `adjustments` | list | partial — `API.inventory.adjust` | extend `API.inventory.adjustments` |
| `receives` | list, create | none | New `API.purchasing.receives` |
| `counts` | list, get, create, post | none | New `API.inventory.stocktakes` |
| `suppliers` | list, get | partial — `API.crm.suppliers` | extend |
| `po` | list, get, create, send, receive, cancel | none | New `API.purchasing.purchaseOrders` |
| `rfq` | list, get, create, quote, award | none | New `API.purchasing.rfqs` |
| `alerts` | list (derived) | n/a | leave as derived |
| `reorder` | suggest (derived) | n/a | leave as derived |
| `wastage` | derived from adjustments | n/a | leave as derived |
| `expiry` (batches) | list, _seed | none | New `API.inventory.expiryBatches` |
| `receipts` | byNumber | n/a | leave as stub |

**Verdict:** roughly half the surface already has API mocks; the other half needs new mock handlers added to `api-namespace.js` first, then `RetailHooks` rewired.

---

## 3 · Five-phase plan

### R1 · Products (catalog) — **execute first**
**Why first:** largest surface (14 methods), most-used by other screens (POS, Products, Inventory, Reorder, Alerts, Expiry).

**Mocks to add to `api-namespace.js`:**
- `GET /catalog/products/:id/branch-stock` — returns `{ branch_id: qty }` map
- `POST /catalog/products/:id/adjust` — wraps existing `/inventory/adjustments` with product context
- `POST /catalog/products/bulk-reprice` — body `{ ids[], pct }`
- `POST /catalog/products/bulk-tag` — body `{ ids[], tag }`
- `POST /catalog/products/import` — body `{ rows[] }`
- `GET /catalog/products/:id/history` — change log
- `POST /catalog/products/:id/archive`, `POST /catalog/products/:id/restore`

**`RetailHooks.products` rewrite:**
- Keep all 14 method names + signatures.
- Each method becomes `await API.catalog.products.<method>(...)`.
- Local mutations (history, undo stack, perBranch cache) move into mockStore via the new endpoints.
- Add `MOCK_LATENCY_MS` const (default 0; tweak panel toggles to 250ms for state testing).

**Acceptance:** POS adds-to-cart, Products list filters, Inventory adjust, Reorder suggest all render unchanged. Tweak panel "simulate latency" shows skeletons.

---

### R2 · Purchasing (PO + RFQ + Suppliers)
- Add `API.purchasing.purchaseOrders.*` (5 methods), `API.purchasing.rfqs.*` (5 methods), extend `API.crm.suppliers` with `get`/`create`.
- Rewire `RetailHooks.po`, `.rfq`, `.suppliers`.
- These are read-mostly — easy migration, no optimistic UI needed.

### R3 · Inventory ops (Transfers, Adjustments, Receives, Counts)
- Write-heavy. Each write needs `Idempotency-Key` header (already supported by `API.call`).
- Adjustments already partial — extend with `list`, `:id`, `void`.
- Add `API.inventory.transfers.*` (CRUD + receive/cancel).
- Add `API.inventory.stocktakes.*` (CRUD + post → generates adjustments).
- Add `API.purchasing.receives.*` (CRUD + post → generates inventory.adjust).

### R4 · Customer-facing (Loyalty, GiftCards, Bundles, Layaway)
- Add `API.crm.loyalty.*`, `API.crm.giftCards.*`, `API.catalog.bundles.*`, `API.sales.layaway.*`.
- Rewire from `RetailHooks`.
- Lower priority — only used in POS edge flows.

### R5 · Derived views (alerts, reorder, wastage, expiry)
- These compute over the source data. **Leave untouched** until R1–R3 are done; they'll automatically read API-backed data once their sources are migrated.
- Optional: add `GET /inventory/alerts`, `GET /inventory/reorder-suggestions` mocks for Cursor handoff fidelity. Not required.

---

## 4 · Bridging pattern

Because `RetailHooks` is sync today and `API.*` is async, we have two options:

### Option A — Make `RetailHooks` async
Breaking change. Every caller becomes `await RetailHooks.products.create(...)`. ~80 callsites. **Reject.**

### Option B — Sync façade over async API ✅
`RetailHooks` keeps sync signatures. Internally:

```js
// retail-hooks.js (after migration)
products: {
  all: () => _cache.products,           // served from local cache
  get: (id) => _cache.products.find(p => p.id === id),
  create: (data) => {
    const optimistic = { id: API.id(), ...data, _pending: true };
    _cache.products.unshift(optimistic);
    emit('product.create', optimistic);
    API.catalog.products.create(toApiShape(data))
      .then(({ data: real }) => {
        Object.assign(optimistic, real, { _pending: false });
        emit('product.create.confirmed', real);
      })
      .catch(err => {
        _cache.products = _cache.products.filter(p => p !== optimistic);
        emit('product.create.failed', { data, err });
      });
    return optimistic;
  },
  // ...
}
```

**Result:** screens still call `RetailHooks.products.create(...)` synchronously and get an object back. Optimistic UI continues working. The cache is hydrated on first read from `API.catalog.products.list()`. Failures emit events the UI can listen for (Toast, retry).

This is the chosen pattern. It preserves screen-level code, gives optimistic UX, and keeps the underlying data source as the API mocks (one source of truth, persisted to localStorage by `API.store`).

---

## 5 · Cache hydration

On first `RetailHooks.products.all()` call:

```js
let _hydrated = false;
async function _hydrate() {
  if (_hydrated) return;
  const [{ data: products }, { data: customers }, { data: suppliers }] = await Promise.all([
    API.catalog.products.list({ per_page: 1000 }),
    API.crm.customers.list({ per_page: 1000 }),
    API.crm.suppliers.list({ per_page: 1000 }),
  ]);
  _cache.products = products;
  _cache.customers = customers;
  _cache.suppliers = suppliers;
  _hydrated = true;
  emit('cache.hydrated');
}
```

Screens that mount before hydration completes get an empty array on first read; they re-render when `cache.hydrated` fires. To keep current behaviour while hydrating, **seed the cache synchronously on script load**:

```js
// At end of api-namespace.js seeding pass:
window.RetailHooks.__seed = {
  products: API.store.list('catalog.products') || [],
  customers: API.store.list('crm.customers') || [],
  // etc
};
```

`RetailHooks` reads `__seed` synchronously into `_cache`, so first render is correct.

---

## 6 · MOCK_MODE flag

After migration, add a top-level toggle:

```js
// front/api/api-foundation.js
window.API.MOCK_MODE = window.localStorage.getItem('api.mode') !== 'live';
```

When `MOCK_MODE = true`: `API.call()` routes to registered mocks (today's behaviour).
When `MOCK_MODE = false`: `API.call()` does real `fetch()` to `BASE_URL`.

**No screen-level changes required.** This is the payoff of the migration.

---

## 7 · Acceptance per phase

Each phase ships when:
1. All registered mocks have valid envelope shape (`{ data, meta }` or `ApiError`).
2. `RetailHooks.<namespace>` keeps its public signatures (no caller changes needed).
3. Screens using that namespace render unchanged in normal mode.
4. With tweak `simulateLatency=250ms`: skeletons appear on first load, then content.
5. With tweak `simulateError=NETWORK_ERROR`: error states render correctly.
6. localStorage persists data across reloads.
7. Verifier passes: `done` + `fork_verifier_agent` clean.

---

## 8 · Out of scope (explicitly)

- Real backend wiring — that's a `MOCK_MODE = false` flip, not part of this migration.
- HR sub-module — separate effort (todo #45).
- Pay / Dine / Shared modules — todos #18, #19, #20.
- Removing `window.DALSEEN_DATA` — keep as the seed source for `API.store` first-time-hydration.

---

## 9 · Execution order

1. **R1 Products** — start now.
2. **R2 Purchasing** — after R1 verified.
3. **R3 Inventory ops** — after R2.
4. **R4 Customer-facing** — after R3 (lower priority, can be deferred).
5. **R5 Derived** — auto-correct once R1–R3 done; only add API mocks if Cursor handoff demands it.

Cursor / next session can pick up from any phase boundary using the acceptance checklist in §7.
