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
#/merchantdashboard 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):
SAMPLEconstants fake offer counts, venue counts, recent offers, a 68% wave-accept rate, andbuildSampleActivity()fabricates the chart series. A venue owner shown this is being lied to. - Web
#/merchantdashboard: mock venues merged ahead of real ones, a "Select Your Merchant (Dev Mode)" picker,fakeAdswithMath.random()impressions/clicks/spend, a dead#/merchant/newlink, and a StatCard "change" pill that renders the literal string30d. - Web MerchantSignup: submits nowhere (a
setTimeoutthen "success"); nomerchant_applicationscollection exists. The form lies to applicants.
Goals (this theme) โ
- 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-apiGET /merchants/:id/offers; claims/redemptions from the existing ad-delivery rollup join. RemoveSAMPLEandbuildSampleActivity()entirely. - Web dashboard honesty pass: delete
mockVenues, the dev merchant picker, and the entirefakeAdsapparatus; replace the venues section with OWNED venues (from the same dashboard endpoint) incl. liveactiveLanternCount; 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. - MerchantSignup stops lying: persist applications to a new
merchant_applicationscollection 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 tousers/{uid}.merchantId): per-venue{venueId, name, lanterns, waves, uniqueUsers}+ totals. New client wrappers: adminanalyticsApi.js+ webmerchantMetricsService.js(copy themerchantDeliveryServicesoft-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; writesmerchant_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) โ
- Should merchant applications notify anyone (Discord webhook exists for other flows)? Deferred: admin portal list view is the follow-up.
- Peak-times display (risk doc mentions it):
getMerchantDashboarddoes not bucket by hour today. Candidate follow-up once real traffic exists; not fabricating it now.