const Stripe = require('stripe'); const config = require('../config'); const PLAN_PRICE_ENV = { starter: 'STRIPE_STARTER_PRICE_ID', pro: 'STRIPE_PRO_PRICE_ID', }; let cachedStripeClient = null; let cachedSecretKey = null; function httpError(message, code = 400) { const error = new Error(message); error.code = code; return error; } function compactMissing(items) { return items.filter(Boolean); } function getPriceIdForPlan(planId) { if (planId === 'pro') { return config.stripe.proPriceId; } if (planId === 'starter') { return config.stripe.starterPriceId; } return ''; } function getPlanIdForPriceId(priceId) { if (!priceId) { return null; } if (priceId === config.stripe.proPriceId) { return 'pro'; } if (priceId === config.stripe.starterPriceId) { return 'starter'; } return null; } function getStripeClient() { if (!config.stripe.secretKey) { throw httpError('Stripe billing is not configured yet. Add STRIPE_SECRET_KEY in the backend environment.', 400); } if (!cachedStripeClient || cachedSecretKey !== config.stripe.secretKey) { cachedStripeClient = Stripe(config.stripe.secretKey); cachedSecretKey = config.stripe.secretKey; } return cachedStripeClient; } function formatMissingConfigurationMessage(missing) { return `Stripe billing is not configured yet. Add ${missing.join(', ')} in the backend environment, then reload the backend service.`; } module.exports = class StripeBillingService { static getPriceIdForPlan(planId) { return getPriceIdForPlan(planId); } static getPlanIdForPriceId(priceId) { return getPlanIdForPriceId(priceId); } static getMissingCheckoutConfiguration(planId) { const priceEnvName = PLAN_PRICE_ENV[planId] || PLAN_PRICE_ENV.starter; return compactMissing([ config.stripe.secretKey ? null : 'STRIPE_SECRET_KEY', getPriceIdForPlan(planId) ? null : priceEnvName, ]); } static getMissingPortalConfiguration() { return compactMissing([ config.stripe.secretKey ? null : 'STRIPE_SECRET_KEY', ]); } static getMissingWebhookConfiguration() { return compactMissing([ config.stripe.secretKey ? null : 'STRIPE_SECRET_KEY', config.stripe.webhookSecret ? null : 'STRIPE_WEBHOOK_SECRET', ]); } static getSetupStatus(planId = 'starter') { const allMissing = new Set([ ...this.getMissingCheckoutConfiguration('starter'), ...this.getMissingCheckoutConfiguration('pro'), ...this.getMissingWebhookConfiguration(), ]); return { checkoutReady: this.getMissingCheckoutConfiguration(planId).length === 0, portalReady: this.getMissingPortalConfiguration().length === 0, webhookReady: this.getMissingWebhookConfiguration().length === 0, missingConfiguration: Array.from(allMissing), }; } static assertCheckoutConfigured(planId) { const missing = this.getMissingCheckoutConfiguration(planId); if (missing.length) { throw httpError(formatMissingConfigurationMessage(missing), 400); } } static assertPortalConfigured() { const missing = this.getMissingPortalConfiguration(); if (missing.length) { throw httpError(formatMissingConfigurationMessage(missing), 400); } } static assertWebhookConfigured() { const missing = this.getMissingWebhookConfiguration(); if (missing.length) { throw httpError(formatMissingConfigurationMessage(missing), 400); } } static async createCheckoutSession(params) { const { user, plan, baseUrl, trialPeriodDays, } = params; const priceId = getPriceIdForPlan(plan.id); const subscriptionData = { metadata: { userId: user.id, planId: plan.id, }, }; this.assertCheckoutConfigured(plan.id); const stripe = getStripeClient(); if (trialPeriodDays && trialPeriodDays > 0) { subscriptionData.trial_period_days = trialPeriodDays; } return stripe.checkout.sessions.create({ mode: 'subscription', customer: user.stripeCustomerId || undefined, customer_email: user.stripeCustomerId ? undefined : user.email, client_reference_id: user.id, line_items: [ { price: priceId, quantity: 1, }, ], allow_promotion_codes: true, billing_address_collection: 'auto', success_url: `${baseUrl}/subscription?checkout=success&session_id={CHECKOUT_SESSION_ID}`, cancel_url: `${baseUrl}/subscription?checkout=cancelled`, metadata: { userId: user.id, planId: plan.id, }, subscription_data: subscriptionData, }); } static async createPortalSession(params) { const { customerId, baseUrl } = params; this.assertPortalConfigured(); const stripe = getStripeClient(); return stripe.billingPortal.sessions.create({ customer: customerId, return_url: `${baseUrl}/subscription`, }); } static constructWebhookEvent(rawBody, signature) { this.assertWebhookConfigured(); if (!signature) { throw httpError('Missing Stripe webhook signature.', 400); } const stripe = getStripeClient(); return stripe.webhooks.constructEvent( rawBody, signature, config.stripe.webhookSecret, ); } static async retrieveSubscription(subscriptionId) { if (!subscriptionId || typeof subscriptionId !== 'string') { return null; } const stripe = getStripeClient(); return stripe.subscriptions.retrieve(subscriptionId, { expand: ['items.data.price'], }); } };