Skip to content

Merchant Surfaces on Live Data (Design) โ€‹

Date: 2026-07-25 Status: Draft; implementation proceeding (operator granted full autonomy for this theme)

Revision 2026-07-26 (operator decision, D28): the web #/merchant dashboard is REMOVED outright rather than kept as an honesty-passed viewer. The merchant portal is canonical in the ADMIN app's MerchantShell, and a second portal surface in the web app kept confusing both agents and humans. Goal 2 below is superseded accordingly; the web app keeps only the public application funnel (#/merchant/signup, goal 3). Issues: Part of #178 (pilot integration) and the risk-doc mandate; #320/#167 analytics stay future scope. Related shipped work: #697/#703 (ad delivery rollup), #705 (claim/redeem). Plan: ../plans/2026-07-25-merchant-live-data.md

Problem โ€‹

The June risk assessment calls the merchant portal "the storefront the first paying customer judges" and mandates wiring it to the 4-5 renewal-earning numbers: lanterns lit at their venue, waves, peak times, redemptions, with obvious accuracy. Since then the ad-delivery rollup landed (web dashboard headline metrics are live) and the operator declared the ADMIN MerchantShell the canonical merchant page. What remains is a credibility problem on both surfaces:

  • Admin MerchantShell Overview (canonical): SAMPLE constants fake offer counts, venue counts, recent offers, a 68% wave-accept rate, and buildSampleActivity() fabricates the chart series. A venue owner shown this is being lied to.
  • Web #/merchant dashboard: mock venues merged ahead of real ones, a "Select Your Merchant (Dev Mode)" picker, fakeAds with Math.random() impressions/clicks/spend, a dead #/merchant/new link, and a StatCard "change" pill that renders the literal string 30d.
  • Web MerchantSignup: submits nowhere (a setTimeout then "success"); no merchant_applications collection exists. The form lies to applicants.

Goals (this theme) โ€‹

  1. Admin MerchantShell Overview goes fully live: owned venues + lanterns + waves + unique users from the (currently unused) GET /analytics/merchant/dashboard; offer counts + recent offers from merchants-api GET /merchants/:id/offers; claims/redemptions from the existing ad-delivery rollup join. Remove SAMPLE and buildSampleActivity() entirely.
  2. Web dashboard honesty pass: delete mockVenues, the dev merchant picker, and the entire fakeAds apparatus; replace the venues section with OWNED venues (from the same dashboard endpoint) incl. live activeLanternCount; fix the dead create-offer link (routes to the admin portal's existing create-offer flow, not a new web screen); StatCard pill becomes an honest period label.
  3. MerchantSignup stops lying: persist applications to a new merchant_applications collection via a real endpoint (merchants-api), server-only reads (admin reviews in the portal later; out of scope here beyond persistence + confirmation state).

Non-goals (deferred, tracked) โ€‹

  • Spend/budget-consumption metrics (no billing source exists; ad-network economics deferred by the risk doc).
  • Period-over-period deltas and wave-accept rate (no data source; DROP the fake stat rather than build trend infra now).
  • Interest/vibe insights (#320), advanced analytics + community (#167), a web create-offer screen (admin owns offer CRUD).
  • Realtime auto-refresh; manual refresh + load-time freshness is enough for the pilot.

Design โ€‹

Data flow (all existing endpoints; zero new metric surfaces server-side) โ€‹

  • GET /analytics/merchant/dashboard?period= (analytics-api, requireRole('merchant'), scope pinned to users/{uid}.merchantId): per-venue {venueId, name, lanterns, waves, uniqueUsers} + totals. New client wrappers: admin analyticsApi.js + web merchantMetricsService.js (copy the merchantDeliveryService soft-fail pattern: {ok, data}, never throw into render).
  • GET /merchants/:merchantId/offers (merchants-api): real offer list -> counts by status + recent offers.
  • Existing ad-delivery rollup join keeps powering impressions/clicks/claims/redemptions.
  • Web owned-venue cards read venues/{id}.activeLanternCount (public, server-authoritative) for "lit right now".
  • POST /merchants/apply (NEW, merchants-api): App-Check + auth, rate-limited; writes merchant_applications/{autoId} {userId, businessName, contactEmail, venueName, message?, status: 'pending', createdAt}. Firestore rules: client read/write false (server-only, same posture as offerClaims). Email stays out of any client-readable surface.

Honesty rules (the point of the theme) โ€‹

  • No metric renders a fabricated number, ever. Unavailable -> explicit "not available yet" state (the delivery service's suppressed/unavailable states are the pattern).
  • Stats with no source (wave accept rate, spend, deltas) are REMOVED from the UI, not zeroed or randomized.
  • The activity chart renders from real per-venue/per-day data or does not render.

Key decisions โ€‹

Logged in the plan's decision log (D20+). Headlines: canonical surface first (admin), web gets an honesty pass not a rebuild; create-offer stays admin-owned; applications persist server-only.

Open questions (not blocking) โ€‹

  1. Should merchant applications notify anyone (Discord webhook exists for other flows)? Deferred: admin portal list view is the follow-up.
  2. Peak-times display (risk doc mentions it): getMerchantDashboard does not bucket by hour today. Candidate follow-up once real traffic exists; not fabricating it now.

Built with VitePress