diff --git a/web/backend/env.example.txt b/web/backend/env.example.txt new file mode 100644 index 0000000..63cdb68 --- /dev/null +++ b/web/backend/env.example.txt @@ -0,0 +1,14 @@ +# Stripe Configuration +STRIPE_SECRET_KEY=sk_test_your_stripe_secret_key_here +STRIPE_WEBHOOK_SECRET=whsec_your_webhook_secret_here + +# Base URL for redirects (production/development) +BASE_URL=http://localhost + +# MongoDB Configuration (optional, if not in config.yaml) +# MONGODB_HOST=localhost +# MONGODB_PORT=27017 +# MONGODB_DATABASE=wallabicher +# MONGODB_USERNAME= +# MONGODB_PASSWORD= + diff --git a/web/backend/package-lock.json b/web/backend/package-lock.json index 9f134ec..93699e4 100644 --- a/web/backend/package-lock.json +++ b/web/backend/package-lock.json @@ -15,6 +15,7 @@ "express": "^4.18.2", "mongodb": "^6.3.0", "rate-limiter-flexible": "^5.0.3", + "stripe": "^20.2.0", "web-push": "^3.6.7", "ws": "^8.14.2", "yaml": "^2.3.4" @@ -1780,6 +1781,26 @@ "node": ">=8" } }, + "node_modules/stripe": { + "version": "20.2.0", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-20.2.0.tgz", + "integrity": "sha512-m8niTfdm3nPP/yQswRWMwQxqEUcTtB3RTJQ9oo6NINDzgi7aPOadsH/fPXIIfL1Sc5+lqQFKSk7WiO6CXmvaeA==", + "license": "MIT", + "dependencies": { + "qs": "^6.14.1" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "@types/node": ">=16" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/tar": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", diff --git a/web/backend/package.json b/web/backend/package.json index 2f03ce6..3879059 100644 --- a/web/backend/package.json +++ b/web/backend/package.json @@ -20,8 +20,9 @@ "chokidar": "^3.5.3", "cors": "^2.8.5", "express": "^4.18.2", - "rate-limiter-flexible": "^5.0.3", "mongodb": "^6.3.0", + "rate-limiter-flexible": "^5.0.3", + "stripe": "^20.2.0", "web-push": "^3.6.7", "ws": "^8.14.2", "yaml": "^2.3.4" diff --git a/web/backend/routes/payments.js b/web/backend/routes/payments.js new file mode 100644 index 0000000..32e6d32 --- /dev/null +++ b/web/backend/routes/payments.js @@ -0,0 +1,418 @@ +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; + diff --git a/web/backend/routes/users.js b/web/backend/routes/users.js index 29d82f9..d8b0196 100644 --- a/web/backend/routes/users.js +++ b/web/backend/routes/users.js @@ -7,6 +7,98 @@ import { combineFingerprint } from '../utils/fingerprint.js'; const router = express.Router(); +// Endpoint de registro (público) +router.post('/register', async (req, res) => { + try { + const db = getDB(); + if (!db) { + return res.status(500).json({ error: 'MongoDB no está disponible' }); + } + + const { username, password, email, planId } = req.body; + + if (!username || !password) { + return res.status(400).json({ error: 'username y password son requeridos' }); + } + + if (username.length < 3) { + return res.status(400).json({ error: 'El nombre de usuario debe tener al menos 3 caracteres' }); + } + + if (password.length < 6) { + return res.status(400).json({ error: 'La contraseña debe tener al menos 6 caracteres' }); + } + + // Validar email si se proporciona + if (email) { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(email)) { + return res.status(400).json({ error: 'Email no válido' }); + } + } + + // Verificar si el usuario ya existe + const existingUser = await getUser(username); + if (existingUser) { + return res.status(409).json({ error: 'El usuario ya existe' }); + } + + // Verificar si es plan de pago y si Stripe está disponible + const selectedPlanId = planId || 'free'; + const isPaidPlan = selectedPlanId !== 'free'; + + if (isPaidPlan) { + // Para planes de pago, verificar que Stripe esté configurado + const { getStripeClient } = await import('../services/stripe.js'); + const stripeClient = getStripeClient(); + + if (!stripeClient) { + return res.status(503).json({ + error: 'El sistema de pagos no está disponible actualmente', + message: 'No se pueden procesar planes de pago en este momento. Por favor, intenta con el plan gratuito o contacta con soporte.' + }); + } + } + + // Hashear contraseña y crear usuario + const passwordHash = await bcrypt.hash(password, 10); + await createUser({ + username, + passwordHash, + email: email || null, + role: 'user', + // Si es plan de pago, marcar como pendiente hasta que se complete el pago + status: isPaidPlan ? 'pending_payment' : 'active', + }); + + // Crear suscripción inicial + const { updateUserSubscription } = await import('../services/mongodb.js'); + await updateUserSubscription(username, { + planId: selectedPlanId, + status: isPaidPlan ? 'pending' : 'active', + currentPeriodStart: new Date(), + currentPeriodEnd: null, + cancelAtPeriodEnd: false, + }); + + console.log(`✅ Usuario registrado: ${username} (${selectedPlanId}) - Estado: ${isPaidPlan ? 'pending_payment' : 'active'}`); + res.json({ + success: true, + message: 'Usuario creado correctamente', + username, + planId: selectedPlanId, + requiresPayment: isPaidPlan, + }); + } catch (error) { + console.error('Error registrando usuario:', error); + // Manejar error de duplicado + if (error.code === 11000) { + return res.status(409).json({ error: 'El usuario ya existe' }); + } + res.status(500).json({ error: error.message }); + } +}); + // Endpoint de login (público) router.post('/login', async (req, res) => { try { @@ -42,6 +134,23 @@ router.post('/login', async (req, res) => { return res.status(401).json({ error: 'Invalid credentials', message: 'Usuario o contraseña incorrectos' }); } + // Verificar estado del usuario + if (user.status === 'pending_payment') { + return res.status(403).json({ + error: 'Payment pending', + message: 'Tu cuenta está pendiente de pago. Por favor, completa el proceso de pago para activar tu cuenta.', + status: 'pending_payment' + }); + } + + if (user.status === 'suspended' || user.status === 'disabled') { + return res.status(403).json({ + error: 'Account suspended', + message: 'Tu cuenta ha sido suspendida. Contacta con soporte.', + status: user.status + }); + } + // Generar fingerprint del dispositivo const { fingerprint, deviceInfo } = combineFingerprint(clientFingerprint, clientDeviceInfo, req); diff --git a/web/backend/server.js b/web/backend/server.js index a44818b..ef97101 100644 --- a/web/backend/server.js +++ b/web/backend/server.js @@ -8,6 +8,7 @@ import { initVAPIDKeys } from './services/webPush.js'; import { initWebSocket } from './services/websocket.js'; import { startArticleMonitoring } from './services/articleMonitor.js'; import { initFileWatcher } from './services/fileWatcher.js'; +import { initStripe } from './services/stripe.js'; import routes from './routes/index.js'; import workersRouter from './routes/workers.js'; import articlesRouter from './routes/articles.js'; @@ -19,6 +20,7 @@ import pushRouter from './routes/push.js'; import usersRouter from './routes/users.js'; import adminRouter from './routes/admin.js'; import subscriptionRouter from './routes/subscription.js'; +import paymentsRouter from './routes/payments.js'; const app = express(); const server = createServer(app); @@ -28,6 +30,12 @@ app.set('trust proxy', true); // Middlewares globales app.use(cors()); + +// ⚠️ IMPORTANTE: Webhook de Stripe necesita el body RAW (sin parsear) +// Por eso usamos express.raw() SOLO para esta ruta, ANTES de express.json() +app.use('/api/payments/webhook', express.raw({ type: 'application/json' })); + +// Ahora sí, parseamos JSON para todas las demás rutas app.use(express.json()); // Aplicar rate limiting a todas las rutas API @@ -36,6 +44,9 @@ app.use('/api', rateLimitMiddleware); // Inicializar VAPID keys para Web Push initVAPIDKeys(); +// Inicializar Stripe +initStripe(); + // Inicializar WebSocket initWebSocket(server); @@ -51,6 +62,7 @@ app.use('/api/push', pushRouter); app.use('/api/users', usersRouter); app.use('/api/admin', adminRouter); app.use('/api/subscription', subscriptionRouter); +app.use('/api/payments', paymentsRouter); // Inicializar servidor async function startServer() { diff --git a/web/backend/services/mongodb.js b/web/backend/services/mongodb.js index 7b27386..192d3ec 100644 --- a/web/backend/services/mongodb.js +++ b/web/backend/services/mongodb.js @@ -1331,6 +1331,7 @@ export async function updateUserSubscription(username, subscriptionData) { subscription: { planId: subscriptionData.planId || 'free', status: subscriptionData.status || 'active', + billingPeriod: subscriptionData.billingPeriod || 'monthly', currentPeriodStart: subscriptionData.currentPeriodStart || new Date(), currentPeriodEnd: subscriptionData.currentPeriodEnd || null, cancelAtPeriodEnd: subscriptionData.cancelAtPeriodEnd || false, diff --git a/web/backend/services/stripe.js b/web/backend/services/stripe.js new file mode 100644 index 0000000..5bf5735 --- /dev/null +++ b/web/backend/services/stripe.js @@ -0,0 +1,226 @@ +import Stripe from 'stripe'; +import { SUBSCRIPTION_PLANS } from '../config/subscriptionPlans.js'; + +let stripeClient = null; + +// Inicializar Stripe +export function initStripe() { + let stripeSecretKey = process.env.STRIPE_SECRET_KEY; + + if (!stripeSecretKey) { + stripeSecretKey = 'sk_test_51SrpOfH73CrYqhOp2NfijzzU07ADADmwigscMVdLGzKu9zA83dsrODhfsaY1X4EFTSihhIB0lVtDQ2HpeOfMWTur00YLuuktSL'; + } + + if (!stripeSecretKey) { + console.warn('⚠️ STRIPE_SECRET_KEY no configurado. Los pagos estarán deshabilitados.'); + return null; + } + + try { + stripeClient = new Stripe(stripeSecretKey, { + apiVersion: '2024-12-18.acacia', + }); + console.log('✅ Stripe inicializado correctamente'); + return stripeClient; + } catch (error) { + console.error('Error inicializando Stripe:', error.message); + return null; + } +} + +// Obtener cliente de Stripe +export function getStripeClient() { + return stripeClient; +} + +// Crear sesión de checkout de Stripe +export async function createCheckoutSession({ planId, billingPeriod, email, userId }) { + if (!stripeClient) { + throw new Error('Stripe no está configurado'); + } + + const plan = SUBSCRIPTION_PLANS[planId]; + if (!plan) { + throw new Error('Plan no válido'); + } + + if (planId === 'free') { + throw new Error('El plan gratuito no requiere pago'); + } + + // Precio según período de facturación + const priceAmount = billingPeriod === 'yearly' ? plan.price.yearly : plan.price.monthly; + const priceCents = Math.round(priceAmount * 100); // Convertir a centavos + + // Crear precio en Stripe (o usar precio existente si ya lo tienes) + const priceId = await getOrCreatePrice(planId, billingPeriod, priceCents); + + // URL de éxito y cancelación + const baseUrl = process.env.BASE_URL || 'http://localhost'; + const successUrl = `${baseUrl}/dashboard/?session_id={CHECKOUT_SESSION_ID}&payment_success=true`; + const cancelUrl = `${baseUrl}/dashboard/?payment_cancelled=true`; + + // Crear sesión de checkout + const session = await stripeClient.checkout.sessions.create({ + mode: 'subscription', + line_items: [ + { + price: priceId, + quantity: 1, + }, + ], + success_url: successUrl, + cancel_url: cancelUrl, + customer_email: email, + client_reference_id: userId, + metadata: { + userId, + planId, + billingPeriod, + }, + subscription_data: { + metadata: { + userId, + planId, + billingPeriod, + }, + }, + }); + + return session; +} + +// Obtener o crear precio en Stripe +async function getOrCreatePrice(planId, billingPeriod, priceCents) { + const plan = SUBSCRIPTION_PLANS[planId]; + + // Buscar precio existente (usando lookup_key) + const lookupKey = `${planId}_${billingPeriod}`; + + try { + const prices = await stripeClient.prices.list({ + lookup_keys: [lookupKey], + limit: 1, + }); + + if (prices.data.length > 0) { + return prices.data[0].id; + } + } catch (error) { + console.log('Precio no encontrado, creando nuevo...'); + } + + // Si no existe, crear producto y precio + let product; + try { + // Buscar producto existente + const products = await stripeClient.products.list({ + limit: 100, + }); + product = products.data.find(p => p.metadata.planId === planId); + + // Si no existe, crear producto + if (!product) { + product = await stripeClient.products.create({ + name: `Wallabicher ${plan.name}`, + description: plan.description, + metadata: { + planId, + }, + }); + } + } catch (error) { + console.error('Error creando/buscando producto:', error); + throw error; + } + + // Crear precio + const price = await stripeClient.prices.create({ + product: product.id, + unit_amount: priceCents, + currency: 'eur', + recurring: { + interval: billingPeriod === 'yearly' ? 'year' : 'month', + }, + lookup_key: lookupKey, + metadata: { + planId, + billingPeriod, + }, + }); + + return price.id; +} + +// Crear portal del cliente para gestionar suscripción +export async function createCustomerPortalSession(customerId) { + if (!stripeClient) { + throw new Error('Stripe no está configurado'); + } + + const baseUrl = process.env.BASE_URL || 'http://localhost'; + const returnUrl = `${baseUrl}/dashboard/`; + + const session = await stripeClient.billingPortal.sessions.create({ + customer: customerId, + return_url: returnUrl, + }); + + return session; +} + +// Cancelar suscripción +export async function cancelSubscription(subscriptionId) { + if (!stripeClient) { + throw new Error('Stripe no está configurado'); + } + + // Cancelar al final del período + const subscription = await stripeClient.subscriptions.update(subscriptionId, { + cancel_at_period_end: true, + }); + + return subscription; +} + +// Reactivar suscripción cancelada +export async function reactivateSubscription(subscriptionId) { + if (!stripeClient) { + throw new Error('Stripe no está configurado'); + } + + const subscription = await stripeClient.subscriptions.update(subscriptionId, { + cancel_at_period_end: false, + }); + + return subscription; +} + +// Obtener suscripción por ID +export async function getSubscription(subscriptionId) { + if (!stripeClient) { + throw new Error('Stripe no está configurado'); + } + + return await stripeClient.subscriptions.retrieve(subscriptionId); +} + +// Verificar webhook signature +export function verifyWebhookSignature(payload, signature) { + if (!stripeClient) { + throw new Error('Stripe no está configurado'); + } + + let webhookSecret = process.env.STRIPE_WEBHOOK_SECRET; + + if (!webhookSecret) { + webhookSecret = 'whsec_8ebec8c2aa82a791aa9f2cd68211e297a5d172aea62ebd7b771d230e3a597aa8'; + } + + if (!webhookSecret) { + throw new Error('STRIPE_WEBHOOK_SECRET no configurado'); + } + + return stripeClient.webhooks.constructEvent(payload, signature, webhookSecret); +} + diff --git a/web/dashboard/src/App.vue b/web/dashboard/src/App.vue index 594b340..b9dd644 100644 --- a/web/dashboard/src/App.vue +++ b/web/dashboard/src/App.vue @@ -1,7 +1,7 @@