Files
tenderpilot/stripe-billing.js

282 lines
7.4 KiB
JavaScript
Raw Permalink Normal View History

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
};