Files
loyaly_cutomerweb/docs/BEHAVISION-GAP-ANALYSIS.md
2026-09-25 16:31:10 +05:30

14 KiB
Raw Permalink Blame History

Behavision API ↔ Loyaly Merchant OS — gap analysis

Audit date: 2026-09-09 · API: https://mcp.loyaly.ai · Frontend: this repo

(Host corrected 2026-09-21: this line read https://platform.loyaly.ai, which serves this console, not the API. See src/shared/config/platformApi.ts:76-80.)


Headline finding

These are two different products.

This frontend was built as a loyalty & rewards console: LYT points, reward catalogues, engagement activities (spin / selfie / scratch / challenge / referral), campaigns, redemption liability, commerce orders.

Behavision is a camera-based footfall & visitor-recognition platform: sites, cameras, face templates, arrivals, visitor identity, footfall and conversion reports.

They overlap on roughly one third of the surface — the part that is genuinely about people arriving at a shop and buying something. The rest of the UI has no data source in this API and never will until a loyalty backend exists.

So "remove all hardcodes" has a consequence that needs a decision, not a guess: removing the fixtures makes about half the current UI go blank. §4 lays out the options.

The reverse is also true and more interesting: Behavision exposes a lot of real product this console does not surface at all — a visitor directory, live camera views, site health checks, an invitation/join flow, device management, GDPR erasure. See §5.


1. What maps — build these for real

Screen / panel Behavision endpoint Notes
Login POST /api/auth/login different token model — §3
Session restore GET /api/auth/me
Logout POST /api/auth/logout
Dashboard · Visitors KPI GET /api/reports/footfall use server total, never sum buckets
Dashboard · Purchases KPI GET /api/reports/conversion
Dashboard · Revenue KPI GET /api/reports/conversion
Dashboard · Footfall chart GET /api/reports/footfall?bucket=day
Dashboard · Revenue chart GET /api/reports/conversion?bucket=day
Dashboard · Visitors vs purchases both reports
Dashboard · Conversion chart GET /api/reports/conversion
Dashboard · Peak hours heatmap GET /api/reports/footfall?bucket=hour 7×24 grid from hourly buckets
Dashboard · Period rollup …?bucket=week|month
Dashboard · Store comparison GET /api/reports/footfall?site= per site
Dashboard · Recent activity feed GET /api/visits arrivals only — see below
Stores list GET /api/sites + fraction_below_gate, cameras up
Store detail GET /api/sites/{id}/check five-step smoke test
Settings · Team GET /api/team, PATCH /api/team/{id} replaces INITIAL_STAFF
Settings · Security → sessions GET/DELETE /api/auth/sessions, POST …/revoke-others replaces INITIAL_SESSIONS
Store switcher (STORE_OPTIONS) GET /api/sites critical — scopes every request

Customer journey maps partially and honestly:

stage source
Visit footfall total
Engage ✗ no source
Purchase conversion
Return footfall returning
Refer ✗ no source

Activity feed caveat. The current feed renders five event kinds (reward_redeemed, staff_checked_in, purchase, reward_expired, store_opened). /api/visits supplies arrivals only. Four of the five kinds have no source. The feed becomes an arrivals feed — which is arguably the better screen, and is what the spec calls "the screen a mobile app is for".


2. What does NOT map — no endpoint exists

Area Frontend surface Status
LYT programme entire /lyts page: rewards, claimed/used, redemption rate, outstanding liability, expiry alerts, reward usage chart, LYTs issued ✗ no rewards concept in the API
Activity intelligence walk / selfie / spin / scratch / brand / challenge / friend / shop / event, impact chains, attribution, activity grouping ✗ only visit has an analogue
Campaign performance campaign funnels ✗
Store insights "What needs your attention", AI briefing ✗
Commerce orders, products, categories, stock, payment methods, refunds, AOV, alerts ✗ except revenue/basket via conversion report; POST /api/purchases writes but there is no orders read
Staff module attendance (present/absent/late/leave), punctuality, sales per head, rewards issued, performance score ✗ different concept — /api/team is console user accounts, not shop-floor rostering
Settings · Profile business name, GSTIN, timezone, currency, LYTs per ₹100 ✗ no business-profile endpoint
Settings · Roles matrix per-permission grid ✗ role is a single enum via PATCH /api/team/{id}
Settings · Integrations connected apps ✗
Settings · API & Webhooks keys, endpoints, delivery logs ✗
Settings · Billing plan, invoices, payout account ✗
Settings · Security → audit log, 2FA ✗ (sessions do exist)
Loyaly AI chat panel ✗

3. Architectural changes required

These are not cosmetic. Each one has a defined failure mode.

3.1 Auth: cookie+HMAC → Bearer JWT with rotation

Today: proxy.ts gates every route on an httpOnly cookie carrying an HMAC-signed payload minted locally by sessionToken.ts. There is no upstream.

Behavision: access_token + refresh_token, both rotating.

Recommendation — keep the Next routes as a BFF (Backend-For-Frontend). Tokens live server-side; the browser keeps only the existing httpOnly cookie. This is not conservatism, it buys five specific things:

  1. proxy.ts and the whole SSR gate keep working unchanged.
  2. Tokens never reach JS, so an XSS cannot exfiltrate a refresh token.
  3. Web <img> cannot send Authorization. A BFF can proxy /api/faces/… and the browser just uses a normal <img src>. Otherwise every avatar needs fetch + createObjectURL + revoke-on-unmount.
  4. The single-flight refresh lock lives in one server process, not in N tabs.
  5. CORS and SameSite=None disappear as problems.

Three client rules the spec calls out — all must be implemented in the BFF:

  • 401 + "error": "token_expired" → refresh once, retry, silently. Any other 401 is a real sign-out.
  • Serialise refresh behind one lock. Refresh tokens are single-use; four concurrent panels would each spend it and three would lose. This dashboard fires 9 parallel requests on one page load — it will hit this on the first expiry, every time, without the lock.
  • Persist rotated tokens before using them. A crash between refresh and persist leaves a token the server has already invalidated.

Also: marshal the request body before the first attempt — a retry re-sends it, and a stream is spent after the first read.

3.2 Response envelope

Frontend expects {data, meta:{generatedAt, range, storeId}}. Behavision returns bare objects/arrays.

meta.generatedAt is load-bearing — every relative timestamp, the greeting, and every days-to-expiry countdown measures against the server clock, never Date.now(). The BFF must synthesise it (from polled_at, or the response time) or ~20 useResource call sites all need rewriting.

Wrap in the BFF. Cheapest correct option by a wide margin.

3.3 Scope parameters

?storeId=all&range=30d → ?site=<slug>&from=YYYY-MM-DD&to=YYYY-MM-DD&tz=…&bucket=…

  • storeId: 'all' → omit site entirely.
  • RangeKey → concrete from/to. to is inclusive.
  • Send tz; buckets come back as local wall time with no offset.
  • Use site (documented spelling). An unknown query param is silently ignored, so site_id in the wrong place returns the whole estate instead of an error.

3.4 Report arithmetic — three traps

  • Do not sum buckets to get the total. total is unique people over the window; someone who came Monday and Thursday is 1 person, 2 bucket-visitors. Show the server's total, visits underneath.
  • new + returning can be less than total — a site sending counts without templates records real footfall by an unidentified person.
  • Do not new Date() a bucket label. They are wall-time strings with no offset; parsing shifts every label into the viewer's zone. formatDayLabel() currently does new Date(iso) — it pins timeZone:'UTC' so it survives, but bucket labels should be passed through, not parsed.

3.5 Errors

Behavision: {"error": "invalid_code", "message": "…"} (flat). Frontend: {error: {code, message}} (nested), with codes internal|not_found|bad_request|unauthorized.

Map in the BFF. And show the server's message — it is written for humans; branch on error, never on the prose. New codes to handle: token_expired, too_many_attempts (429), last_owner (409), invalid_code (404), 501 = feature off for this deployment, not an error.

404 is also what another tenant's data returns — never surface it as "deleted".

3.6 Cursor pagination — new concept

GET /api/visits is cursor-based and lossless; polling by timestamp permanently skips rows when a burst exceeds limit. useResource has no cursor concept — the arrivals feed needs a cursor-aware hook.

  • Echo the returned cursor on every poll.
  • An empty poll returns your own cursor, not "".
  • A cursor that fails to parse → drop it, re-poll without one.
  • Delivery is at-least-once → de-duplicate on visit_id.
  • GET /api/visits/stream (SSE) is the low-latency path; needs an HTTP client that can set Authorization (browser EventSource cannot).

3.7 Images — new subsystem

  • Missing photo is data, not an error. Images are off by default across the product; available: false with a reason is the normal case. Render initials, never an error state.
  • auth: true → send Bearer. auth absent/false → presigned, use directly. Do not infer from the URL shape. Treat any relative URL as needing auth.
  • Every hand-out is written to the audit log. Fetch once per screen, not once per component — two components asking for one face puts two rows in "who looked at my customers" for one glance.
  • Web: object URL + revoke on unmount, or a screen left open all afternoon holds hundreds of copies of one photograph. (The BFF proxy in §3.1 avoids this entirely.)

3.8 Roles

UserRole = 'owner' | 'manager' | 'analyst' → 'staff' | 'manager' | 'owner'. analyst does not exist. Platform admin = role === 'admin' and empty client_id — the two together, never the role alone.

3.9 References instead of uuids

V-42, chennai, Office1, priya@tenext.in all work in paths and filters, and are immutable — safe to put in a URL or a saved report. Display names are not. The store switcher should key on site_slug, and deep links should use it.

Ambiguity resolves to nothing, not a guess. Unknown ref: 404 in a path, 400 in a filter.


4. The decision: what happens to the unmapped half

The API gives a clean idiom for this: 501 — "the feature is off for this deployment, not an error."

option result
A Delete the unmapped modules Smallest, most honest app. Lose /lyts, /commerce, activity intelligence, campaigns, insights, staff attendance, most of settings. Recoverable from git when a loyalty backend ships.
B Keep the UI, render "not available in this deployment" Nothing hardcoded, nothing invented, screens stay for when the backend arrives. Costs a small unavailable-state component.
C Keep fixtures behind an explicit DEMO_MODE flag Sales demos keep working; real deployments show B. Highest complexity.

My recommendation: B, with A for /commerce specifically — commerce is the one module with no seam at all (10 components import a service synchronously), so there is nothing to preserve, and conversion-report revenue can move onto the dashboard where it belongs.

Either way no invented number survives, which is what you asked for.


5. Real product this console is not exposing

Worth knowing before we decide what to delete — the API supports screens that do not exist here yet:

  • Visitor directory — GET /api/visitors?q= search by name/phone/V-42, /history, PUT /profile (name, phone, notes), and DELETE erasure (destroys face template + photo, keeps visits unlinked, irreversible; a 502 means nothing was deleted and must be reported as failure, never swallowed).
  • Live camera view — GET /api/cameras/{id}/live, SSE relayed from the shop PC.
  • Camera management — GET /api/cameras with latest still, PATCH /api/cameras/{id}.
  • Site health — GET /api/sites/{id}/check, five-step smoke test.
  • Invitation / join flow — mint a code, preview it unauthenticated, redeem it into a full session. Replaces the fake "Add Staff Member" form entirely.
  • Device management — the user's own signed-in devices, with current marked.
  • Arrivals SSE stream — the live feed.

6. Proposed sequence

  1. BFF + auth — Behavision login/refresh/logout/me behind the existing cookie; single-flight refresh; envelope + error adapters; scope→params mapper. Nothing else can be wired until this exists.
  2. Kill the highest-risk hardcode — STORE_OPTIONS → GET /api/sites.
  3. Reports — dashboard KPIs, footfall, conversion, peak hours, rollup, comparison.
  4. Arrivals — cursor-aware feed replacing the mock activity timeline.
  5. Team + Sessions + Invitations — replaces four fake settings screens with real ones.
  6. Apply the §4 decision to everything unmapped; delete src/features/*/mock/** and src/shared/mock/.
  7. Verify — typecheck, build, walk every screen against the live API.