update ui and remove mock data
This commit is contained in:
279
docs/API-INVENTORY.md
Normal file
279
docs/API-INVENTORY.md
Normal file
@@ -0,0 +1,279 @@
|
||||
# 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.
|
||||
|
||||
Reference in New Issue
Block a user