From 245393e09ef694a9faf66a2d74e7a60c2921febe Mon Sep 17 00:00:00 2001 From: Steven Tey Date: Wed, 26 Nov 2025 18:22:03 -0800 Subject: [PATCH 1/2] Improve `checkout.session.completed` webhook to match customer by email --- .../webhook/checkout-session-completed.ts | 76 +++++++++---------- 1 file changed, 37 insertions(+), 39 deletions(-) diff --git a/apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.ts b/apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.ts index 2d19b76e94b..c5db385babe 100644 --- a/apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.ts +++ b/apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.ts @@ -27,7 +27,7 @@ import { transformSaleEventData, } from "@/lib/webhook/transform"; import { prisma } from "@dub/prisma"; -import { Customer, WorkflowTrigger } from "@dub/prisma/client"; +import { Customer, Project, WorkflowTrigger } from "@dub/prisma/client"; import { COUNTRIES_TO_CONTINENTS, nanoid, pick } from "@dub/utils"; import { waitUntil } from "@vercel/functions"; import type Stripe from "stripe"; @@ -60,6 +60,22 @@ export async function checkoutSessionCompleted( let leadEvent: LeadEventTB | undefined; let linkId: string | undefined; + const workspace = await prisma.project.findUnique({ + where: { + stripeConnectId: stripeAccountId, + }, + select: { + id: true, + stripeConnectId: true, + defaultProgramId: true, + webhookEnabled: true, + }, + }); + + if (!workspace) { + return `Workspace with stripeConnectId ${stripeAccountId} not found, skipping...`; + } + /* for stripe checkout links: - if client_reference_id is a dub_id, we find the click event @@ -75,24 +91,10 @@ export async function checkoutSessionCompleted( return `Click event with dub_id ${dubClickId} not found, skipping...`; } - const workspace = await prisma.project.findUnique({ - where: { - stripeConnectId: stripeAccountId, - }, - select: { - id: true, - }, - }); - - if (!workspace) { - return `Workspace with stripeConnectId ${stripeAccountId} not found, skipping...`; - } - existingCustomer = await prisma.customer.findFirst({ where: { projectId: workspace.id, - // check for existing customer with the same externalId (via clickId or email) - // TODO: should we support checks for email and stripeCustomerId too? + // check for existing customer with the same externalId (via clickId or email) or email or stripeCustomerId OR: [ { externalId: clickEvent.click_id, @@ -189,6 +191,7 @@ export async function checkoutSessionCompleted( const promoCodeResponse = await attributeViaPromoCode({ promotionCodeId, stripeAccountId, + workspace, mode, charge, }); @@ -202,9 +205,18 @@ export async function checkoutSessionCompleted( } } } else { - existingCustomer = await prisma.customer.findUnique({ + // find customer by stripeCustomerId or email + existingCustomer = await prisma.customer.findFirst({ where: { - stripeCustomerId, + OR: [ + { + stripeCustomerId, + }, + { + projectId: workspace.id, + email: stripeCustomerEmail, + }, + ], }, }); @@ -232,6 +244,7 @@ export async function checkoutSessionCompleted( const promoCodeResponse = await attributeViaPromoCode({ promotionCodeId, stripeAccountId, + workspace, mode, charge, }); @@ -350,7 +363,7 @@ export async function checkoutSessionCompleted( linkId, }); - const [_sale, linkUpdated, workspace] = await Promise.all([ + const [_sale, linkUpdated] = await Promise.all([ recordSale(saleData), // update link stats @@ -495,11 +508,16 @@ export async function checkoutSessionCompleted( async function attributeViaPromoCode({ promotionCodeId, stripeAccountId, + workspace, mode, charge, }: { promotionCodeId: string; stripeAccountId: string; + workspace: Pick< + Project, + "id" | "defaultProgramId" | "stripeConnectId" | "webhookEnabled" + >; mode: StripeMode; charge: Stripe.Checkout.Session; }) { @@ -517,26 +535,6 @@ async function attributeViaPromoCode({ return null; } - // Find the workspace - const workspace = await prisma.project.findUnique({ - where: { - stripeConnectId: stripeAccountId, - }, - select: { - id: true, - stripeConnectId: true, - defaultProgramId: true, - webhookEnabled: true, - }, - }); - - if (!workspace) { - console.log( - `Workspace with stripeConnectId ${stripeAccountId} not found, skipping...`, - ); - return null; - } - if (!workspace.defaultProgramId) { console.log( `Workspace with stripeConnectId ${stripeAccountId} has no default program, skipping...`, From 2d0ead091cc8a0ed28edb9f263640632774f4c24 Mon Sep 17 00:00:00 2001 From: Steven Tey Date: Thu, 27 Nov 2025 14:35:45 -0800 Subject: [PATCH 2/2] guard against undefined stripeCustomerEmail --- .../webhook/checkout-session-completed.ts | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.ts b/apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.ts index c5db385babe..e4be287a6e2 100644 --- a/apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.ts +++ b/apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.ts @@ -99,9 +99,13 @@ export async function checkoutSessionCompleted( { externalId: clickEvent.click_id, }, - { - externalId: stripeCustomerEmail, - }, + ...(stripeCustomerEmail + ? [ + { + externalId: stripeCustomerEmail, + }, + ] + : []), ], }, }); @@ -212,10 +216,14 @@ export async function checkoutSessionCompleted( { stripeCustomerId, }, - { - projectId: workspace.id, - email: stripeCustomerEmail, - }, + ...(stripeCustomerEmail + ? [ + { + projectId: workspace.id, + email: stripeCustomerEmail, + }, + ] + : []), ], }, });