Files
Omar Sánchez Pizarro c72ef29319 Add Stripe payment integration and update configuration
- Added Stripe environment variables to docker-compose.yml for secret key, webhook secret, and base URL.
- Created PAYMENTS.md to document the setup and usage of the Stripe payment system.
- Updated webhook signature handling in stripe.js to use a new secret key.

Signed-off-by: Omar Sánchez Pizarro <omar.sanchez@pistacero.net>
2026-01-21 10:18:34 +01:00

227 lines
5.8 KiB
JavaScript

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_n6tTKRSG38WJQDRX8jLjZTs7kPKxbdNP';
}
if (!webhookSecret) {
throw new Error('STRIPE_WEBHOOK_SECRET no configurado');
}
return stripeClient.webhooks.constructEvent(payload, signature, webhookSecret);
}