import Stripe from 'stripe'; import dotenv from 'dotenv'; dotenv.config(); let stripe = null; try { if (process.env.STRIPE_SECRET_KEY && !process.env.STRIPE_SECRET_KEY.includes("placeholder")) { stripe = new Stripe(process.env.STRIPE_SECRET_KEY); } else { console.warn("Stripe not configured - billing endpoints will return 503"); } } catch (e) { console.warn("Stripe init failed:", e.message); } // Map plan names to Stripe Price IDs const planPriceMap = { 'starter': process.env.STRIPE_PRICE_STARTER, 'growth': process.env.STRIPE_PRICE_GROWTH, 'pro': process.env.STRIPE_PRICE_PRO }; // Plan metadata const planMetadata = { 'starter': { tier: 'starter', amount: 3900, name: 'Starter' }, 'growth': { tier: 'growth', amount: 9900, name: 'Growth' }, 'pro': { tier: 'pro', amount: 24900, name: 'Pro' } }; /** * Create or retrieve Stripe customer for a user */ export async function getOrCreateStripeCustomer(pool, userId, email) { try { // Check if subscription already exists const subResult = await pool.query( 'SELECT stripe_customer_id FROM subscriptions WHERE user_id = $1', [userId] ); if (subResult.rows.length > 0) { return subResult.rows[0].stripe_customer_id; } // Create new customer const customer = await stripe.customers.create({ email, metadata: { user_id: userId } }); return customer.id; } catch (error) { console.error('Error getting/creating Stripe customer:', error); throw error; } } /** * Create a checkout session for a selected plan */ export async function createCheckoutSession(pool, userId, email, plan, successUrl, cancelUrl) { try { if (!planPriceMap[plan]) { throw new Error(`Invalid plan: ${plan}`); } const customerId = await getOrCreateStripeCustomer(pool, userId, email); const session = await stripe.checkout.sessions.create({ customer: customerId, mode: 'subscription', payment_method_types: ['card'], line_items: [ { price: planPriceMap[plan], quantity: 1 } ], subscription_data: { trial_period_days: 14, metadata: { user_id: userId, plan: plan } }, success_url: successUrl, cancel_url: cancelUrl, metadata: { user_id: userId, plan: plan } }); return session; } catch (error) { console.error('Error creating checkout session:', error); throw error; } } /** * Handle Stripe webhook events */ export async function handleWebhookEvent(pool, event) { try { const { type, data } = event; const object = data.object; console.log(`Processing webhook event: ${type}`); switch (type) { case 'checkout.session.completed': { const session = object; const { user_id, plan } = session.metadata; // Create/update subscription record const result = await pool.query( `INSERT INTO subscriptions (user_id, stripe_customer_id, plan, status, trial_start, trial_end) VALUES ($1, $2, $3, 'active', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP + INTERVAL '14 days') ON CONFLICT (user_id) DO UPDATE SET stripe_customer_id = $2, plan = $3, status = 'active', trial_start = CURRENT_TIMESTAMP, trial_end = CURRENT_TIMESTAMP + INTERVAL '14 days', updated_at = CURRENT_TIMESTAMP RETURNING *`, [user_id, session.customer, plan] ); // Update user tier await pool.query( 'UPDATE users SET tier = $1 WHERE id = $2', [plan, user_id] ); console.log(`Subscription created for user ${user_id} on plan ${plan}`); return result.rows[0]; } case 'customer.subscription.updated': { const subscription = object; const { user_id, plan } = subscription.metadata; // Update subscription record const result = await pool.query( `UPDATE subscriptions SET stripe_subscription_id = $1, plan = $2, status = $3, current_period_start = to_timestamp($4), current_period_end = to_timestamp($5), trial_end = $6, updated_at = CURRENT_TIMESTAMP WHERE user_id = $7 RETURNING *`, [ subscription.id, plan || 'unknown', subscription.status, subscription.current_period_start, subscription.current_period_end, subscription.trial_end ? new Date(subscription.trial_end * 1000) : null, user_id ] ); console.log(`Subscription updated for user ${user_id}`); return result.rows[0]; } case 'customer.subscription.deleted': { const subscription = object; const { user_id } = subscription.metadata; // Update subscription status const result = await pool.query( `UPDATE subscriptions SET status = 'cancelled', stripe_subscription_id = NULL, updated_at = CURRENT_TIMESTAMP WHERE user_id = $1 RETURNING *`, [user_id] ); // Reset user tier to free await pool.query( 'UPDATE users SET tier = $1 WHERE id = $2', ['free', user_id] ); console.log(`Subscription cancelled for user ${user_id}`); return result.rows[0]; } case 'invoice.payment_failed': { const invoice = object; const subscription = invoice.subscription; // Get user_id from subscription metadata const sub = await stripe.subscriptions.retrieve(subscription); const { user_id } = sub.metadata; console.log(`Payment failed for user ${user_id}`); // Could send alert email here, etc. return sub; } default: console.log(`Unhandled event type: ${type}`); return null; } } catch (error) { console.error('Error handling webhook event:', error); throw error; } } /** * Get current subscription status for a user */ export async function getSubscriptionStatus(pool, userId) { try { const result = await pool.query( `SELECT * FROM subscriptions WHERE user_id = $1`, [userId] ); if (result.rows.length === 0) { return null; } return result.rows[0]; } catch (error) { console.error('Error fetching subscription status:', error); throw error; } } /** * Create a Stripe Customer Portal session */ export async function createPortalSession(pool, userId, returnUrl) { try { const subscription = await getSubscriptionStatus(pool, userId); if (!subscription) { throw new Error('No subscription found for user'); } const session = await stripe.billingPortal.sessions.create({ customer: subscription.stripe_customer_id, return_url: returnUrl }); return session; } catch (error) { console.error('Error creating portal session:', error); throw error; } } /** * Verify webhook signature */ export function verifyWebhookSignature(body, signature, secret) { try { return stripe.webhooks.constructEvent(body, signature, secret); } catch (error) { console.error('Webhook signature verification failed:', error.message); throw error; } } export default { getOrCreateStripeCustomer, createCheckoutSession, handleWebhookEvent, getSubscriptionStatus, createPortalSession, verifyWebhookSignature };