From 588c361b53ece5ddd3e7a119ea43edd7a7d8762c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 9 Jul 2026 12:31:31 +0000 Subject: [PATCH] Guard downgrade email against redelivered deleted events, add replyTo Read the previous plan inside the deletion transaction and skip the member notification when the workspace was already on free. Include the old plan in the email wording and set a monitored reply-to address. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_016XHJEjbDqPV7zAJhDbjHBy --- packages/api/src/router/stripe/webhook.ts | 23 +++++++++++++++++------ packages/emails/src/client.tsx | 1 + 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/packages/api/src/router/stripe/webhook.ts b/packages/api/src/router/stripe/webhook.ts index d37d6238..e73353e9 100644 --- a/packages/api/src/router/stripe/webhook.ts +++ b/packages/api/src/router/stripe/webhook.ts @@ -244,8 +244,14 @@ export const webhookRouter = createTRPCRouter({ return; } - const { _workspace, memberEmails } = await opts.ctx.db.transaction( + const { _workspace, memberEmails, oldPlan } = await opts.ctx.db.transaction( async (tx) => { + const existing = await tx + .select({ plan: workspace.plan }) + .from(workspace) + .where(eq(workspace.stripeId, customerId)) + .get(); + const _workspace = await tx .update(workspace) .set({ @@ -355,6 +361,7 @@ export const webhookRouter = createTRPCRouter({ memberEmails: members .map((m) => m.email) .filter((email): email is string => Boolean(email)), + oldPlan: existing?.plan ?? "free", }; }, ); @@ -368,11 +375,15 @@ export const webhookRouter = createTRPCRouter({ const workspaceId = _workspace[0].id; - await emailClient.sendPlanDowngrade({ - to: memberEmails, - workspaceName: _workspace[0].name, - newPlan: "free", - }); + // Stripe can redeliver the event; only notify on an actual plan change + if (oldPlan !== "free") { + await emailClient.sendPlanDowngrade({ + to: memberEmails, + workspaceName: _workspace[0].name, + oldPlan, + newPlan: "free", + }); + } const customer = await stripe.customers.retrieve(customerId); diff --git a/packages/emails/src/client.tsx b/packages/emails/src/client.tsx index 3fabd77e..23991feb 100644 --- a/packages/emails/src/client.tsx +++ b/packages/emails/src/client.tsx @@ -282,6 +282,7 @@ export class EmailClient { const result = await this.client.batch.send( recipients.map((member) => ({ from: "OpenStatus ", + replyTo: "Thibault Le Ouay Ducasse ", subject: `Your workspace has been downgraded to the ${req.newPlan} plan`, to: member, html, -- 2.51.2