'use strict'; /** * Partner attribution report (A1 Session 3). * * GET /api/partners/report/:code — internal-only (requireInternalAuth, * same key as the snapshot pipeline; never browser-reachable, no Next * proxy on purpose). Returns per-partner attribution: * * { ok, code, signups, conversions, mrr_attributed, note? } * * Data source: public.user_profiles filtered by `partner_ref` — the * code a signup carried in its metadata (set by the web PartnerRefCapture * → AuthContext.signUp flow). * * IMPORTANT — the `partner_ref` column does NOT exist on user_profiles * yet. Signup metadata lands in auth.users.raw_user_meta_data, which * PostgREST can't query. Until the TODO migration in docs/PARTNERS.md * is applied (adds the column + copies it from the signup metadata in * handle_new_user), the select errors and this endpoint HONESTLY * degrades to zeros with a `note` — it never fabricates attribution. * Once the migration runs, real numbers flow with no code change here. * * MRR attribution uses the REAL tier prices (founder_pricing-aware) on * currently-active paid profiles. It is monthly recurring revenue at * today's price book — not lifetime value, not a projection. */ const express = require('express'); const { requireInternalAuth } = require('../middleware/internalAuth'); const { getSupabaseServiceClient } = require('../utils/supabase'); const router = express.Router(); router.use(requireInternalAuth({ loopbackOnly: false })); // Tier price book (matches stripeService / pricing page). const TIER_MRR = { analyst: { standard: 19.99, founder: 14.99 }, desk: { standard: 49.99, founder: 34.99 }, }; /** Same convention as web/src/lib/partnerRef.js — A-Z 0-9 - _, ≤32, uppercase. */ function sanitizePartnerCode(raw) { const s = String(raw == null ? '' : raw).trim().toUpperCase(); if (!s || s.length > 32) return null; return /^[A-Z0-9_-]+$/.test(s) ? s : null; } /** * Pure report math over user_profiles rows * ({ tier, subscription_status, founder_pricing }). * signups — every profile attributed to the code * conversions — profiles currently on an active paid tier * mrr_attributed — sum of those profiles' monthly price (founder-aware) */ function buildPartnerReport(rows) { const list = Array.isArray(rows) ? rows : []; let conversions = 0; let mrr = 0; for (const r of list) { if (!r) continue; const tier = String(r.tier || 'free').toLowerCase(); const active = String(r.subscription_status || '') === 'active'; const prices = TIER_MRR[tier]; if (!prices || !active) continue; conversions += 1; mrr += r.founder_pricing === true ? prices.founder : prices.standard; } return { signups: list.length, conversions, mrr_attributed: Math.round(mrr * 100) / 100, }; } // Injectable for tests (no network / no Supabase env needed). let _getClient = getSupabaseServiceClient; function _setClientForTests(fn) { _getClient = typeof fn === 'function' ? fn : getSupabaseServiceClient; } router.get('/report/:code', async (req, res) => { const code = sanitizePartnerCode(req.params.code); if (!code) { return res.status(400).json({ ok: false, error: 'invalid partner code' }); } try { const supabase = _getClient(); const { data, error } = await supabase .from('user_profiles') .select('tier, subscription_status, founder_pricing') .eq('partner_ref', code); if (error) { // Column not migrated yet (docs/PARTNERS.md TODO) — zeros, never invented. return res.json({ ok: true, code, signups: 0, conversions: 0, mrr_attributed: 0, note: 'partner_ref is not queryable yet — apply the TODO migration in docs/PARTNERS.md to activate attribution', }); } return res.json({ ok: true, code, ...buildPartnerReport(data) }); } catch (err) { const message = err && err.message ? err.message : String(err); console.error('[partners/report] failed:', message); return res.status(500).json({ ok: false, error: message }); } }); module.exports = router; module.exports.sanitizePartnerCode = sanitizePartnerCode; module.exports.buildPartnerReport = buildPartnerReport; module.exports._setClientForTests = _setClientForTests;