diff --git a/app/webhooks/stripe/handle-invoice-creation.ts b/app/webhooks/stripe/handle-invoice-creation.ts new file mode 100644 index 00000000..eb4f2ef6 --- /dev/null +++ b/app/webhooks/stripe/handle-invoice-creation.ts @@ -0,0 +1,21 @@ +import { stripe } from "@/services/external/stripe"; +import type Stripe from "stripe"; + +export async function handleInvoiceCreation(invoice: Stripe.Invoice) { + if (!invoice.subscription || typeof invoice.subscription !== "string") { + throw new Error( + "Invoice is missing a subscription ID. Please check the invoice data.", + ); + } + + const subscription = await stripe.subscriptions.retrieve( + invoice.subscription, + ); + + if (subscription.status !== "canceled") { + return; + } + + // When a subscription is canceled, we should charge for usage-based billing from the previous billing cycle. The final invoice, which includes these charges, will be automatically created but will not be processed for payment. Therefore, we need to handle this case manually. + await stripe.invoices.pay(invoice.id); +} diff --git a/app/webhooks/stripe/route.ts b/app/webhooks/stripe/route.ts index 8987ea56..fafa46c3 100644 --- a/app/webhooks/stripe/route.ts +++ b/app/webhooks/stripe/route.ts @@ -2,6 +2,7 @@ import { stripe } from "@/services/external/stripe"; import { upsertSubscription } from "@/services/external/stripe/actions/upsert-subscription"; import { reportUserSeatUsage } from "@/services/usage-based-billing"; import type Stripe from "stripe"; +import { handleInvoiceCreation } from "./handle-invoice-creation"; import { handleSubscriptionCancellation } from "./handle-subscription-cancellation"; const relevantEvents = new Set([ @@ -91,28 +92,13 @@ export async function POST(req: Request) { ); } await handleSubscriptionCancellation(event.data.object); + await upsertSubscription(event.data.object.id); break; case "invoice.created": console.log(`🔔 Invoice created: ${event.data.object.id}`); - // TODO: Skip for now - will be handled when implementing subscription cancellation invoice processing - if ( - event.data.object.subscription && - typeof event.data.object.subscription === "string" - ) { - const subscriptionId = event.data.object.subscription; - const subscription = - await stripe.subscriptions.retrieve(subscriptionId); - - if (subscription.status === "canceled") { - console.log( - "Skipping processing for canceled subscription invoice: ", - subscriptionId, - ); - break; - } - } + await handleInvoiceCreation(event.data.object); break; default: