Skip to content

Conversation

Velka-DEV
Copy link

@Velka-DEV Velka-DEV commented Sep 5, 2025

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

    • None
  • Bug Fixes

    • Improved Stripe integration compatibility by recognizing both dubCustomerId and dub_customer_id in metadata.
    • Ensures reliable customer identification during checkout completion, customer creation, and customer updates.
    • Reduces missed customer associations and improves sync accuracy across connected projects.
  • Chores

    • No user-facing changes beyond reliability improvements.

Copy link
Contributor

coderabbitai bot commented Sep 5, 2025

Walkthrough

Adds 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

Cohort / File(s) Summary
Stripe webhook metadata fallback
apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.ts, apps/web/app/(ee)/api/stripe/integration/webhook/customer-created.ts, apps/web/app/(ee)/api/stripe/integration/webhook/customer-updated.ts, apps/web/app/(ee)/api/stripe/integration/webhook/utils.ts
Add fallback to metadata.dub_customer_id when metadata.dubCustomerId is absent for external/dub customer ID extraction; no other logic or signatures changed.

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
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Suggested reviewers

  • steven-tey
  • devkiran

Poem

A twitch of whiskers, swift and spry,
I nosed two keys where one ran dry—
dubCustomerId? If not, I try
dub_customer_id (oh my!).
With metadata tidied, I hop with glee,
Webhooks aligned—carrots for me! 🥕🐇

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@CLAassistant
Copy link

CLAassistant commented Sep 5, 2025

CLA assistant check
All committers have signed the CLA.

Copy link
Contributor

vercel bot commented Sep 5, 2025

@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;
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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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 be undefined, and leadEvent.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) and getDubExternalId(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 or price 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
The externalId field in packages/prisma/schema/customer.prisma is declared as String?, so creating a customer without it won’t error. If you’d like a stable identifier for linkage, you can default to clickId when dubCustomerExternalId 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 600d334 and 89505f9.

📒 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants