const express = require('express'); const { requireAuth } = require('../middleware/auth'); const { createCheckoutSession, handleWebhookEvent, createPortalSession, getSubscriptionStatus, constructWebhookEvent, } = require('../services/stripeService'); const router = express.Router(); // Checkout — creates Stripe checkout session router.post('/checkout', requireAuth, async (req, res) => { const { tier, founder_code } = req.body; // Session 14 — 'africa' joins the validation whitelist. Whether // the checkout succeeds for 'africa' depends on STRIPE_PRICE_AFRICA // being set (see stripeService.PRICE_UNCONFIGURED handling); when // it isn't, the service throws a 503 the catch block surfaces. if (!tier || !['africa', 'analyst', 'desk'].includes(tier)) { return res.status(400).json({ error: 'tier must be "africa", "analyst" or "desk"' }); } try { const result = await createCheckoutSession(req.user.id, req.user.email, tier, founder_code); return res.json(result); } catch (err) { console.error('[VYNDR] Checkout error:', err.message); // Session 14 — surface the "tier valid but Stripe price not // provisioned yet" case with the explicit message + 503. This // path is what the Africa-tier user hits until // STRIPE_PRICE_AFRICA is configured in Coolify. if (err && err.code === 'tier_unconfigured') { return res.status(503).json({ error: err.message || 'Tier pricing not configured yet.', code: 'tier_unconfigured', }); } return res.status(503).json({ error: 'Checkout creation failed' }); } }); // Webhook — Stripe sends events here // IMPORTANT: This route needs raw body, not parsed JSON // Must be registered with express.raw() in app.js router.post('/webhook', async (req, res) => { const signature = req.headers['stripe-signature']; if (!signature) { return res.status(400).json({ error: 'Missing stripe-signature header' }); } let event; try { event = constructWebhookEvent(req.body, signature); } catch (err) { console.error('[VYNDR] Webhook signature failed:', err.message); return res.status(400).json({ error: 'Invalid signature' }); } try { await handleWebhookEvent(event); return res.json({ received: true }); } catch (err) { console.error('[VYNDR] Webhook handler error:', err.message); return res.status(500).json({ error: 'Webhook processing failed' }); } }); // Portal — subscription management router.post('/portal', requireAuth, async (req, res) => { if (!req.user.stripe_customer_id) { return res.status(400).json({ error: 'No active subscription' }); } try { const result = await createPortalSession(req.user.stripe_customer_id); return res.json(result); } catch (err) { console.error('[VYNDR] Portal error:', err.message); return res.status(503).json({ error: 'Portal creation failed' }); } }); // Status — current subscription info router.get('/status', requireAuth, async (req, res) => { try { let subStatus = { subscription_status: 'none', current_period_end: null, cancel_at_period_end: false }; if (req.user.stripe_customer_id) { subStatus = await getSubscriptionStatus(req.user.stripe_customer_id); } return res.json({ tier: req.user.tier, is_founder: req.user.founder_status, ...subStatus, }); } catch (err) { console.error('[VYNDR] Status error:', err.message); return res.status(503).json({ error: 'Status check failed' }); } }); module.exports = router;