From 9ee207439529f85e05c97808ef6dcd01719ae361 Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Mon, 25 Aug 2025 16:56:07 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A7=20Enhance=20stripe=20webhookHandle?= =?UTF-8?q?r=20to=20manage=20invoice.payment=5Ffailed=20events=20and=20qua?= =?UTF-8?q?rantine=20workspaces?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/billing/src/api/webhookHandler.ts | 68 ++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/packages/billing/src/api/webhookHandler.ts b/packages/billing/src/api/webhookHandler.ts index ea91d9edb..4d42a2d9e 100644 --- a/packages/billing/src/api/webhookHandler.ts +++ b/packages/billing/src/api/webhookHandler.ts @@ -213,6 +213,7 @@ export const webhookHandler = async ( }, data: { isPastDue: false, + isQuarantined: false, }, }); @@ -229,6 +230,73 @@ export const webhookHandler = async ( return res.send({ message: "Nothing to do" }); } + case "invoice.payment_failed": { + const invoice = event.data.object; + if (invoice.collection_method === "charge_automatically") + return res.send({ + message: "Manual payment required", + }); + + const stripeId = + typeof invoice.customer === "string" + ? invoice.customer + : invoice.customer?.id; + if (!stripeId) { + throw new Error("Stripe ID not found"); + } + + const existingWorkspace = await prisma.workspace.findFirst({ + where: { + stripeId, + }, + select: { + id: true, + plan: true, + isPastDue: true, + isQuarantined: true, + members: { + select: { userId: true, role: true }, + where: { role: WorkspaceRole.ADMIN }, + }, + }, + }); + + if (!existingWorkspace) { + return res.send({ + message: "Workspace not found for failed invoice", + }); + } + + if (existingWorkspace.isQuarantined) { + return res.send({ + message: "Workspace is already quarantined", + }); + } + + if (invoice.next_payment_attempt === null) { + if (!existingWorkspace.isQuarantined) { + await prisma.workspace.updateMany({ + where: { + id: existingWorkspace.id, + }, + data: { + isQuarantined: true, + isPastDue: true, + }, + }); + } + + return res.send({ + message: + "Invoice payment permanently failed - workspace marked as past due", + }); + } + + return res.send({ + message: `Invoice payment failed (attempt ${invoice.attempt_count}) - retries may continue`, + }); + } + case "customer.subscription.deleted": { const subscription = event.data.object as Stripe.Subscription; const { data } = await stripe.subscriptions.list({