Skip to content

Add customer email to the Postback payload#3498

Open
devkiran wants to merge 4 commits intomainfrom
postback-customer-email
Open

Add customer email to the Postback payload#3498
devkiran wants to merge 4 commits intomainfrom
postback-customer-email

Conversation

@devkiran
Copy link
Copy Markdown
Collaborator

@devkiran devkiran commented Feb 24, 2026

Summary by CodeRabbit

  • New Features

    • Partner postbacks now include programId and use program-specific context for enrichment, honoring program-level customer data sharing.
    • Customer email added to conversion and commission event payloads and sample events.
  • Bug Fixes

    • Partner postbacks are only sent when both partner ID and program ID are present, preventing incomplete deliveries and ensuring consistent payloads.

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Feb 24, 2026

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

Project Deployment Actions Updated (UTC)
dub Ready Ready Preview Mar 24, 2026 0:18am

Request Review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 24, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: be5ffec3-4877-49f2-b5cd-b98c4c3a5ee8

📥 Commits

Reviewing files that changed from the base of the PR and between e86f885 and 63c3614.

📒 Files selected for processing (4)
  • apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.ts
  • apps/web/app/(ee)/api/stripe/integration/webhook/invoice-paid.ts
  • apps/web/lib/api/conversions/track-sale.ts
  • apps/web/lib/integrations/shopify/create-sale.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • apps/web/lib/integrations/shopify/create-sale.ts
  • apps/web/lib/api/conversions/track-sale.ts
  • apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.ts
  • apps/web/app/(ee)/api/stripe/integration/webhook/invoice-paid.ts

📝 Walkthrough

Walkthrough

Tightens partner postback emission to require both partnerId and programId, includes programId in partner postback payloads, threads program-level customerDataSharingEnabledAt into postback enrichment, adds customer masking/random-name logic, and exposes email in postback customer schemas and sample events.

Changes

Cohort / File(s) Summary
Stripe webhooks & new-customer
apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.ts, apps/web/app/(ee)/api/stripe/integration/webhook/invoice-paid.ts, apps/web/app/(ee)/api/stripe/integration/webhook/utils/create-new-customer.ts
Partner postbacks now require both link.partnerId and linkUpdated.programId; when sent, payloads include programId.
Conversion tracking
apps/web/lib/api/conversions/track-lead.ts, apps/web/lib/api/conversions/track-sale.ts
Gates partner postback on both link.partnerId and link.programId; adds programId to lead/sale postback payloads.
Shopify integrations
apps/web/lib/integrations/shopify/create-lead.ts, apps/web/lib/integrations/shopify/create-sale.ts
Partner postbacks emitted only when both partnerId and programId exist; programId added to postback payloads.
Partner commission
apps/web/lib/partners/create-partner-commission.ts
Includes programId in commission-created partner postback payload.
Postback enrichment plumbing
apps/web/lib/postback/api/postback-event-enrichers.ts, apps/web/lib/postback/api/send-partner-postback.ts
Adds PostbackEnricherContext (customerDataSharingEnabledAt), extends enricher signatures to accept optional context, sendPartnerPostback accepts programId and looks up ProgramEnrollment to obtain sharing flag; passes context to enrichers and applies transformCustomer to mask/fallback customer data.
Postback schemas & samples
apps/web/lib/postback/schemas.ts, apps/web/lib/postback/sample-events/lead-created.json, apps/web/lib/postback/sample-events/sale-created.json, apps/web/lib/postback/sample-events/commission-created.json
Adds email to postbackCustomerSchema and sample event customer objects.

Sequence Diagram(s)

sequenceDiagram
    participant Trigger as Conversion Trigger
    participant SendPB as sendPartnerPostback
    participant DB as ProgramEnrollment DB
    participant Enricher as PostbackEventEnricher
    participant Transformer as transformCustomer
    participant Partner as Partner Endpoint

    Trigger->>SendPB: call(event, data, partnerId, programId)
    SendPB->>DB: query ProgramEnrollment(partnerId, programId)
    alt enrollment found
        DB-->>SendPB: { customerDataSharingEnabledAt }
        SendPB->>Enricher: enrich(event, data, { customerDataSharingEnabledAt })
        Enricher->>Transformer: transformCustomer(customer, customerDataSharingEnabledAt)
        alt sharing enabled
            Transformer-->>Enricher: original customer/email
        else sharing disabled
            Transformer-->>Enricher: masked email or random name
        end
        Enricher-->>SendPB: enriched payload
        SendPB->>Partner: POST enriched payload (includes programId)
    else enrollment missing
        DB-->>SendPB: not found (log & abort)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • steven-tey
  • TWilson023

Poem

🐇 I hop through webhooks, neat and spry,
Threading programId as I fly.
If sharing's off I'll hide your name,
Else send the customer, true and plain.
Hop, enrich, and mail—a partner's delight.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add customer email to the Postback payload' directly and accurately summarizes the main change across the entire changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch postback-customer-email

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.

Copy link
Copy Markdown
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: 1

🧹 Nitpick comments (3)
apps/web/lib/integrations/shopify/create-lead.ts (1)

121-154: Inconsistent condition key order between the two guard blocks.

Line 121 checks link.partnerId && link.programId while line 137 checks link.programId && link.partnerId. These are semantically equivalent, but aligning them avoids confusion during future diffs.

♻️ Proposed fix
-      ...(link.programId && link.partnerId
+      ...(link.partnerId && link.programId
         ? [
             syncPartnerLinksStats({
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/lib/integrations/shopify/create-lead.ts` around lines 121 - 154, Two
adjacent guard blocks use the same boolean check but in different operand
orders; make them consistent to avoid noisy diffs by using the same key order in
both places (e.g., change the second guard to use link.partnerId &&
link.programId). Update the conditional that guards the syncPartnerLinksStats
and prisma.customer.update calls so it matches the first guard that wraps
sendPartnerPostback (both should check link.partnerId && link.programId),
leaving sendPartnerPostback, syncPartnerLinksStats, and prisma.customer.update
unchanged.
apps/web/app/(ee)/api/stripe/integration/webhook/invoice-paid.ts (1)

299-313: LGTM. Minor: unnecessary optional chaining on linkUpdated.

linkUpdated is the direct result of prisma.link.update(), so it's always non-null; linkUpdated?.programId can safely drop the ?.. Non-blocking.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/app/`(ee)/api/stripe/integration/webhook/invoice-paid.ts around
lines 299 - 313, The conditional uses unnecessary optional chaining on
linkUpdated (result of prisma.link.update) — change `linkUpdated?.programId` to
`linkUpdated.programId` in the ternary that builds the array for
sendPartnerPostback so the condition reads `...(link?.partnerId &&
linkUpdated.programId ? [ sendPartnerPostback({ ... }) ] : []),` referencing the
existing variables `link`, `linkUpdated`, and the call to `sendPartnerPostback`.
apps/web/lib/partners/create-partner-commission.ts (1)

364-372: Redundant customer override after spreading commission.

commission already includes the customer relation (from include: { customer: true } at line 294), so explicitly restating it is a no-op.

♻️ Proposed fix
  sendPartnerPostback({
    partnerId,
    programId,
    event: "commission.created",
    data: {
-     ...commission,
-     customer: commission.customer,
+     ...commission,
    },
  }),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/lib/partners/create-partner-commission.ts` around lines 364 - 372,
The data object passed to sendPartnerPostback redundantly sets customer:
commission.customer after spreading commission; remove the explicit "customer:
commission.customer" property so the spread of commission already provides the
customer relation and avoid the no-op override in sendPartnerPostback({
partnerId, programId, event: "commission.created", data: { ...commission } }).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/lib/postback/api/postback-event-enrichers.ts`:
- Around line 58-78: The transformCustomer function is incorrectly populating
the email field with customer.name or a generated name when customer.email is
missing; change it so email remains null/undefined when no valid customer.email
exists and move the fallback logic to the name field instead. Specifically, in
transformCustomer ensure you only set email from customer.email (masked when
customerDataSharingEnabledAt is falsy) or leave it unset/null if absent, and set
name to customer.name || generateRandomName() so consumers get a valid name
fallback without polluting the email field; update the returned object (from
transformCustomer) to spread ...customer and explicitly override name and email
accordingly.

---

Nitpick comments:
In `@apps/web/app/`(ee)/api/stripe/integration/webhook/invoice-paid.ts:
- Around line 299-313: The conditional uses unnecessary optional chaining on
linkUpdated (result of prisma.link.update) — change `linkUpdated?.programId` to
`linkUpdated.programId` in the ternary that builds the array for
sendPartnerPostback so the condition reads `...(link?.partnerId &&
linkUpdated.programId ? [ sendPartnerPostback({ ... }) ] : []),` referencing the
existing variables `link`, `linkUpdated`, and the call to `sendPartnerPostback`.

In `@apps/web/lib/integrations/shopify/create-lead.ts`:
- Around line 121-154: Two adjacent guard blocks use the same boolean check but
in different operand orders; make them consistent to avoid noisy diffs by using
the same key order in both places (e.g., change the second guard to use
link.partnerId && link.programId). Update the conditional that guards the
syncPartnerLinksStats and prisma.customer.update calls so it matches the first
guard that wraps sendPartnerPostback (both should check link.partnerId &&
link.programId), leaving sendPartnerPostback, syncPartnerLinksStats, and
prisma.customer.update unchanged.

In `@apps/web/lib/partners/create-partner-commission.ts`:
- Around line 364-372: The data object passed to sendPartnerPostback redundantly
sets customer: commission.customer after spreading commission; remove the
explicit "customer: commission.customer" property so the spread of commission
already provides the customer relation and avoid the no-op override in
sendPartnerPostback({ partnerId, programId, event: "commission.created", data: {
...commission } }).

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fee2b96 and 2483af1.

📒 Files selected for processing (14)
  • apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.ts
  • apps/web/app/(ee)/api/stripe/integration/webhook/invoice-paid.ts
  • apps/web/app/(ee)/api/stripe/integration/webhook/utils/create-new-customer.ts
  • apps/web/lib/api/conversions/track-lead.ts
  • apps/web/lib/api/conversions/track-sale.ts
  • apps/web/lib/integrations/shopify/create-lead.ts
  • apps/web/lib/integrations/shopify/create-sale.ts
  • apps/web/lib/partners/create-partner-commission.ts
  • apps/web/lib/postback/api/postback-event-enrichers.ts
  • apps/web/lib/postback/api/send-partner-postback.ts
  • apps/web/lib/postback/sample-events/commission-created.json
  • apps/web/lib/postback/sample-events/lead-created.json
  • apps/web/lib/postback/sample-events/sale-created.json
  • apps/web/lib/postback/schemas.ts

Comment thread apps/web/lib/postback/api/postback-event-enrichers.ts
@devkiran devkiran requested a review from steven-tey February 24, 2026 10:55
@devkiran
Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review please

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 25, 2026

@devkiran I'll perform a full review of all the changes in this PR.

✅ Actions performed

Full review triggered.

Copy link
Copy Markdown
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.

♻️ Duplicate comments (1)
apps/web/lib/postback/api/postback-event-enrichers.ts (1)

58-79: email field is populated with non-email values when customer.email is absent.

When customer.email is falsy, line 73 assigns customer?.name || generateRandomName() to email. This puts a person's name (or a random name) into the email field, which is semantically incorrect and will confuse postback consumers expecting an email address or null/undefined.

🐛 Proposed fix
 function transformCustomer({
   customer,
   customerDataSharingEnabledAt,
 }: {
   customer: Record<string, any>;
   customerDataSharingEnabledAt: PostbackEnricherContext["customerDataSharingEnabledAt"];
 }) {
   if (!customer) {
     return null;
   }

-  const email = customer?.email
-    ? customerDataSharingEnabledAt
-      ? customer.email
-      : customer.email.replace(/(?<=^.).+(?=.@)/, "****")
-    : customer?.name || generateRandomName();
+  const email = customer?.email
+    ? customerDataSharingEnabledAt
+      ? customer.email
+      : customer.email.replace(/(?<=^.).+(?=.@)/, "****")
+    : null;

   return {
     ...customer,
     email,
   };
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/lib/postback/api/postback-event-enrichers.ts` around lines 58 - 79,
In transformCustomer, the email variable is being set to customer?.name or
generateRandomName() when customer.email is falsy; change this so the email
field is either the actual (or obfuscated) customer.email or null/undefined
instead of a non-email string. Update the email assignment in transformCustomer
to: if customer.email is truthy use customerDataSharingEnabledAt ?
customer.email : obfuscated email, otherwise set email to null (or undefined) so
the returned object from transformCustomer does not place names/random strings
into the email property.
🧹 Nitpick comments (4)
apps/web/app/(ee)/api/stripe/integration/webhook/utils/create-new-customer.ts (1)

154-158: Use one source object for IDs to avoid stale/mixed reads.

Line 154 and Line 157 mix link and linkUpdated. Prefer using linkUpdated consistently for both guard and payload IDs.

♻️ Suggested refactor
-      ...(link.partnerId && linkUpdated.programId
+      ...(linkUpdated.partnerId && linkUpdated.programId
         ? [
             sendPartnerPostback({
-              partnerId: link.partnerId,
+              partnerId: linkUpdated.partnerId,
               programId: linkUpdated.programId,
               event: "lead.created",
               data: {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/web/app/`(ee)/api/stripe/integration/webhook/utils/create-new-customer.ts
around lines 154 - 158, The guard and payload are mixing two sources (link vs
linkUpdated) which can cause stale/mixed reads; update the conditional and
payload to use linkUpdated consistently so both the truthy check and the
sendPartnerPostback call reference the same object (use linkUpdated.partnerId
and linkUpdated.programId) — locate the conditional that opens with
...(link.partnerId && linkUpdated.programId ? and the sendPartnerPostback
invocation and replace the guard to check linkUpdated.partnerId and pass
partnerId/programId from linkUpdated to sendPartnerPostback.
apps/web/lib/integrations/shopify/create-lead.ts (1)

121-121: Nit: Inconsistent operand ordering between the two guard conditions.

Line 121 uses link.partnerId && link.programId while Line 137 uses link.programId && link.partnerId. Consider picking one order for consistency.

Also applies to: 137-137

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/lib/integrations/shopify/create-lead.ts` at line 121, Unify the
boolean guard ordering in create-lead.ts by choosing one consistent operand
order for the link checks; replace either occurrences of "link.partnerId &&
link.programId" or "link.programId && link.partnerId" so both use the same order
(e.g., always "link.partnerId && link.programId") in the relevant conditional
expressions within the createLead handler/function to keep the codebase
consistent and readable.
apps/web/lib/integrations/shopify/create-sale.ts (1)

216-230: LGTM — postback gated and programId propagated correctly.

The condition properly requires both IDs, and programId is passed to sendPartnerPostback.

Minor nit: link?.partnerId && link?.programId uses optional chaining here while create-lead.ts (Line 121) uses link.partnerId && link.programId without it. Since link is always defined at this point (result of prisma.link.update from the Promise.all above), the optional chaining is unnecessary. Consider aligning the style across both Shopify integration files.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/lib/integrations/shopify/create-sale.ts` around lines 216 - 230, The
guard currently uses optional chaining in the expression link?.partnerId &&
link?.programId even though link is guaranteed to be defined here (result of
prisma.link.update); change the condition to use direct property access
(link.partnerId && link.programId) to match the style used in create-lead.ts and
remove the unnecessary optional chaining; update the conditional around
sendPartnerPostback in create-sale.ts to reference link.partnerId and
link.programId without the "?" to align both Shopify integration files.
apps/web/lib/postback/api/send-partner-postback.ts (1)

46-72: Early return when enrollment is missing silently drops the postback.

When programId is provided but the enrollment lookup fails (Line 64-68), the function returns without sending any postback. This is a reasonable guard, but note that any transient DB issue or data inconsistency (e.g., race condition during enrollment creation) will silently suppress the postback with only a console.warn. Consider whether this warrants a more visible alert (e.g., logging to your error tracking) for production observability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/lib/postback/api/send-partner-postback.ts` around lines 46 - 72, The
current early return when prisma.programEnrollment.findUnique returns no result
silently drops the postback; in sendPartnerPostback replace the console.warn +
return with a visible error/alert: log an error via the app's error logger
(e.g., processLogger.error) and send the event to error tracking (e.g.,
Sentry.captureException or your notifyError helper) including partnerId,
programId and context, or alternatively throw a retryable error so the postback
can be retried, ensuring programEnrollment lookup failures are observable and
not silently ignored.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@apps/web/lib/postback/api/postback-event-enrichers.ts`:
- Around line 58-79: In transformCustomer, the email variable is being set to
customer?.name or generateRandomName() when customer.email is falsy; change this
so the email field is either the actual (or obfuscated) customer.email or
null/undefined instead of a non-email string. Update the email assignment in
transformCustomer to: if customer.email is truthy use
customerDataSharingEnabledAt ? customer.email : obfuscated email, otherwise set
email to null (or undefined) so the returned object from transformCustomer does
not place names/random strings into the email property.

---

Nitpick comments:
In
`@apps/web/app/`(ee)/api/stripe/integration/webhook/utils/create-new-customer.ts:
- Around line 154-158: The guard and payload are mixing two sources (link vs
linkUpdated) which can cause stale/mixed reads; update the conditional and
payload to use linkUpdated consistently so both the truthy check and the
sendPartnerPostback call reference the same object (use linkUpdated.partnerId
and linkUpdated.programId) — locate the conditional that opens with
...(link.partnerId && linkUpdated.programId ? and the sendPartnerPostback
invocation and replace the guard to check linkUpdated.partnerId and pass
partnerId/programId from linkUpdated to sendPartnerPostback.

In `@apps/web/lib/integrations/shopify/create-lead.ts`:
- Line 121: Unify the boolean guard ordering in create-lead.ts by choosing one
consistent operand order for the link checks; replace either occurrences of
"link.partnerId && link.programId" or "link.programId && link.partnerId" so both
use the same order (e.g., always "link.partnerId && link.programId") in the
relevant conditional expressions within the createLead handler/function to keep
the codebase consistent and readable.

In `@apps/web/lib/integrations/shopify/create-sale.ts`:
- Around line 216-230: The guard currently uses optional chaining in the
expression link?.partnerId && link?.programId even though link is guaranteed to
be defined here (result of prisma.link.update); change the condition to use
direct property access (link.partnerId && link.programId) to match the style
used in create-lead.ts and remove the unnecessary optional chaining; update the
conditional around sendPartnerPostback in create-sale.ts to reference
link.partnerId and link.programId without the "?" to align both Shopify
integration files.

In `@apps/web/lib/postback/api/send-partner-postback.ts`:
- Around line 46-72: The current early return when
prisma.programEnrollment.findUnique returns no result silently drops the
postback; in sendPartnerPostback replace the console.warn + return with a
visible error/alert: log an error via the app's error logger (e.g.,
processLogger.error) and send the event to error tracking (e.g.,
Sentry.captureException or your notifyError helper) including partnerId,
programId and context, or alternatively throw a retryable error so the postback
can be retried, ensuring programEnrollment lookup failures are observable and
not silently ignored.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fee2b96 and 2483af1.

📒 Files selected for processing (14)
  • apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.ts
  • apps/web/app/(ee)/api/stripe/integration/webhook/invoice-paid.ts
  • apps/web/app/(ee)/api/stripe/integration/webhook/utils/create-new-customer.ts
  • apps/web/lib/api/conversions/track-lead.ts
  • apps/web/lib/api/conversions/track-sale.ts
  • apps/web/lib/integrations/shopify/create-lead.ts
  • apps/web/lib/integrations/shopify/create-sale.ts
  • apps/web/lib/partners/create-partner-commission.ts
  • apps/web/lib/postback/api/postback-event-enrichers.ts
  • apps/web/lib/postback/api/send-partner-postback.ts
  • apps/web/lib/postback/sample-events/commission-created.json
  • apps/web/lib/postback/sample-events/lead-created.json
  • apps/web/lib/postback/sample-events/sale-created.json
  • apps/web/lib/postback/schemas.ts

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