mirror of
https://github.com/documenso/documenso.git
synced 2026-06-22 04:12:06 +10:00
122 lines
3.3 KiB
TypeScript
122 lines
3.3 KiB
TypeScript
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
|
import type { Stripe } from '@documenso/lib/server-only/stripe';
|
|
import { stripe } from '@documenso/lib/server-only/stripe';
|
|
import { env } from '@documenso/lib/utils/env';
|
|
import { syncStripeCustomerSubscription } from '../sync-stripe-customer-subscription';
|
|
|
|
type StripeWebhookResponse = {
|
|
success: boolean;
|
|
message: string;
|
|
};
|
|
|
|
/**
|
|
* Events that trigger a sync of the customer's subscription state.
|
|
*
|
|
* The event payload is never trusted beyond extracting the customer ID,
|
|
* the sync function fetches the current truth from Stripe.
|
|
*/
|
|
const SYNCED_EVENT_TYPES: string[] = [
|
|
'customer.subscription.created',
|
|
'customer.subscription.updated',
|
|
'customer.subscription.deleted',
|
|
'checkout.session.completed',
|
|
'invoice.payment_succeeded',
|
|
'invoice.payment_failed',
|
|
];
|
|
|
|
export const stripeWebhookHandler = async (req: Request): Promise<Response> => {
|
|
try {
|
|
const isBillingEnabled = IS_BILLING_ENABLED();
|
|
|
|
const webhookSecret = env('NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET');
|
|
|
|
if (!webhookSecret) {
|
|
throw new Error('Missing Stripe webhook secret');
|
|
}
|
|
|
|
if (!isBillingEnabled) {
|
|
return Response.json(
|
|
{
|
|
success: false,
|
|
message: 'Billing is disabled',
|
|
} satisfies StripeWebhookResponse,
|
|
{ status: 500 },
|
|
);
|
|
}
|
|
|
|
const signature =
|
|
typeof req.headers.get('stripe-signature') === 'string' ? req.headers.get('stripe-signature') : '';
|
|
|
|
if (!signature) {
|
|
return Response.json(
|
|
{
|
|
success: false,
|
|
message: 'No signature found in request',
|
|
} satisfies StripeWebhookResponse,
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
const payload = await req.text();
|
|
|
|
if (!payload) {
|
|
return Response.json(
|
|
{
|
|
success: false,
|
|
message: 'No payload found in request',
|
|
} satisfies StripeWebhookResponse,
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
const event = stripe.webhooks.constructEvent(payload, signature, webhookSecret);
|
|
|
|
if (!SYNCED_EVENT_TYPES.includes(event.type)) {
|
|
return Response.json(
|
|
{
|
|
success: true,
|
|
message: 'Webhook received',
|
|
} satisfies StripeWebhookResponse,
|
|
{ status: 200 },
|
|
);
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
const eventObject = event.data.object as { customer?: string | Stripe.Customer | null };
|
|
|
|
const customerId = typeof eventObject.customer === 'string' ? eventObject.customer : eventObject.customer?.id;
|
|
|
|
if (!customerId) {
|
|
console.error(`No customer found on ${event.type} event ${event.id}, nothing to sync`);
|
|
|
|
return Response.json(
|
|
{
|
|
success: true,
|
|
message: 'Webhook received',
|
|
} satisfies StripeWebhookResponse,
|
|
{ status: 200 },
|
|
);
|
|
}
|
|
|
|
await syncStripeCustomerSubscription({ customerId });
|
|
|
|
return Response.json(
|
|
{
|
|
success: true,
|
|
message: 'Webhook received',
|
|
} satisfies StripeWebhookResponse,
|
|
{ status: 200 },
|
|
);
|
|
} catch (err) {
|
|
console.error(err);
|
|
|
|
return Response.json(
|
|
{
|
|
success: false,
|
|
message: 'Unknown error',
|
|
} satisfies StripeWebhookResponse,
|
|
{ status: 500 },
|
|
);
|
|
}
|
|
};
|