Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@steven-tey
Copy link
Collaborator

@steven-tey steven-tey commented Oct 4, 2025

Summary by CodeRabbit

  • New Features

    • Adds promotion code-based attribution during Stripe checkout when a matching customer isn’t found, improving capture of referrals and partner rewards.
  • Bug Fixes

    • Reduces missed attributions by falling back to promotion codes, ensuring purchases are properly linked.
    • Improves tracking continuity for associated links and lead events.
    • Prevents unintended skips in checkout attribution, resulting in more accurate analytics and payouts.

@vercel
Copy link
Contributor

vercel bot commented Oct 4, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Updated (UTC)
dub Ready Ready Preview Oct 4, 2025 0:07am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 4, 2025

Walkthrough

Stripe checkout-session-completed webhook now attempts promotion-code-based attribution when a Dub customer is not found. If a promotionCodeId exists and attribution succeeds, it updates linkId, customer, clickEvent, and leadEvent, and disables the lead webhook. If attribution fails, it returns a failure and skips. This branch is added in two not-found paths.

Changes

Cohort / File(s) Summary of Edits
Stripe checkout webhook attribution logic
apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.ts
Added promotion-code attribution fallback in two customer-not-found branches; on success, updates linkId/customer/clickEvent/leadEvent and disables lead webhook; on failure, returns a skip with failure message instead of immediate not-found skip.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Stripe as Stripe
  participant Webhook as CheckoutSessionCompleted Handler
  participant DB as Database
  participant Promo as Promotion Attribution
  participant Lead as Lead Webhook

  Stripe->>Webhook: POST /webhook (checkout.session.completed)
  Webhook->>DB: Find Dub customer by externalId / Stripe customerId
  alt Customer found
    Webhook->>DB: Proceed with standard updates
    Webhook-->>Stripe: 200 OK
  else Customer not found
    alt promotionCodeId present
      Webhook->>Promo: Attribute via promotion code
      alt Attribution success
        Webhook->>DB: Update linkId, customer, clickEvent, leadEvent
        Webhook-x Lead: Disable lead webhook
        Webhook-->>Stripe: 200 OK (attributed)
      else Attribution failure
        Webhook-->>Stripe: 200 OK (skip: attribution failed)
      end
    else No promotion code
      Webhook-->>Stripe: 200 OK (skip: customer not found)
    end
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

A twitch of whiskers, code hops through the night,
I sniff a promo trail—aha! The path is right.
Lost customers found, with links neatly tied,
Lead bells silenced where attributions abide.
Thump-thump—webhook’s done, tail high with pride.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Title Check ⚠️ Warning The title is truncated, contains a typo, and fails to clearly and concisely summarize the primary change related to promotion code attribution when a dubCustomerExternalId is provided but no matching customer is found. Revise the title to a clear, complete sentence that highlights the main change, for example: “Add promotion code attribution fallback when dubCustomerExternalId is provided but no existing customer is found.”
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch checkout-promocode

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

Comment @coderabbitai help to get the list of available commands and usage tips.

@steven-tey steven-tey changed the title Add attributeViaPromoCode to cases where dubCustmomerExternalId was p… Add attributeViaPromoCode to cases where dubCustomerId is provided Oct 4, 2025
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)

592-607: Add idempotency checks for customer creation.

The attributeViaPromoCode function creates a new customer without checking if a customer with the same stripeCustomerId already exists. If the Stripe webhook is retried (due to network issues or timeout), this could create duplicate customers.

Consider checking for an existing customer before creation:

+  // Check if customer already exists
+  const existingCustomer = await prisma.customer.findUnique({
+    where: {
+      stripeCustomerId: charge.customer as string,
+    },
+  });
+
+  if (existingCustomer) {
+    return {
+      linkId,
+      customer: existingCustomer,
+      clickEvent,
+      leadEvent: await getLeadEvent({ customerId: existingCustomer.id }).then(
+        (res) => res.data[0],
+      ),
+    };
+  }
+
   const customer = await prisma.customer.create({

581-620: Add error handling for recordFakeClick and recordLead.

The recordFakeClick (line 581) and recordLead (line 620) calls are not wrapped in try-catch blocks. If either operation fails, the function will throw an exception, and the webhook handler will return a 500 error to Stripe, causing unnecessary retries.

Consider adding error handling:

+  let clickEvent;
+  try {
-  const clickEvent = await recordFakeClick({
+    clickEvent = await recordFakeClick({
     link,
     customer: {
       continent: customerAddress?.country
         ? COUNTRIES_TO_CONTINENTS[customerAddress.country]
         : "Unknown",
       country: customerAddress?.country ?? "Unknown",
       region: customerAddress?.state ?? "Unknown",
     },
   });
+  } catch (error) {
+    console.error("Failed to record fake click", error);
+    return null;
+  }

   // ... customer creation ...

+  try {
     await recordLead(leadEvent);
+  } catch (error) {
+    console.error("Failed to record lead", error);
+    // Consider whether to return null or continue
+  }
🧹 Nitpick comments (4)
apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.ts (4)

609-620: Consider idempotency for lead recording.

The function records a lead without checking if one already exists for this customer. If the Stripe webhook is retried, this could result in duplicate lead events in Tinybird.

If Tinybird does not deduplicate based on event_id, consider checking for an existing lead before recording:

// Check if lead already exists
const existingLead = await getLeadEvent({ customerId: customer.id }).then(
  (res) => res.data[0],
);

if (!existingLead) {
  await recordLead(leadEvent);
}

Alternatively, ensure the Redis idempotency check (lines 267-293) covers lead creation as well, or leverage Tinybird's deduplication if available.


522-572: Consider structured error logging.

The function uses console.log for error messages before returning null. In production, these errors might be difficult to track without structured logging or error monitoring.

Consider using a structured logger or error tracking service (e.g., Sentry) to capture these failure scenarios with context:

logger.warn("Promotion code attribution failed", {
  reason: "promotion_code_not_found",
  promotionCodeId,
  stripeAccountId,
});

This will improve observability and make debugging easier.


581-590: Document the "Unknown" fallback for missing address data.

When customer address data is missing from the Stripe checkout session, the code falls back to "Unknown" for continent, country, and region. This is reasonable for handling incomplete data, but it might affect analytics and reporting.

Consider documenting this behavior in a comment to clarify the expected impact on analytics:

// Note: If customer address is missing, we use "Unknown" for geo fields.
// This ensures attribution succeeds but may affect geo-based analytics.
const clickEvent = await recordFakeClick({
  link,
  customer: {
    continent: customerAddress?.country
      ? COUNTRIES_TO_CONTINENTS[customerAddress.country]
      : "Unknown",
    country: customerAddress?.country ?? "Unknown",
    region: customerAddress?.state ?? "Unknown",
  },
});

599-599: Use the stripeCustomerId variable instead of unsafe casting.

Line 599 casts charge.customer as string, but according to Stripe types, charge.customer can be a string | Stripe.Customer | Stripe.DeletedCustomer | null. While the main handler verifies stripeCustomerId is present (line 145), directly casting here is fragile.

Pass stripeCustomerId as a parameter to attributeViaPromoCode and use it directly:

 async function attributeViaPromoCode({
   promotionCodeId,
   stripeAccountId,
   livemode,
   charge,
+  stripeCustomerId,
 }: {
   promotionCodeId: string;
   stripeAccountId: string;
   livemode: boolean;
   charge: Stripe.Checkout.Session;
+  stripeCustomerId: string;
 }) {
   // ...
   const customer = await prisma.customer.create({
     data: {
       // ...
-      stripeCustomerId: charge.customer as string,
+      stripeCustomerId,
       // ...
     },
   });

Then update the callers at lines 180 and 224 to pass stripeCustomerId.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 532cb57 and 5d0d1fd.

📒 Files selected for processing (1)
  • apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.ts (2 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: build
🔇 Additional comments (2)
apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.ts (2)

149-169: LGTM: Clear documentation of promotion code flow.

The updated comments clearly describe the new promotion code attribution logic and the two scenarios where it applies. This improves code readability and maintainability.


179-194: Promo code attribution fallback is sound
shouldSendLeadWebhook correctly prevents duplicate lead.created webhooks after promotion code attribution, and defined leadEvent skips the Tinybird fetch as intended.

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