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

12 KiB
Raw Permalink Blame History

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:

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):

// 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:

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:

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.