const express = require('express'); const router = express.Router(); const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); const db = require('../../database/db'); const notificationService = require('../../services/notificationService'); /** * Stripe webhook endpoint * Handles subscription lifecycle events * * Note: This endpoint uses express.raw() middleware for signature verification * Set this up in server.js before the main JSON body parser */ router.post('/', express.raw({ type: 'application/json' }), async (req, res) => { const sig = req.headers['stripe-signature']; let event; try { // Verify webhook signature event = stripe.webhooks.constructEvent( req.body, sig, process.env.STRIPE_WEBHOOK_SECRET ); } catch (err) { console.error('⚠️ Webhook signature verification failed:', err.message); return res.status(400).send(`Webhook Error: ${err.message}`); } console.log(`✅ Webhook received: ${event.type}`); // Handle event try { switch (event.type) { case 'customer.subscription.created': await handleSubscriptionCreated(event.data.object); break; case 'customer.subscription.updated': await handleSubscriptionUpdated(event.data.object); break; case 'customer.subscription.deleted': await handleSubscriptionDeleted(event.data.object); break; case 'invoice.payment_succeeded': await handlePaymentSucceeded(event.data.object); break; case 'invoice.payment_failed': await handlePaymentFailed(event.data.object); break; case 'customer.subscription.trial_will_end': await handleTrialWillEnd(event.data.object); break; default: console.log(`Unhandled event type: ${event.type}`); } res.json({ received: true }); } catch (error) { console.error('Error processing webhook:', error); res.status(500).json({ error: 'Webhook processing failed' }); } }); /** * Handle subscription created */ async function handleSubscriptionCreated(subscription) { console.log(`[Webhook] Subscription created: ${subscription.id}`); // Subscription is already created in premiumService.subscribe() // This webhook just confirms it } /** * Handle subscription updated */ async function handleSubscriptionUpdated(subscription) { console.log(`[Webhook] Subscription updated: ${subscription.id}`); await db.query( `UPDATE premium_subscriptions SET current_period_end = to_timestamp($2), cancel_at_period_end = $3, status = $4, updated_at = NOW() WHERE stripe_subscription_id = $1`, [ subscription.id, subscription.current_period_end, subscription.cancel_at_period_end, subscription.status ] ); // Update domain expiration if applicable await db.query( `UPDATE blockchain_domains SET expires_at = to_timestamp($2) WHERE owner_user_id IN ( SELECT user_id FROM premium_subscriptions WHERE stripe_subscription_id = $1 )`, [subscription.id, subscription.current_period_end] ); } /** * Handle subscription deleted/cancelled */ async function handleSubscriptionDeleted(subscription) { console.log(`[Webhook] Subscription deleted: ${subscription.id}`); // Update subscription status await db.query( `UPDATE premium_subscriptions SET status = 'cancelled', cancelled_at = NOW(), updated_at = NOW() WHERE stripe_subscription_id = $1`, [subscription.id] ); // Downgrade user to free tier const subResult = await db.query( `SELECT user_id FROM premium_subscriptions WHERE stripe_subscription_id = $1`, [subscription.id] ); if (subResult.rows.length > 0) { const userId = subResult.rows[0].user_id; await db.query( `UPDATE users SET premium_tier = 'free' WHERE id = $1`, [userId] ); console.log(`[Webhook] User ${userId} downgraded to free tier`); // Send notification to user about downgrade await notificationService.notifySubscriptionDowngrade(userId); } } /** * Handle successful payment */ async function handlePaymentSucceeded(invoice) { console.log(`[Webhook] Payment succeeded for invoice: ${invoice.id}`); // Log the transaction if (invoice.customer && invoice.subscription) { const subResult = await db.query( `SELECT user_id FROM premium_subscriptions WHERE stripe_subscription_id = $1`, [invoice.subscription] ); if (subResult.rows.length > 0) { await db.query( `INSERT INTO payment_transactions (user_id, transaction_type, amount_usd, stripe_payment_intent_id, stripe_invoice_id, status) VALUES ($1, 'subscription_renewal', $2, $3, $4, 'succeeded')`, [ subResult.rows[0].user_id, invoice.amount_paid / 100, // Convert from cents invoice.payment_intent, invoice.id ] ); console.log(`[Webhook] Payment logged for user ${subResult.rows[0].user_id}`); // Send receipt email to user const amount = invoice.amount_paid / 100; await notificationService.notifyPaymentSuccess( subResult.rows[0].user_id, amount, invoice.id ); } } } /** * Handle failed payment */ async function handlePaymentFailed(invoice) { console.log(`[Webhook] Payment failed for invoice: ${invoice.id}`); // Log failed transaction if (invoice.customer && invoice.subscription) { const subResult = await db.query( `SELECT user_id FROM premium_subscriptions WHERE stripe_subscription_id = $1`, [invoice.subscription] ); if (subResult.rows.length > 0) { const userId = subResult.rows[0].user_id; await db.query( `INSERT INTO payment_transactions (user_id, transaction_type, amount_usd, stripe_payment_intent_id, stripe_invoice_id, status) VALUES ($1, 'subscription_renewal', $2, $3, $4, 'failed')`, [ userId, invoice.amount_due / 100, invoice.payment_intent, invoice.id ] ); console.log(`[Webhook] Failed payment logged for user ${userId}`); // Send notification to user about failed payment const amount = invoice.amount_due / 100; const reason = invoice.last_payment_error?.message || 'Unknown error'; await notificationService.notifyPaymentFailed(userId, amount, reason); } } } /** * Handle trial ending soon */ async function handleTrialWillEnd(subscription) { console.log(`[Webhook] Trial ending soon for subscription: ${subscription.id}`); const subResult = await db.query( `SELECT user_id FROM premium_subscriptions WHERE stripe_subscription_id = $1`, [subscription.id] ); if (subResult.rows.length > 0) { const userId = subResult.rows[0].user_id; // Calculate days remaining in trial const trialEnd = new Date(subscription.trial_end * 1000); const now = new Date(); const daysRemaining = Math.ceil((trialEnd - now) / (1000 * 60 * 60 * 24)); // Send notification to user that trial is ending await notificationService.notifyTrialEnding(userId, daysRemaining); console.log(`[Webhook] Trial ending notification sent to user ${userId}`); } } module.exports = router;