-
Notifications
You must be signed in to change notification settings - Fork 2.8k
notifyPartnerCommission #2805
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
notifyPartnerCommission #2805
Conversation
|
Warning Rate limit exceeded@steven-tey has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 2 minutes and 48 seconds before requesting another review. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. 📒 Files selected for processing (1)
WalkthroughPartner sale notifications were removed and replaced with a commission-centered notification flow. Webhook and sales handlers no longer call notifyPartnerSale; a new notifyPartnerCommission API and updated email templates are introduced and invoked during commission creation. Minor copy tweak in partner welcome email. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User as User/Customer
participant Source as Source (Shopify/Stripe/Conversions)
participant Server as App Server
participant DB as Database
participant Mail as Email Service
participant Hooks as Webhooks/Workflows
User->>Source: Purchase or action
Source->>Server: Emit sale/commission event
Server->>DB: createPartnerCommission(...)
DB-->>Server: Commission record
rect rgba(230,245,255,0.5)
note right of Server: Async side effects (waitUntil)
Server-)Mail: notifyPartnerCommission(program, workspace, commission)
Server-)Hooks: Trigger webhooks, audits, workflows
end
sequenceDiagram
autonumber
participant Stripe as Stripe
participant WH as Webhook handler
participant Work as Workflows
Stripe-->>WH: checkout-session-completed / invoice-paid event
WH-)Work: executeWorkflows()
note right of WH: Partner sale notifications removed from webhook handlers
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
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 (
|
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
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: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/web/lib/api/partners/notify-partner-commission.ts (1)
105-116: Ensure a fallback recipient when no partner users are opted-in.If no partner user has commissionCreated enabled, the partner won’t get notified even if
partner.emailexists.Apply this refactor to add a safe fallback and de-duplicate:
- const partnerEmailsToNotify = partner.users - .map(({ user }) => user.email) - .filter(Boolean) as string[]; + const partnerEmailsToNotify = Array.from( + new Set( + (partner.users.map(({ user }) => user.email).filter(Boolean) as string[]), + ), + ); + const finalPartnerEmails = + partnerEmailsToNotify.length > 0 + ? partnerEmailsToNotify + : partner.email + ? [partner.email] + : [];And use
finalPartnerEmailsbelow:- ...partnerEmailsToNotify.map((email) => ({ + ...finalPartnerEmails.map((email) => ({
🧹 Nitpick comments (7)
apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.ts (1)
367-371: Harden background task with error logging (keep fire-and-forget semantics).Wrap the workflow call in
.catchto avoid unhandled rejections and to log context.- waitUntil( - executeWorkflows({ - trigger: WorkflowTrigger.saleRecorded, - programId: link.programId, - partnerId: link.partnerId, - }), - ); + waitUntil( + executeWorkflows({ + trigger: WorkflowTrigger.saleRecorded, + programId: link.programId, + partnerId: link.partnerId, + }).catch((err) => + console.error("[executeWorkflows] saleRecorded failed", { + err, + programId: link.programId, + partnerId: link.partnerId, + }), + ), + );Also confirm createPartnerCommission is idempotent so workflows aren’t triggered twice on duplicate Stripe events.
apps/web/app/(ee)/api/stripe/integration/webhook/invoice-paid.ts (1)
214-219: Add.catchon background workflow execution.Same rationale as the checkout handler—log but don’t fail the request.
- waitUntil( - executeWorkflows({ - trigger: WorkflowTrigger.saleRecorded, - programId: link.programId, - partnerId: link.partnerId, - }), - ); + waitUntil( + executeWorkflows({ + trigger: WorkflowTrigger.saleRecorded, + programId: link.programId, + partnerId: link.partnerId, + }).catch((err) => + console.error("[executeWorkflows] saleRecorded failed", { + err, + programId: link.programId, + partnerId: link.partnerId, + }), + ), + );packages/email/src/templates/new-commission-alert-partner.tsx (1)
92-96: Fix invalid Tailwind class; optionally use component.Use
font-semibold(nottext-semibold) and drop the conflictingfont-medium.- <a - href={shortLink} - className="text-semibold font-medium text-black underline" - > + <a + href={shortLink} + className="font-semibold text-black underline" + >Optionally:
- <a + <Link href={shortLink} - className="font-semibold text-black underline" + className="font-semibold text-black underline" > {getPrettyUrl(shortLink)} - </a> + </Link>apps/web/lib/api/partners/notify-partner-commission.ts (3)
124-126: Handle missing partner name in owner email subject.Avoid “New commission for null”.
- subject: `New commission for ${partner.name}`, + subject: `New commission${partner.name ? ` for ${partner.name}` : ""}`,
141-144: Fail fast when email client isn’t configured.
resend?.batch.send(...)silently no-ops ifresendis undefined, making issues hard to detect.- await Promise.all( - emailChunks.map((emailChunk) => resend?.batch.send(emailChunk)), - ); + if (!resend) { + console.warn("[notifyPartnerCommission] Resend client not configured; skipping email send."); + return; + } + await Promise.all(emailChunks.map((emailChunk) => resend.batch.send(emailChunk)));
68-79: Drop unnecessary Promise.resolve wrappers.Simplifies readability without changing behavior.
- commission.linkId - ? Promise.resolve( - prisma.link.findUnique({ - where: { - id: commission.linkId, - }, - select: { - shortLink: true, - }, - }), - ) - : Promise.resolve(null), + commission.linkId + ? prisma.link.findUnique({ + where: { id: commission.linkId }, + select: { shortLink: true }, + }) + : null,apps/web/lib/partners/create-partner-commission.ts (1)
243-262: Good to fetch program+workspace in waitUntil; minor select tightening.Nice placement off the request path. You can drop
idfrom the program select since it's unused here to shave payload a bit.I’m also leveraging the prior learning that each workspace has exactly one program—fetching by
programIdand destructuringworkspaceis consistent with that rule.select: { - id: true, name: true, slug: true, logo: true, holdingPeriodDays: true, workspace: { select: { id: true, slug: true, name: true, webhookEnabled: true, }, }, },
📜 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 (9)
apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.ts(1 hunks)apps/web/app/(ee)/api/stripe/integration/webhook/invoice-paid.ts(1 hunks)apps/web/lib/api/conversions/track-sale.ts(1 hunks)apps/web/lib/api/partners/notify-partner-commission.ts(4 hunks)apps/web/lib/integrations/shopify/create-sale.ts(1 hunks)apps/web/lib/partners/create-partner-commission.ts(3 hunks)packages/email/src/templates/new-commission-alert-partner.tsx(2 hunks)packages/email/src/templates/new-sale-alert-program-owner.tsx(2 hunks)packages/email/src/templates/welcome-email-partner.tsx(1 hunks)
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-08-25T17:42:13.600Z
Learnt from: devkiran
PR: dubinc/dub#2736
File: apps/web/lib/api/get-workspace-users.ts:76-83
Timestamp: 2025-08-25T17:42:13.600Z
Learning: Business rule confirmed: Each workspace has exactly one program. The code should always return workspace.programs[0] since there's only one program per workspace.
Applied to files:
apps/web/lib/partners/create-partner-commission.ts
📚 Learning: 2025-06-10T19:16:23.445Z
Learnt from: devkiran
PR: dubinc/dub#2510
File: apps/web/lib/actions/partners/onboard-program.ts:16-19
Timestamp: 2025-06-10T19:16:23.445Z
Learning: Business rule: Each workspace may have at most one program; attempting to create more must be blocked in code.
Applied to files:
apps/web/lib/partners/create-partner-commission.ts
📚 Learning: 2025-05-29T04:45:18.504Z
Learnt from: devkiran
PR: dubinc/dub#2448
File: packages/email/src/templates/partner-program-summary.tsx:0-0
Timestamp: 2025-05-29T04:45:18.504Z
Learning: In the PartnerProgramSummary email template (packages/email/src/templates/partner-program-summary.tsx), the stat titles are hardcoded constants ("Clicks", "Leads", "Sales", "Earnings") that will always match the ICONS object keys after toLowerCase() conversion, so icon lookup failures are not possible.
Applied to files:
packages/email/src/templates/new-commission-alert-partner.tsx
🧬 Code graph analysis (8)
apps/web/lib/api/conversions/track-sale.ts (1)
apps/web/lib/partners/create-partner-commission.ts (1)
createPartnerCommission(25-336)
apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.ts (2)
apps/web/lib/api/workflows/execute-workflows.ts (1)
executeWorkflows(17-122)packages/prisma/client.ts (1)
WorkflowTrigger(25-25)
packages/email/src/templates/new-sale-alert-program-owner.tsx (1)
packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(5-16)
apps/web/app/(ee)/api/stripe/integration/webhook/invoice-paid.ts (2)
apps/web/lib/api/workflows/execute-workflows.ts (1)
executeWorkflows(17-122)packages/prisma/client.ts (1)
WorkflowTrigger(25-25)
apps/web/lib/partners/create-partner-commission.ts (1)
apps/web/lib/api/partners/notify-partner-commission.ts (1)
notifyPartnerCommission(10-145)
apps/web/lib/integrations/shopify/create-sale.ts (1)
apps/web/lib/partners/create-partner-commission.ts (1)
createPartnerCommission(25-336)
apps/web/lib/api/partners/notify-partner-commission.ts (2)
packages/email/src/templates/new-commission-alert-partner.tsx (1)
NewCommissionAlertPartner(17-165)packages/email/src/templates/new-sale-alert-program-owner.tsx (1)
NewSaleAlertProgramOwner(19-203)
packages/email/src/templates/new-commission-alert-partner.tsx (2)
packages/utils/src/constants/main.ts (1)
DUB_WORDMARK(72-72)packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(5-16)
⏰ 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). (2)
- GitHub Check: Vade Review
- GitHub Check: build
🔇 Additional comments (8)
packages/email/src/templates/welcome-email-partner.tsx (1)
39-41: Copy tweak LGTM.Concise and reads well.
packages/email/src/templates/new-sale-alert-program-owner.tsx (1)
38-41: Prop shape migration to commission looks good.Default values and downstream computations read correctly.
Also applies to: 61-64, 69-75
apps/web/lib/api/partners/notify-partner-commission.ts (2)
121-136: Confirm owner notification preference usage.Owner notifications filter by
notificationPreference.newPartnerSale. If the intent is “commission-based” going forward, confirm this is the right flag or plan a rename/migration.
67-78: Short link may be missing protocol.Templates use
href={shortLink}. Iflink.shortLinkis domain/path-only, this becomes a relative URL.Optional in-place hardening:
- shortLink: partnerLink?.shortLink ?? null, + shortLink: partnerLink?.shortLink + ? partnerLink.shortLink.startsWith("http") + ? partnerLink.shortLink + : `https://${partnerLink.shortLink}` + : null,Also applies to: 102-103
apps/web/lib/api/conversions/track-sale.ts (1)
221-241: LGTM; obsolete sale-notification patterns removed
Awaiting createPartnerCommission here is correct—commission creation now handles notifications via notifyPartnerCommission;rgsearch found nonotifyPartnerSaleor related remnants.apps/web/lib/integrations/shopify/create-sale.ts (1)
140-156: LGTM: unified on commission-based notifications.Shifting partner notifications into
createPartnerCommissionkeeps Shopify flow consistent with Track Sale.apps/web/lib/partners/create-partner-commission.ts (2)
13-13: Correct import for new notification flow.Importing
notifyPartnerCommissionaligns this module with the commission-based notifications. Looks good.
264-265: Destructuring workspace is clean and avoids re-selects.This keeps the call sites concise and matches downstream expectations.
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/lib/api/conversions/track-sale.ts (1)
221-247: Gate saleRecorded workflows on commission creation
createPartnerCommission may no-op (no reward, duration exceeded, cap hit); only call executeWorkflows when a commission was actually created.Apply to apps/web/lib/api/conversions/track-sale.ts:
- await createPartnerCommission({ + const commission = await createPartnerCommission({ event: "sale", programId: link.programId, partnerId: link.partnerId, linkId: link.id, eventId, customerId: customer.id, amount: saleData.amount, quantity: 1, invoiceId, currency, context: { customer: { country: customer.country, }, sale: { productId: metadata?.productId as string, }, }, }); - - await executeWorkflows({ - trigger: WorkflowTrigger.saleRecorded, - programId: link.programId, - partnerId: link.partnerId, - }); + if (commission) { + await executeWorkflows({ + trigger: WorkflowTrigger.saleRecorded, + programId: link.programId, + partnerId: link.partnerId, + }); + }apps/web/lib/integrations/shopify/create-sale.ts (1)
140-156: Restore saleRecorded workflow trigger for program links (regression vs Stripe/track-sale).After removing notifyPartnerSale, no workflow runs here. Fire saleRecorded only when a commission is created.
Diff in this block:
- await createPartnerCommission({ + const commission = await createPartnerCommission({ event: "sale", programId: link.programId, partnerId: link.partnerId, linkId: link.id, eventId: saleData.event_id, customerId: customer.id, amount: saleData.amount, quantity: 1, invoiceId: saleData.invoice_id, currency: saleData.currency, context: { customer: { country: customer.country, }, }, }); + if (commission) { + waitUntil( + executeWorkflows({ + trigger: WorkflowTrigger.saleRecorded, + programId: link.programId, + partnerId: link.partnerId, + }), + ); + }Add imports (outside this hunk):
+import { executeWorkflows } from "@/lib/api/workflows/execute-workflows"; +import { WorkflowTrigger } from "@dub/prisma/client";
♻️ Duplicate comments (3)
packages/email/src/templates/new-commission-alert-partner.tsx (1)
53-57: Stray “$” in Preview copy has been fixed.No more dangling character; preview reads correctly.
apps/web/lib/api/partners/notify-partner-commission.ts (1)
81-84: Gate zero/negative earnings to avoid misleading emails.Clawbacks/adjustments (earnings <= 0) currently send “You just made a commission…”. Skip or route to a dedicated template.
if (!partner) { return; } + + // Skip misleading notifications for clawbacks/zero-earnings. + if (commission.earnings <= 0) { + // TODO: consider a dedicated "commission adjusted/clawback" notification. + return; + }apps/web/lib/partners/create-partner-commission.ts (1)
266-268: Don’t email partners for clawbacks/fraud/canceled or zero/negative earnings.This was already raised; notifyPartnerCommission is still unconditional here. Gate it to avoid misleading “you earned a commission” emails.
Apply:
const isClawback = earnings < 0; const shouldTriggerWorkflow = !isClawback && !skipWorkflow; + const shouldNotify = + !isClawback && + commission.earnings > 0 && + commission.status !== "fraud" && + commission.status !== "canceled"; @@ - notifyPartnerCommission({ - program, - workspace, - commission, - }), + ...(shouldNotify + ? [ + notifyPartnerCommission({ + program, + workspace, + commission, + }), + ] + : []),Alternative: enforce this centrally inside notifyPartnerCommission with an early return on disallowed statuses or non-positive earnings.
Also applies to: 289-294
🧹 Nitpick comments (10)
packages/email/src/templates/welcome-email-partner.tsx (1)
39-40: Copy tweak looks good.Shorter line reads cleanly. If you prefer a slightly warmer tone, consider “It’s time to start earning…”.
packages/email/src/templates/new-sale-alert-program-owner.tsx (1)
69-75: Verify currency duplication with currencyFormatter.If currencyFormatter already includes “$”, appending “USD” in the rows below results in “$X.XX USD”. Confirm intended style; otherwise drop the “ USD” suffix for consistency.
packages/email/src/templates/new-commission-alert-partner.tsx (1)
89-98: Use and remove invalid Tailwind class.Replace the raw with the react-email for consistency, and drop “text-semibold” (not a valid Tailwind class).
- <a - href={shortLink} - className="text-semibold font-medium text-black underline" - > - {getPrettyUrl(shortLink)} - </a> + <Link + href={shortLink} + className="font-medium text-black underline" + > + {getPrettyUrl(shortLink)} + </Link>apps/web/lib/api/partners/notify-partner-commission.ts (5)
67-79: Simplify optional link fetch; avoid redundant Promise.resolve.Promise.resolve around an existing promise is unnecessary. This also reads cleaner:
- commission.linkId - ? Promise.resolve( - prisma.link.findUnique({ - where: { - id: commission.linkId, - }, - select: { - shortLink: true, - }, - }), - ) - : Promise.resolve(null), + commission.linkId + ? prisma.link.findUnique({ + where: { id: commission.linkId }, + select: { shortLink: true }, + }) + : null,
105-108: Deduplicate partner recipient emails.Guard against duplicate addresses if multiple user records map to the same email.
- const partnerEmailsToNotify = partner.users - .map(({ user }) => user.email) - .filter(Boolean) as string[]; + const partnerEmailsToNotify = Array.from( + new Set( + (partner.users.map(({ user }) => user.email).filter(Boolean) as string[]) || + [], + ), + );
111-120: Pass only required props to NewCommissionAlertPartner.Avoid spreading unused fields (partner) into the template props.
- ...partnerEmailsToNotify.map((email) => ({ + ...partnerEmailsToNotify.map((email) => ({ subject: "You just made a commission via Dub Partners!", from: VARIANT_TO_FROM_MAP.notifications, to: email, - react: NewCommissionAlertPartner({ - email, - ...data, - }), + react: NewCommissionAlertPartner({ + email, + program: data.program, + commission: data.commission, + shortLink: data.shortLink, + }), })),
122-126: Align owner email subject with body copy.Body/preview says “You received a sale…”, but subject says “New commission…”. Consider a sale-focused subject for clarity when type === "sale".
- subject: `New commission for ${partner.name}`, + subject: `New sale referred by ${partner.name}`,
142-144: Optional: add basic error logging around batch sends.Helps diagnose delivery issues without failing the whole promise chain.
- await Promise.all( - emailChunks.map((emailChunk) => resend?.batch.send(emailChunk)), - ); + await Promise.all( + emailChunks.map(async (emailChunk) => { + try { + return await resend?.batch.send(emailChunk); + } catch (err) { + console.error("[notifyPartnerCommission] resend.batch.send failed", { + size: emailChunk.length, + err, + }); + } + }), + );apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.ts (1)
367-371: LGTM; minor hardening for background errors.Wrap executeWorkflows with a catch to avoid unhandled rejections inside waitUntil.
- waitUntil( - executeWorkflows({ + waitUntil( + executeWorkflows({ trigger: WorkflowTrigger.saleRecorded, programId: link.programId, partnerId: link.partnerId, - }), + }).catch(console.error), );apps/web/app/(ee)/api/stripe/integration/webhook/invoice-paid.ts (1)
214-218: LGTM; mirror the small error guard.Same optional catch as in checkout-session-completed to prevent unhandled rejections inside waitUntil.
- executeWorkflows({ + executeWorkflows({ trigger: WorkflowTrigger.saleRecorded, programId: link.programId, partnerId: link.partnerId, - }), + }).catch(console.error),
📜 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 (9)
apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.ts(1 hunks)apps/web/app/(ee)/api/stripe/integration/webhook/invoice-paid.ts(1 hunks)apps/web/lib/api/conversions/track-sale.ts(1 hunks)apps/web/lib/api/partners/notify-partner-commission.ts(4 hunks)apps/web/lib/integrations/shopify/create-sale.ts(1 hunks)apps/web/lib/partners/create-partner-commission.ts(3 hunks)packages/email/src/templates/new-commission-alert-partner.tsx(2 hunks)packages/email/src/templates/new-sale-alert-program-owner.tsx(2 hunks)packages/email/src/templates/welcome-email-partner.tsx(1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-08-25T17:42:13.600Z
Learnt from: devkiran
PR: dubinc/dub#2736
File: apps/web/lib/api/get-workspace-users.ts:76-83
Timestamp: 2025-08-25T17:42:13.600Z
Learning: Business rule confirmed: Each workspace has exactly one program. The code should always return workspace.programs[0] since there's only one program per workspace.
Applied to files:
apps/web/lib/partners/create-partner-commission.ts
📚 Learning: 2025-06-10T19:16:23.445Z
Learnt from: devkiran
PR: dubinc/dub#2510
File: apps/web/lib/actions/partners/onboard-program.ts:16-19
Timestamp: 2025-06-10T19:16:23.445Z
Learning: Business rule: Each workspace may have at most one program; attempting to create more must be blocked in code.
Applied to files:
apps/web/lib/partners/create-partner-commission.ts
⏰ 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 (3)
packages/email/src/templates/new-sale-alert-program-owner.tsx (2)
38-41: Prop rename to commission: LGTM.Defaults and shape align with downstream usage.
61-65: Types updated for commission: LGTM.Props match how the template computes amounts/earnings.
apps/web/lib/partners/create-partner-commission.ts (1)
243-265: Program/workspace fetch expansion looks good.The selected fields cover notify and webhook needs without over-fetching.
Summary by CodeRabbit
New Features
Enhancements
Content