-
Notifications
You must be signed in to change notification settings - Fork 2.7k
fix(stripe): accept snake_case dub_customer_id as fallback #2813
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
fix(stripe): accept snake_case dub_customer_id as fallback #2813
Conversation
WalkthroughAdds a fallback to read Stripe metadata key dub_customer_id wherever dubCustomerId was previously used for identifying the external/dub customer ID across checkout session completed, customer created/updated webhooks, and shared utils. No exported signatures changed; primary flows otherwise unchanged. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant Stripe as Stripe
participant Webhook as Webhook Endpoint
participant Handler as Handler (checkout/customer/*)
participant Utils as Utils.createNewCustomer
participant DB as Database
Stripe->>Webhook: Event (checkout.completed / customer.created / customer.updated)
Webhook->>Handler: Parse event
alt Extract external/dub customer ID
Handler->>Handler: Read metadata.dubCustomerId
opt Fallback
Handler->>Handler: If missing, read metadata.dub_customer_id
end
end
alt Customer exists
Handler->>DB: Lookup/update by projectConnectId / stripeCustomerId
DB-->>Handler: Match found / updated
else Not found
Handler->>Utils: createNewCustomer(event, extracted externalId)
Utils->>DB: Insert customer and related records
DB-->>Utils: Created
Utils-->>Handler: Result
end
Handler-->>Stripe: 200 OK
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Suggested reviewers
Poem
✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
@Velka-DEV is attempting to deploy a commit to the Dub Team on Vercel. A member of the Team first needs to authorize it. |
export async function checkoutSessionCompleted(event: Stripe.Event) { | ||
let charge = event.data.object as Stripe.Checkout.Session; | ||
let dubCustomerId = charge.metadata?.dubCustomerId; | ||
let dubCustomerId = charge.metadata?.dubCustomerId || charge.metadata?.dub_customer_id; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The checkout session handler has two places where it accesses connectedCustomer.metadata.dubCustomerId
directly without the fallback logic.
View Details
📝 Patch Details
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 80d634a6a..434a66d81 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
@@ -178,11 +178,11 @@ export async function checkoutSessionCompleted(event: Stripe.Event) {
livemode: event.livemode,
});
- if (!connectedCustomer || !connectedCustomer.metadata.dubCustomerId) {
+ if (!connectedCustomer || (!connectedCustomer.metadata.dubCustomerId && !connectedCustomer.metadata.dub_customer_id)) {
return `dubCustomerId not found in Stripe checkout session metadata (nor is it available in Dub, or on the connected customer ${stripeCustomerId}) and client_reference_id is not a dub_id, skipping...`;
}
- dubCustomerId = connectedCustomer.metadata.dubCustomerId;
+ dubCustomerId = connectedCustomer.metadata.dubCustomerId || connectedCustomer.metadata.dub_customer_id;
customer = await updateCustomerWithStripeCustomerId({
stripeAccountId,
dubCustomerId,
Analysis
Stripe Metadata Fallback Pattern Inconsistency
Bug Description
The checkout session completion handler in apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.ts
had inconsistent metadata access patterns for Stripe customer metadata. While line 34 correctly implemented a fallback from dubCustomerId
to dub_customer_id
, lines 181 and 185 were missing this fallback logic.
Technical Details
Lines Affected
- Line 181: Conditional check
!connectedCustomer.metadata.dubCustomerId
- Line 185: Assignment
dubCustomerId = connectedCustomer.metadata.dubCustomerId
Root Cause
The code was inconsistent in how it accessed Stripe customer metadata. Some parts of the codebase (like line 34 and line 19 in utils.ts
) used the proper fallback pattern:
stripeCustomer.metadata?.dubCustomerId || stripeCustomer.metadata?.dub_customer_id
However, lines 181 and 185 only checked for dubCustomerId
without the dub_customer_id
fallback.
Impact
Functional Impact
- Early Return Bug: Line 181's condition would cause the function to return early with an error when a customer only had
dub_customer_id
metadata but notdubCustomerId
metadata - Undefined Assignment: Line 185 would assign
undefined
todubCustomerId
when onlydub_customer_id
was present, leading to downstream processing failures - Inconsistent Behavior: Different parts of the same webhook handler would behave differently based on which metadata field was used
Business Impact
- Payment processing failures for customers using the
dub_customer_id
metadata pattern - Loss of conversion tracking data for affected customers
- Potential revenue attribution issues in analytics
Solution
Applied consistent fallback pattern across both problematic lines:
// Line 181: Updated condition to check both metadata fields
if (!connectedCustomer || (!connectedCustomer.metadata.dubCustomerId && !connectedCustomer.metadata.dub_customer_id)) {
// Line 185: Updated assignment to use fallback pattern
dubCustomerId = connectedCustomer.metadata.dubCustomerId || connectedCustomer.metadata.dub_customer_id;
This ensures that the checkout session completion handler consistently handles both dubCustomerId
and dub_customer_id
metadata fields, matching the pattern already established elsewhere in the codebase.
Validation Evidence
The fix was validated by:
- Code Pattern Analysis: Confirmed the fallback pattern exists and is used in
utils.ts
line 19 - Codebase Consistency: Verified line 34 in the same file already uses the correct pattern
- Logic Verification: Ensured the fix maintains the same logical flow while adding proper fallback support
The inconsistency was real and could cause functional failures in production scenarios where customers are created with dub_customer_id
instead of dubCustomerId
metadata.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.ts (2)
181-186
: Connected customer still only checks camelCase — add the same fallback.When pulling the external ID from the connected Stripe customer, only
metadata.dubCustomerId
is considered; snake_case customers will be skipped.Apply this minimal diff:
- if (!connectedCustomer || !connectedCustomer.metadata.dubCustomerId) { - return `dubCustomerId not found in Stripe checkout session metadata (nor is it available in Dub, or on the connected customer ${stripeCustomerId}) and client_reference_id is not a dub_id, skipping...`; - } - - dubCustomerId = connectedCustomer.metadata.dubCustomerId; + const connectedExternalId = + connectedCustomer?.metadata?.dubCustomerId || + connectedCustomer?.metadata?.dub_customer_id; + if (!connectedExternalId) { + return `dubCustomerId/dub_customer_id not found in Stripe checkout session metadata (nor is it available in Dub, or on the connected customer ${stripeCustomerId}) and client_reference_id is not a dub_id, skipping...`; + } + dubCustomerId = connectedExternalId;
197-203
: Potential runtime crash if no lead is found for customer.
getLeadEvent(...).then(res => res.data[0])
can beundefined
, andleadEvent.link_id
will throw. Guard before use.Apply this diff:
leadEvent = await getLeadEvent({ customerId: customer.id }).then( (res) => res.data[0], ); + if (!leadEvent) { + return `Lead event for Dub customer ${customer.id} not found, skipping...`; + } + linkId = leadEvent.link_id;
🧹 Nitpick comments (6)
apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.ts (2)
204-204
: Clarify message to reflect both supported keys.Aligns logs with new behavior.
- return "No dubCustomerId or stripeCustomerId found in Stripe checkout session metadata, skipping..."; + return "No dubCustomerId/dub_customer_id or stripeCustomerId found in Stripe checkout session metadata, skipping...";
34-34
: DRY: extract a tiny helper to read the external ID from Stripe metadata.This logic now exists in multiple places; a single helper avoids drift.
Add in a shared utils file (or here) and reuse:
export const getDubExternalId = (meta?: Record<string, string> | null) => meta?.dubCustomerId || meta?.dub_customer_id;Then replace usages with
getDubExternalId(charge.metadata)
andgetDubExternalId(connectedCustomer?.metadata)
.Also applies to: 181-186
apps/web/app/(ee)/api/stripe/integration/webhook/customer-created.ts (1)
9-9
: Good addition — fallback accepted.Snake_case support added without changing control flow.
Optionally replace inline OR-chain with a shared
getDubExternalId(...)
helper to keep behavior consistent across handlers.apps/web/app/(ee)/api/stripe/integration/webhook/utils.ts (2)
221-229
: Guard against empty subscription items.Avoids a possible runtime error if
items.data
is empty orprice
is missing.- return subscription.items.data[0].price.product as string; + const product = subscription.items.data[0]?.price?.product as string | undefined; + return product ?? null;
48-56
: externalId is optional in the Prisma schema; consider a fallback
TheexternalId
field inpackages/prisma/schema/customer.prisma
is declared asString?
, so creating a customer without it won’t error. If you’d like a stable identifier for linkage, you can default toclickId
whendubCustomerExternalId
is missing:@@ apps/web/app/(ee)/api/stripe/integration/webhook/utils.ts:48-56 - externalId: dubCustomerExternalId, + externalId: dubCustomerExternalId ?? clickId,apps/web/app/(ee)/api/stripe/integration/webhook/customer-updated.ts (1)
9-9
: Good fallback to snake_case key.Consistent with the rest of the PR.
Optionally use a shared
getDubExternalId(...)
helper for parity with other handlers.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (4)
apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.ts
(1 hunks)apps/web/app/(ee)/api/stripe/integration/webhook/customer-created.ts
(1 hunks)apps/web/app/(ee)/api/stripe/integration/webhook/customer-updated.ts
(1 hunks)apps/web/app/(ee)/api/stripe/integration/webhook/utils.ts
(1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Vade Review
🔇 Additional comments (2)
apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.ts (1)
34-34
: Good fallback to snake_case key.The
dub_customer_id
fallback is correctly added and preserves existing behavior.apps/web/app/(ee)/api/stripe/integration/webhook/utils.ts (1)
19-21
: Good: consistent fallback when creating new customers.This keeps
createNewCustomer
aligned with the other handlers.
This PR adds support for using dub_customer_id as a fallback when processing events from the Stripe webhook integration.
This is useful for libraries that normalize checkoutSessionParams to follow Stripe’s snake_case convention.
Summary by CodeRabbit
New Features
Bug Fixes
Chores