Files
loyaly_cutomerweb/docs/API-INVENTORY.md
2026-09-25 16:31:10 +05:30

280 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Loyaly Merchant OS — API inventory & backend wiring plan
Audit date: 2026-09-09 · Branch `main` · Audited against the running app on :3100
Purpose: establish exactly what data the frontend consumes today, which of it is
fake, and what a real backend must expose. Compare this against your API spec and
mark each row **match / rename / missing / extra**.
---
## 1. How data flows today
```
component → hook (useResource) → repository (owns the URL) → httpClient
│
NEXT_PUBLIC_API_BASE ────┤
│
(unset) → Next route handler → fixture generator
(set) → YOUR BACKEND
```
**The seam already exists and works.** `src/shared/services/httpClient.ts` line 22:
```ts
const BASE = process.env.NEXT_PUBLIC_API_BASE ?? '';
```
Set that to your API host and every endpoint in §2 bypasses the local route
handlers entirely. No component changes. This is the intended cutover.
**Response envelope** — every endpoint must return this shape (`src/shared/types/api.ts`):
```jsonc
// success
{ "data": <T>, "meta": { "generatedAt": "ISO-8601", "range": "30d", "storeId": "all" } }
// failure (any non-2xx, or 2xx with error present)
{ "error": { "code": "internal|not_found|bad_request|unauthorized", "message": "…" } }
```
`meta.generatedAt` is **required**: the UI measures every relative time
("2 hours ago", days-to-expiry, the greeting) against the server clock, never
`Date.now()`.
**Scope query** — every analytics endpoint is filtered by the same two params:
| param | values |
|---|---|
| `storeId` | `all` \| a store id |
| `range` | `7d` \| `30d` \| `90d` \| `mtd` \| `ytd` \| `custom` |
**Auth** — session is an httpOnly cookie. `credentials: 'same-origin'` is sent on
every request. A cross-origin backend needs CORS + `SameSite=None; Secure`, or
keep the Next routes as a thin proxy.
---
## 2. Endpoints that EXIST (24) — all fixture-backed
Legend: **T** = TypeScript response type, defined in the file noted.
### Auth — `src/features/auth/types/auth.ts`
| Method | Path | Body / Query | Returns |
|---|---|---|---|
| POST | `/api/auth/login` | `{email, password, rememberMe}` (JSON **and** form-encoded) | `AuthSession` |
| POST | `/api/auth/logout` | `{}` | `{ok: boolean}` |
| GET | `/api/auth/session` | — | `AuthSession \| null` |
`AuthSession = {user: {id, email, name, role: 'owner'|'manager'|'analyst', organisation}, expiresAt}`
### Dashboard — `src/features/dashboard/types/dashboard.ts` + `intelligence.ts`
| Method | Path | Extra query | Returns |
|---|---|---|---|
| GET | `/api/dashboard/kpis` | scope | `Kpi[]` |
| GET | `/api/dashboard/timeseries` | scope | `TimePoint[]` |
| GET | `/api/dashboard/peak-hours` | scope | `HourCell[]` |
| GET | `/api/dashboard/activity` | scope | `ActivityEvent[]` |
| GET | `/api/dashboard/store-comparison` | scope | `StoreComparison[]` |
| GET | `/api/dashboard/reward-usage` | scope | `RewardUsagePoint[]` |
| GET | `/api/dashboard/performance` | scope + `granularity=weekly\|monthly` | `PeriodPoint[]` |
| GET | `/api/dashboard/briefing` | scope | `DashboardBriefing` |
| GET | `/api/dashboard/activity-metrics` | scope | `ActivityMetric[]` |
| GET | `/api/dashboard/journey` | scope | `JourneyStage[]` |
| GET | `/api/dashboard/campaigns` | scope | `CampaignSummary[]` |
| GET | `/api/dashboard/insights` | scope | `Insight[]` |
Key shapes:
```ts
Kpi { id:'visitors'|'purchases'|'revenue'|'activeRewards', label, value,
unit:'count'|'inr'|'lyt'|'pct', deltaPct, isRiseGood, trend:{t,v}[] }
TimePoint { t:'YYYY-MM-DD', visitors, purchases, revenue, conversion }
HourCell { day:0-6 (0=Mon), hour:0-23, value }
ActivityEvent{ id, at:ISO, kind:'reward_redeemed'|'staff_checked_in'|'purchase'
|'reward_expired'|'store_opened', title, detail?, storeId }
PeriodPoint { label:'W32'|'Aug', visitors, purchases, revenue }
Insight { id, severity:'info'|'success'|'warning'|'error', title, body,
action?:{label, href} }
```
**Activity intelligence** (`intelligence.ts`) — the model shared by Dashboard and Lyts:
```ts
ActivityMetric {
id: 'walk'|'visit'|'selfie'|'spin'|'scratch'|'brand'|'challenge'|'friend'|'shop'|'event'
label, description
group: 'engagement'|'growth'|'commerce'
accent: 'warm'|'cool' // fixed per activity, travels on the payload
count, deltaPct
status: 'live'|'paused'|'draft'
lytsIssued // MEASURED (ledger). 1 LYT = ₹1
isFeatured // the 6 the dashboard summarises
impact: {
customers // MEASURED
rewardClaims? // omitted, never 0, when activity grants none
repeatVisits, purchases // ATTRIBUTED
attributedRevenueInr // ATTRIBUTED — never name this `revenue`
attribution: 'estimated'|'observed'
}
}
JourneyStage { id:'visit'|'engage'|'purchase'|'return'|'refer', label, value, conversionPct? }
CampaignSummary { id, name, activityId, accent, status:'live'|'ended'|'scheduled',
steps:{label,value}[], attributedRevenueInr, attribution }
```
> **Contract rule.** `attribution` is not decorative. When it is `'estimated'` the
> UI shows a disclosure ("Attribution is estimated…") beside every attributed
> figure; when your backend can join purchases to activity events, return
> `'observed'` and the disclosure disappears with no frontend change.
> Invariants the UI assumes: `purchases ≤ repeatVisits ≤ customers ≤ count`.
### Lyts — `src/features/lyts/types/reward.ts`
| Method | Path | Returns |
|---|---|---|
| GET | `/api/lyts/rewards` | `Reward[]` |
| GET | `/api/lyts/redemptions` | `TimePoint[]` ⚠️ |
| GET | `/api/lyts/activity` | `ActivityEvent[]` |
`Reward { id, name, costLyt, claimed, used, expiresAt: ISO|null, status: 'active'|'paused'|'expiring'|'expired' }`
⚠️ **Known contract smell:** redemptions reuses `TimePoint`, and the Lyts charts
read `visitors` as "Issued" and `purchases` as "Redeemed". Your backend should
return a purpose-built shape — `{t, issued, redeemed}` — and I'll update the two
chart call sites.
### Stores — `src/features/stores/types/store.ts`
| Method | Path | Returns |
|---|---|---|
| GET | `/api/stores` | `Store[]` |
| GET | `/api/stores/:storeId` | `Store` (404 → `not_found`) |
`Store { id, name, status:'open'|'closed'|'maintenance', visitors, purchases, revenueInr, conversionPct, staffCount }`
### Staff — `src/features/staff/types/staff.ts`
| Method | Path | Returns |
|---|---|---|
| GET | `/api/staff` | `StaffMember[]` |
| GET | `/api/staff/summary` | `StaffSummary` |
| GET | `/api/staff/attendance` | `AttendancePoint[]` |
### Settings — `src/features/settings/types/settings.ts`
| Method | Path | Returns |
|---|---|---|
| GET | `/api/settings/profile` | `MerchantProfile` |
| PATCH | `/api/settings/profile` | `MerchantProfile` (merged) — **does not persist today** |
---
## 3. Endpoints that DO NOT EXIST — must be created
These screens are live in the UI with **no API, no repository, and no persistence**.
Mutations are `useState` only: refresh the page and every change is gone.
### 3a. Commerce — the worst-wired module
`/commerce` imports `commerceService.ts` **directly and synchronously** from 10
component files. No repository, no route handler, no loading or error state.
Needed:
| Method | Path | Returns |
|---|---|---|
| GET | `/api/commerce/kpis` | revenue, orders, AOV, net sales, refunds, conversion (value + delta each) |
| GET | `/api/commerce/revenue-trend` | `{t, revenue, target}[]` |
| GET | `/api/commerce/orders-trend` | `{t, orders}[]` |
| GET | `/api/commerce/payment-breakdown` | `{method, amount, sharePct}[]` |
| GET | `/api/commerce/store-performance` | `{storeId, name, revenueInr}[]` |
| GET | `/api/commerce/top-products` | `{id, name, category, unitsSold, revenueInr, growthPct, stockCount, stockStatus}[]` |
| GET | `/api/commerce/alerts` | `{id, severity, title, body}[]` |
| GET | `/api/commerce/orders` | recent orders feed |
### 3b. Settings — 7 screens, all local-state fakes
| Screen | Needed endpoints |
|---|---|
| Team & Staff | `GET/POST/PATCH/DELETE /api/settings/team` (+ suspend, password reset, invite) |
| Stores | `GET/POST/PATCH/DELETE /api/settings/stores` |
| Roles & Permissions | `GET/PUT /api/settings/roles` (permission matrix) |
| Integrations | `GET /api/settings/integrations`, `POST/DELETE .../:id/connection` |
| API & Webhooks | `GET/POST/DELETE /api/settings/api-keys`, `GET/POST/DELETE /api/settings/webhooks`, `GET /api/settings/webhooks/logs` |
| Security | `GET /api/settings/sessions`, `DELETE /api/settings/sessions/:id`, `GET /api/settings/audit-log`, `POST /api/auth/password`, `POST/DELETE /api/auth/2fa` |
| Billing | `GET /api/settings/billing`, `GET /api/settings/invoices`, `PATCH /api/settings/payout-account` |
### 3c. Loyaly AI
The chat panel streams from `services/ai/mockAi.ts` — a local prompt classifier
picking canned templates. `loyalyAiRepository` is the only repository in the app
that imports a mock directly.
Needed: `POST /api/ai/chat` (streaming — SSE or chunked), plus
`GET/POST/DELETE /api/ai/conversations` if history is to survive reload.
### 3d. Store switcher — **critical**
`STORE_OPTIONS` is a **hardcoded array in `src/shared/providers/WorkspaceProvider.tsx`**
(line 45), duplicated from the fixture roster. This array scopes *every request in
the app*. It must be fed from `GET /api/stores`, or a real merchant sees fixture
store names in the global switcher.
---
## 4. Hardcoded / dummy data inventory
| Location | What | Removal |
|---|---|---|
| `src/features/*/mock/*.ts` (11 files, ~1,970 lines) | all fixture generators | delete at cutover |
| `src/shared/mock/rng.ts` | seeded RNG | delete at cutover |
| `src/features/commerce/services/commerceService.ts` | KPIs, trends, products, alerts, store multipliers | replace with repository |
| `src/features/settings/components/*.tsx` × 7 | `INITIAL_STAFF`, `INITIAL_STORES`, `INITIAL_MATRIX`, `INITIAL_APPS`, `INITIAL_KEYS`, `INITIAL_WEBHOOKS`, `WEBHOOK_LOGS`, `INITIAL_SESSIONS`, `AUDIT_LOGS`, `INVOICES` | replace with hooks |
| `src/shared/providers/WorkspaceProvider.tsx` | `STORE_OPTIONS` | feed from `/api/stores` |
| `src/features/loyaly-ai/services/ai/**` (~570 lines) | prompt router + reply templates | replace with real AI endpoint |
| `src/features/auth/mock/users.mock.ts` | 5 users, **plaintext passwords** | replace `verifyCredentials()` body |
| `src/app/api/**/route.ts` (24 files) | fixture wiring + `?_state=` simulation | delete or keep as proxy — see §6 |
**Auth note.** `verifyCredentials()` is already the single credential seam — its
body becomes an HTTP call and nothing else changes. But it currently returns
`unknown_email` vs `wrong_password` separately, which is a **user-enumeration
oracle**. Collapse both to one message at the route when you go live.
`AUTH_SECRET` must be set in production (`sessionToken.ts` throws without it).
---
## 5. What I recommend NOT doing yet
Deleting the fixtures before the real endpoints exist leaves every screen in an
error state and removes the only way to verify the wiring. The fixtures are also
the **executable spec** — `intelligence.mock.ts` encodes the funnel invariants
your backend has to honour.
Order that keeps the app working at every step:
1. **Now (backend-independent):** add the missing seams — commerce repository +
route, settings hooks + routes, `STORE_OPTIONS` from `/api/stores`, AI
repository seam. Fixtures stay behind them. Every module then has one file to
swap.
2. **You send the API spec MD.** I diff it against §2/§3 and report
match / rename / missing / extra per endpoint.
3. **Cutover:** point `NEXT_PUBLIC_API_BASE` at the real host, adapt any shape
mismatches in the repository layer only, delete `mock/` + `src/app/api/**`.
4. **Verify:** typecheck, build, and walk every screen with the network tab.
---
## 6. Decision needed from you
**Do the Next.js route handlers stay?**
- **A — Direct:** frontend calls your backend. Delete `src/app/api/**`. Needs CORS
and a cross-origin-safe session cookie.
- **B — Proxy (recommended):** keep the route handlers, replace each fixture call
with a `fetch` to your backend. Session cookie stays first-party, your API host
is never exposed to the browser, and the `?_state=error|empty|loading` dev
harness keeps working.