Files
wallabicher/web/backend/routes/payments.js
Omar Sánchez Pizarro cc6ffdc5a5 payments with stripe
Signed-off-by: Omar Sánchez Pizarro <omar.sanchez@pistacero.net>
2026-01-21 02:20:13 +01:00

419 lines
12 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;