Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import {
// Handle event "checkout.session.completed"
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;
Copy link
Contributor

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 not dubCustomerId metadata
  • Undefined Assignment: Line 185 would assign undefined to dubCustomerId when only dub_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:

  1. Code Pattern Analysis: Confirmed the fallback pattern exists and is used in utils.ts line 19
  2. Codebase Consistency: Verified line 34 in the same file already uses the correct pattern
  3. 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.

const clientReferenceId = charge.client_reference_id;
const stripeAccountId = event.account as string;
const stripeCustomerId = charge.customer as string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { createNewCustomer } from "./utils";
export async function customerCreated(event: Stripe.Event) {
const stripeCustomer = event.data.object as Stripe.Customer;
const stripeAccountId = event.account as string;
const dubCustomerExternalId = stripeCustomer.metadata?.dubCustomerId;
const dubCustomerExternalId = stripeCustomer.metadata?.dubCustomerId || stripeCustomer.metadata?.dub_customer_id;

if (!dubCustomerExternalId) {
return "External ID not found in Stripe customer metadata, skipping...";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { createNewCustomer } from "./utils";
export async function customerUpdated(event: Stripe.Event) {
const stripeCustomer = event.data.object as Stripe.Customer;
const stripeAccountId = event.account as string;
const dubCustomerExternalId = stripeCustomer.metadata?.dubCustomerId;
const dubCustomerExternalId = stripeCustomer.metadata?.dubCustomerId || stripeCustomer.metadata?.dub_customer_id;

if (!dubCustomerExternalId) {
return "External ID not found in Stripe customer metadata, skipping...";
Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/(ee)/api/stripe/integration/webhook/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import type Stripe from "stripe";
export async function createNewCustomer(event: Stripe.Event) {
const stripeCustomer = event.data.object as Stripe.Customer;
const stripeAccountId = event.account as string;
const dubCustomerExternalId = stripeCustomer.metadata?.dubCustomerId;
const dubCustomerExternalId = stripeCustomer.metadata?.dubCustomerId || stripeCustomer.metadata?.dub_customer_id;
const clickId = stripeCustomer.metadata?.dubClickId;

// The client app should always send dubClickId (dub_id) via metadata
Expand Down