From 4ea6903dd87bfbc40be631be836ab5afe9ae1007 Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Mon, 25 Aug 2025 17:20:58 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A7=20Introduce=20new=20"unpaid"=20sub?= =?UTF-8?q?scription=20status=20mirror=20in=20webhook=20handler?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/billing/src/api/webhookHandler.ts | 88 +++++-------------- .../scripts/src/helpers/resetBillingProps.ts | 1 + 2 files changed, 21 insertions(+), 68 deletions(-) diff --git a/packages/billing/src/api/webhookHandler.ts b/packages/billing/src/api/webhookHandler.ts index e400f4ce1..b34e71574 100644 --- a/packages/billing/src/api/webhookHandler.ts +++ b/packages/billing/src/api/webhookHandler.ts @@ -131,6 +131,7 @@ export const webhookHandler = async ( }, select: { isPastDue: true, + isQuarantined: true, id: true, plan: true, members: { @@ -201,10 +202,28 @@ export const webhookHandler = async ( return res.send({ message: "Workspace set to past due." }); } + if ( + subscription.status === "unpaid" && + previous && + previous.status !== "unpaid" && + !existingWorkspace.isQuarantined + ) { + await prisma.workspace.updateMany({ + where: { + id: existingWorkspace.id, + }, + data: { + isQuarantined: true, + }, + }); + + return res.send({ message: "Workspace quarantined" }); + } + if ( subscription.status === "active" && previous && - previous.status === "past_due" && + (previous.status === "past_due" || previous?.status === "unpaid") && existingWorkspace.isPastDue ) { await prisma.workspace.updateMany({ @@ -230,73 +249,6 @@ 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({ diff --git a/packages/scripts/src/helpers/resetBillingProps.ts b/packages/scripts/src/helpers/resetBillingProps.ts index f26257c77..945b66193 100644 --- a/packages/scripts/src/helpers/resetBillingProps.ts +++ b/packages/scripts/src/helpers/resetBillingProps.ts @@ -4,6 +4,7 @@ export const resetBillingProps = async () => { console.log("Resetting billing props..."); const { count } = await prisma.workspace.updateMany({ where: { + isPastDue: { not: true }, OR: [ { isQuarantined: true,