import express from 'express'; import { basicAuthMiddleware } from '../middlewares/auth.js'; import { getStripeClient, createCheckoutSession, createCustomerPortalSession, cancelSubscription, reactivateSubscription, verifyWebhookSignature, } from '../services/stripe.js'; import { getUser, updateUserSubscription, getUserSubscription, } from '../services/mongodb.js'; import { getPlan } from '../config/subscriptionPlans.js'; const router = express.Router(); // Middleware de autenticación opcional (intenta autenticar pero no falla si no hay token) async function optionalAuthMiddleware(req, res, next) { const authHeader = req.headers.authorization; if (authHeader) { // Si hay header de autorización, intentar autenticar return basicAuthMiddleware(req, res, next); } else { // Si no hay header, continuar sin autenticación req.user = null; next(); } } // Crear sesión de checkout (permite autenticación opcional) // Puede ser llamado por usuarios autenticados o durante el registro router.post('/create-checkout-session', optionalAuthMiddleware, async (req, res) => { try { const stripeClient = getStripeClient(); if (!stripeClient) { return res.status(503).json({ error: 'Pagos no disponibles', message: 'El sistema de pagos no está configurado actualmente' }); } const { planId, billingPeriod, username: providedUsername, email: providedEmail } = req.body; // Username puede venir del token (usuario autenticado) o del body (registro nuevo) const username = req.user?.username || providedUsername; if (!username) { return res.status(400).json({ error: 'Se requiere username (autenticación o en el body)' }); } if (!planId || !billingPeriod) { return res.status(400).json({ error: 'planId y billingPeriod son requeridos' }); } if (planId === 'free') { return res.status(400).json({ error: 'El plan gratuito no requiere pago' }); } if (!['monthly', 'yearly'].includes(billingPeriod)) { return res.status(400).json({ error: 'billingPeriod debe ser monthly o yearly' }); } // Obtener usuario const user = await getUser(username); if (!user) { return res.status(404).json({ error: 'Usuario no encontrado' }); } // Email puede venir del usuario en BD, del body, o generar uno const email = user.email || providedEmail || `${username}@wallabicher.local`; // Crear sesión de checkout const session = await createCheckoutSession({ planId, billingPeriod, email, userId: username, }); console.log(`✅ Sesión de checkout creada para ${username}: ${planId} (${billingPeriod})`); res.json({ success: true, sessionId: session.id, url: session.url, }); } catch (error) { console.error('Error creando sesión de checkout:', error); res.status(500).json({ error: error.message, message: 'Error al crear la sesión de pago' }); } }); // Crear portal del cliente router.post('/create-portal-session', basicAuthMiddleware, async (req, res) => { try { const stripeClient = getStripeClient(); if (!stripeClient) { return res.status(503).json({ error: 'Pagos no disponibles', message: 'El sistema de pagos no está configurado actualmente' }); } const username = req.user.username; const subscription = await getUserSubscription(username); if (!subscription || !subscription.stripeCustomerId) { return res.status(404).json({ error: 'No se encontró información de cliente de Stripe' }); } const session = await createCustomerPortalSession(subscription.stripeCustomerId); res.json({ success: true, url: session.url, }); } catch (error) { console.error('Error creando portal de cliente:', error); res.status(500).json({ error: error.message, message: 'Error al crear el portal de gestión' }); } }); // Cancelar suscripción router.post('/cancel-subscription', basicAuthMiddleware, async (req, res) => { try { const stripeClient = getStripeClient(); if (!stripeClient) { return res.status(503).json({ error: 'Pagos no disponibles', message: 'El sistema de pagos no está configurado actualmente' }); } const username = req.user.username; const subscription = await getUserSubscription(username); if (!subscription || !subscription.stripeSubscriptionId) { return res.status(404).json({ error: 'No se encontró suscripción activa' }); } await cancelSubscription(subscription.stripeSubscriptionId); // Actualizar en BD await updateUserSubscription(username, { ...subscription, cancelAtPeriodEnd: true, }); console.log(`✅ Suscripción cancelada para ${username}`); res.json({ success: true, message: 'Suscripción cancelada. Se mantendrá activa hasta el final del período' }); } catch (error) { console.error('Error cancelando suscripción:', error); res.status(500).json({ error: error.message, message: 'Error al cancelar la suscripción' }); } }); // Reactivar suscripción router.post('/reactivate-subscription', basicAuthMiddleware, async (req, res) => { try { const stripeClient = getStripeClient(); if (!stripeClient) { return res.status(503).json({ error: 'Pagos no disponibles', message: 'El sistema de pagos no está configurado actualmente' }); } const username = req.user.username; const subscription = await getUserSubscription(username); if (!subscription || !subscription.stripeSubscriptionId) { return res.status(404).json({ error: 'No se encontró suscripción' }); } await reactivateSubscription(subscription.stripeSubscriptionId); // Actualizar en BD await updateUserSubscription(username, { ...subscription, cancelAtPeriodEnd: false, }); console.log(`✅ Suscripción reactivada para ${username}`); res.json({ success: true, message: 'Suscripción reactivada correctamente' }); } catch (error) { console.error('Error reactivando suscripción:', error); res.status(500).json({ error: error.message, message: 'Error al reactivar la suscripción' }); } }); // Webhook de Stripe (sin autenticación) // ⚠️ NOTA: express.raw() se aplica en server.js para esta ruta // De lo contrario el body ya viene parseado y Stripe no puede verificar la firma router.post('/webhook', async (req, res) => { const signature = req.headers['stripe-signature']; if (!signature) { return res.status(400).json({ error: 'No stripe-signature header' }); } let event; try { // Verificar firma del webhook event = verifyWebhookSignature(req.body, signature); } catch (error) { console.error('Error verificando webhook:', error.message); return res.status(400).json({ error: `Webhook verification failed: ${error.message}` }); } console.log(`📩 Webhook recibido: ${event.type}`); // Manejar eventos de Stripe try { switch (event.type) { case 'checkout.session.completed': await handleCheckoutComplete(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; default: console.log(`ℹ️ Evento no manejado: ${event.type}`); } res.json({ received: true }); } catch (error) { console.error('Error procesando webhook:', error); res.status(500).json({ error: error.message }); } }); // Handlers de eventos de Stripe async function handleCheckoutComplete(session) { const userId = session.metadata.userId || session.client_reference_id; const planId = session.metadata.planId; const billingPeriod = session.metadata.billingPeriod; if (!userId) { console.error('❌ Checkout sin userId en metadata'); return; } console.log(`✅ Pago completado: ${userId} → ${planId} (${billingPeriod})`); // Obtener detalles de la suscripción const subscriptionId = session.subscription; const customerId = session.customer; const plan = getPlan(planId); const now = new Date(); let periodEnd = new Date(now); if (billingPeriod === 'yearly') { periodEnd.setFullYear(periodEnd.getFullYear() + 1); } else { periodEnd.setMonth(periodEnd.getMonth() + 1); } // Actualizar suscripción en BD await updateUserSubscription(userId, { planId, status: 'active', billingPeriod, currentPeriodStart: now, currentPeriodEnd: periodEnd, cancelAtPeriodEnd: false, stripeCustomerId: customerId, stripeSubscriptionId: subscriptionId, }); // Activar usuario si estaba pendiente de pago const { getDB } = await import('../services/mongodb.js'); const db = getDB(); if (db) { const usersCollection = db.collection('users'); const user = await usersCollection.findOne({ username: userId }); if (user && user.status === 'pending_payment') { await usersCollection.updateOne( { username: userId }, { $set: { status: 'active', activatedAt: new Date(), updatedAt: new Date(), } } ); console.log(`✅ Usuario activado: ${userId}`); } } console.log(`✅ Suscripción actualizada en BD: ${userId}`); } async function handleSubscriptionUpdated(subscription) { const userId = subscription.metadata.userId; if (!userId) { console.error('❌ Subscription sin userId en metadata'); return; } console.log(`📝 Suscripción actualizada: ${userId}`); const status = subscription.status; // active, past_due, canceled, etc. const cancelAtPeriodEnd = subscription.cancel_at_period_end; const currentPeriodEnd = new Date(subscription.current_period_end * 1000); const currentPeriodStart = new Date(subscription.current_period_start * 1000); // Actualizar en BD const currentSubscription = await getUserSubscription(userId); await updateUserSubscription(userId, { ...currentSubscription, status, cancelAtPeriodEnd, currentPeriodStart, currentPeriodEnd, }); console.log(`✅ Suscripción actualizada en BD: ${userId} (${status})`); } async function handleSubscriptionDeleted(subscription) { const userId = subscription.metadata.userId; if (!userId) { console.error('❌ Subscription sin userId en metadata'); return; } console.log(`❌ Suscripción eliminada: ${userId}`); // Revertir a plan gratuito await updateUserSubscription(userId, { planId: 'free', status: 'canceled', cancelAtPeriodEnd: false, currentPeriodStart: new Date(), currentPeriodEnd: null, stripeCustomerId: subscription.customer, stripeSubscriptionId: null, }); console.log(`✅ Usuario revertido a plan gratuito: ${userId}`); } async function handlePaymentSucceeded(invoice) { const subscriptionId = invoice.subscription; const customerId = invoice.customer; console.log(`✅ Pago exitoso: ${customerId} - ${subscriptionId}`); // Aquí podrías enviar un email de confirmación o actualizar estadísticas } async function handlePaymentFailed(invoice) { const subscriptionId = invoice.subscription; const customerId = invoice.customer; console.log(`❌ Pago fallido: ${customerId} - ${subscriptionId}`); // Aquí podrías enviar un email de aviso o marcar la cuenta con problemas } export default router;