Skip to main content
Glama
stripe.webhook.ts8.21 kB
import type { Locale } from '@intlayer/types'; import { logger } from '@logger'; import * as emailService from '@services/email.service'; import { getOrganizationById } from '@services/organization.service'; import { addOrUpdateSubscription, cancelSubscription, changeSubscriptionStatus, } from '@services/subscription.service'; import { getUserById } from '@services/user.service'; import { GenericError } from '@utils/errors'; import type { Request, Response } from 'express'; import { Stripe } from 'stripe'; import type { Plan } from '@/types/plan.types'; type SubscriptionMetadata = { locale: Locale; // Localization setting (e.g., 'en', 'fr', 'es') userId: string; // ID of the user associated with the subscription organizationId: string; // ID of the organization associated with the subscription }; /** * Stripe webhook handler for processing subscription and invoice events. * @param req - Express request object. * @param res - Express response object. */ export const stripeWebhook = async (req: Request, res: Response) => { // Initialize the Stripe client with the secret key const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!); const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET!; // Webhook secret for verifying event signatures const sig = req.headers['stripe-signature']!; // Retrieve the signature from the webhook request headers let event: Stripe.Event; // Verify the webhook signature to ensure the request is authentic try { event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret); } catch (err) { // Respond with a 400 status code if the signature verification fails res.status(400).send(`Webhook Error: ${(err as Error).message}`); return; } // Utility function to extract metadata from a Stripe customer const extractMetadata = async (customerId: string) => { const customer = await stripe.customers.retrieve(customerId); // Retrieve customer details from Stripe return (customer as Stripe.Customer).metadata as SubscriptionMetadata; // Return the metadata object }; // Handles subscription-related events (creation, update, deletion) const handleSubscriptionEvent = async ( subscription: Stripe.Subscription, statusOverride?: Plan['status'] // Optionally override the subscription status ) => { const { id: subscriptionId, customer } = subscription; const priceId = subscription.items.data[0]?.price?.id; // Extract the price ID from subscription items if (!customer) { throw new GenericError('STRIPE_SUBSCRIPTION_NO_CUSTOMER'); } const customerId = customer as string; const { locale, userId, organizationId } = await extractMetadata(customerId); // Extract metadata from the customer // Set localization in response locals if available if (locale) { res.locals.locales = locale; } const organization = await getOrganizationById(organizationId); // Fetch organization details by ID if (!organization) { throw new GenericError('ORGANIZATION_NOT_FOUND'); } const user = await getUserById(userId); if (!user) { throw new GenericError('USER_NOT_FOUND'); } const status = statusOverride ?? subscription.status; // Use the provided status override or the subscription's status // Update or create a subscription record in the database await addOrUpdateSubscription( subscriptionId, priceId!, customerId, userId, organization, status ); if (status === 'active') { await emailService.sendEmail({ type: 'subscriptionPaymentSuccess', to: user.email, email: user.email, subscriptionStartDate: new Date().toLocaleDateString(), manageSubscriptionLink: `${process.env.CLIENT_URL}/dashboard/organization`, username: user.name, organizationName: organization.name, planName: organization.plan?.type ?? 'Unknown', }); } if (status === 'canceled') { await emailService.sendEmail({ type: 'subscriptionPaymentCancellation', to: user.email, email: user.email, cancellationDate: new Date( (subscription as any).current_period_end * 1000 ).toLocaleDateString(), reactivateLink: `${process.env.CLIENT_URL}/pricing`, username: user.name, organizationName: organization.name, planName: organization.plan?.type ?? 'Unknown', }); } }; // Handles invoice-related events (payment success or failure) const handleInvoiceEvent = async ( invoice: Stripe.Invoice, status: 'active' | 'incomplete' ) => { const subscriptionId = typeof (invoice as any).subscription === 'string' ? (invoice as any).subscription : (invoice as any).subscription?.id; // Extract the subscription ID from the invoice if (!subscriptionId) { logger.warn('Subscription ID is undefined in invoice.'); return; } // Retrieve the subscription details from Stripe const subscription = await stripe.subscriptions.retrieve(subscriptionId); const organization = await getOrganizationById( subscription.metadata.organizationId ); // Prevent duplicate subscriptions by canceling conflicting subscriptions if ( organization.plan?.subscriptionId && organization.plan.subscriptionId !== subscriptionId ) { await stripe.subscriptions.cancel(subscriptionId); } const customerId = invoice.customer as string; const { locale, userId, organizationId } = await extractMetadata(customerId); // Set localization in response locals if available if (locale) { res.locals.locales = locale; } // Update the subscription status in the database await changeSubscriptionStatus( subscriptionId, status, userId, organizationId ); }; try { // Log the event type for debugging and monitoring logger.info(`Triggered event type ${event.type}`); // Handle specific event types switch (event.type) { case 'customer.subscription.created': { logger.info(`Handled event type ${event.type}`); // Process a new subscription creation event await handleSubscriptionEvent(event.data.object as Stripe.Subscription); break; } case 'customer.subscription.updated': { logger.info(`Handled event type ${event.type}`); // Process a subscription update event await handleSubscriptionEvent(event.data.object as Stripe.Subscription); break; } case 'customer.subscription.deleted': { logger.info(`Handled event type ${event.type}`); const subscription = event.data .object as unknown as Stripe.Subscription; const customerId = subscription.customer as string; const { locale, organizationId } = await extractMetadata(customerId); // Set localization in response locals if available if (locale) { res.locals.locales = locale; } // Handle subscription deletion by canceling it in the database await cancelSubscription(subscription.id, organizationId); break; } case 'invoice.payment_succeeded': { logger.info(`Handled event type ${event.type}`); // Handle successful invoice payment await handleInvoiceEvent(event.data.object as Stripe.Invoice, 'active'); break; } case 'invoice.payment_failed': { logger.info(`Handled event type ${event.type}`); // Handle failed invoice payment await handleInvoiceEvent( event.data.object as Stripe.Invoice, 'incomplete' ); break; } default: // Log unhandled event types for visibility logger.info(`Unhandled event type ${event.type}`); } // Respond to Stripe to confirm the event was processed successfully res.send(); } catch (error) { // Log errors for debugging and respond with a 500 status code logger.error( `Error handling event ${event.type}: ${(error as Error).message}` ); res.status(500).send('Server Error'); } };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/aymericzip/intlayer'

If you have feedback or need assistance with the MCP directory API, please join our Discord server