105 lines
3.5 KiB
JavaScript
105 lines
3.5 KiB
JavaScript
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;
|