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

Skip to content

Conversation

@devkiran
Copy link
Collaborator

@devkiran devkiran commented Jul 10, 2025

Summary by CodeRabbit

  • New Features

    • Added support for new payout statuses: "processed" and "sent" across the dashboard, sidebar, and status badges.
    • Introduced a new email template to notify partners when a Stripe payout withdrawal is initiated.
    • Added a collapsible "Advanced settings" section in the partner payout settings modal for invoice details.
    • Enhanced Stripe webhook handling to update payout statuses automatically when payouts are completed.
    • Added a new webhook event handler for Stripe "payout.paid" events.
    • Introduced a new partner payout settings sheet with improved form handling and UI.
  • Improvements

    • Refined payout statistics display with a new, more detailed component.
    • Updated payout method connection and settings UI for improved clarity and usability.
    • Streamlined payout processing logic for Stripe and PayPal to improve reliability and grouping by partner.
    • Updated various links to point to the new payouts page for consistency.
    • Simplified Stripe balance available processing by removing currency conversion and fee logic.
    • Improved notification emails to partners about payout processing and withdrawals.
    • Enhanced payout grouping and minimum withdrawal amount handling for Stripe transfers.
    • Updated email templates to use consistent branding and messaging.
    • Replaced modal with sheet component for partner payout settings and added conditional UI for payout connection.
  • Bug Fixes

    • Fixed payout totals in the sidebar to include new statuses.
    • Corrected email link URLs in partner payout-related emails.
    • Disabled payout completion emails temporarily pending template updates.
  • Style

    • Improved button and loading state styling in payout-related dropdowns and modals.
    • Added animated chevron icon and smooth expand/collapse for advanced settings in payout modal.
  • Chores

    • Updated database schema to support new payout statuses and fields.
    • Removed deprecated components and cleaned up unused code.

@vercel
Copy link
Contributor

vercel bot commented Jul 10, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Updated (UTC)
dub ✅ Ready (Inspect) Visit Preview Jul 12, 2025 1:12am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jul 10, 2025

"""

Walkthrough

This update introduces two new payout statuses, "processed" and "sent," and refactors payout processing logic for both Stripe and PayPal. It internalizes payout fetching within processing functions, adds a new Stripe webhook handler, restructures payout-related UI components, and updates email templates and URLs. Database schema and badge definitions are also expanded to support the new statuses.

Changes

File(s) / Path(s) Change Summary
apps/web/app/(ee)/api/cron/payouts/charge-succeeded/route.ts, utils.ts Refactored to remove external payout fetching and type alias; functions now receive invoice/payload only.
apps/web/app/(ee)/api/cron/payouts/charge-succeeded/send-paypal-payouts.ts Refactored: sendPaypalPayouts now fetches payouts internally, removes payouts param, updates query logic.
apps/web/app/(ee)/api/cron/payouts/charge-succeeded/send-stripe-payouts.ts Refactored: sendStripePayouts fetches payouts internally, groups by partner, adds new status logic, withdrawal fee handling, and notification improvements.
apps/web/app/(ee)/api/stripe/connect/webhook/balance-available.ts Simplified: removed currency conversion, withdrawal checks, and fee logic; streamlined to direct balance usage and notification.
apps/web/app/(ee)/api/stripe/connect/webhook/payout-paid.ts (new), route.ts Added handler for Stripe "payout.paid" events; updates payout statuses to "completed" on webhook receipt. Integrated handler into webhook routing.
apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/page.ts.x Swapped PayoutStatsAndSettings for new PayoutStats component.
apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/partner-payout-settings-button.tsx Added conditional rendering of ConnectPayoutButton based on payout enablement.
apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/partner-payout-settings-modal.tsx Restructured modal: new connected account section, collapsible advanced settings, updated imports/UI.
apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-stats-and-settings.tsx (deleted) Removed PayoutStatsAndSettings component.
apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-stats.tsx (new) Added new PayoutStats component with status-aware cards, tooltips, and responsive layout.
apps/web/lib/swr/use-partner-payouts.ts Added "status" to API query string parameters.
apps/web/ui/layout/sidebar/payout-stats.tsx Updated total calculation to include "sent" status.
apps/web/ui/modals/add-edit-tag-modal.tsx Minor: changed tooltip string syntax.
apps/web/ui/partners/payout-methods-dropdown.tsx Adjusted container/button styling for loading state and popover.
apps/web/ui/partners/payout-status-badges.tsx Added "processed" and "sent" badges with icons and styles.
packages/email/src/templates/partner-payout-confirmed.tsx Added a blank line between JSX elements (formatting only).
packages/email/src/templates/partner-payout-processed.tsx Renamed to PartnerPayoutProcessed, updated logo, text, and links.
packages/email/src/templates/partner-payout-withdrawal-initiated.tsx (new) Added new email template for Stripe payout withdrawal initiation.
packages/email/src/templates/partner-paypal-payout-failed.tsx, welcome-email-partner.tsx Updated payout settings URLs and styling in email templates.
packages/prisma/schema/payout.prisma Added "processed" and "sent" to PayoutStatus enum; updated stripeTransferId field; added stripePayoutId.
apps/web/app/(ee)/api/paypal/webhook/payouts-item-succeeded.ts Removed email notification logic for payout completion; added TODO for template update.
apps/web/app/(ee)/app.dub.co/embed/referrals/quickstart.tsx Changed "Connect payouts" button URL.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant WebApp
    participant DB
    participant Stripe
    participant PayPal
    participant Email

    User->>WebApp: Initiate payout (triggered by event)
    WebApp->>DB: Fetch invoice and relevant data
    WebApp->>Stripe: (If Stripe) Create transfer/payout
    Stripe-->>WebApp: Transfer/payout confirmation
    WebApp->>DB: Update payouts to "sent" or "processed"
    WebApp->>Email: Send notification (if applicable)
    Stripe-->>WebApp: Send "payout.paid" webhook
    WebApp->>DB: Update payouts to "completed"
    WebApp->>Email: Send withdrawal confirmation (new template)
    WebApp->>PayPal: (If PayPal) Create batch payout
    PayPal-->>WebApp: Batch payout confirmation
    WebApp->>DB: Update payouts as needed
Loading

Possibly related PRs

  • Min withdrawal amount UI #2619: Related to partner payout settings UI and minimum withdrawal amount handling, overlapping with UI and form changes in this PR.
  • Fix the available balance calculation #2621: Also modifies the balanceAvailable function, focusing on subtracting pending or in-transit payouts from available balance, which aligns with the simplification and refactoring of payout handling in this PR.
  • Update trigger-withdrawal cron #2494: Modifies the trigger-withdrawal cron to include payouts with status "processed" and updates variable naming and reserved balance calculation, related to payout filtering and withdrawal logic changes in this PR.

Poem

(\(\
( -.-)
o_(")(")

Two new statuses join the payout parade,
"Processed" and "Sent"—see how they're displayed!
Stripe and PayPal now fetch their own,
With badges and emails, the changes are shown.
Rabbits rejoice as payouts hop ahead,
With clearer logic and UI, all payout bunnies are fed!
"""


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8dff05c and f0b4beb.

📒 Files selected for processing (1)
  • apps/web/app/(ee)/api/cron/trigger-withdrawal/route.ts (2 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: devkiran
PR: dubinc/dub#2635
File: packages/prisma/schema/payout.prisma:24-25
Timestamp: 2025-07-11T16:28:55.654Z
Learning: In the Dub codebase, multiple payout records can now share the same stripeTransferId because payouts are grouped by partner and processed as single Stripe transfers. This is why the @unique constraint was removed from the stripeTransferId field in the Payout model - a single transfer can include multiple payouts for the same partner.
Learnt from: steven-tey
PR: dubinc/dub#0
File: :0-0
Timestamp: 2025-06-25T21:20:59.837Z
Learning: In the Dub codebase, payout limit validation uses a two-stage pattern: server actions perform quick sanity checks (payoutsUsage > payoutsLimit) for immediate user feedback, while the cron job (/cron/payouts) performs authoritative validation (payoutsUsage + payoutAmount > payoutsLimit) with actual calculated amounts before processing. This design provides fast user feedback while ensuring accurate limit enforcement at transaction time.
apps/web/app/(ee)/api/cron/trigger-withdrawal/route.ts (3)
Learnt from: devkiran
PR: dubinc/dub#2635
File: packages/prisma/schema/payout.prisma:24-25
Timestamp: 2025-07-11T16:28:55.654Z
Learning: In the Dub codebase, multiple payout records can now share the same stripeTransferId because payouts are grouped by partner and processed as single Stripe transfers. This is why the @unique constraint was removed from the stripeTransferId field in the Payout model - a single transfer can include multiple payouts for the same partner.
Learnt from: steven-tey
PR: dubinc/dub#0
File: :0-0
Timestamp: 2025-06-19T01:46:45.723Z
Learning: PayPal webhook verification in the Dub codebase is handled at the route level in `apps/web/app/(ee)/api/paypal/webhook/route.ts` using the `verifySignature` function. Individual webhook handlers like `payoutsItemFailed` don't need to re-verify signatures since they're only called after successful verification.
Learnt from: steven-tey
PR: dubinc/dub#0
File: :0-0
Timestamp: 2025-06-25T21:20:59.837Z
Learning: In the Dub codebase, payout limit validation uses a two-stage pattern: server actions perform quick sanity checks (payoutsUsage > payoutsLimit) for immediate user feedback, while the cron job (/cron/payouts) performs authoritative validation (payoutsUsage + payoutAmount > payoutsLimit) with actual calculated amounts before processing. This design provides fast user feedback while ensuring accurate limit enforcement at transaction time.
🧬 Code Graph Analysis (1)
apps/web/app/(ee)/api/cron/trigger-withdrawal/route.ts (1)
apps/web/lib/stripe/index.ts (1)
  • stripe (3-9)
⏰ 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/cron/trigger-withdrawal/route.ts (2)

17-30: LGTM: Payout query updated to align with new status workflow.

The changes correctly adapt the reserved balance calculation to the new payout lifecycle:

  1. Status inclusion: Adding "processed" status ensures payouts in the new intermediate state are properly counted
  2. Transfer filter: The stripeTransferId: null condition correctly identifies payouts pending transfer creation
  3. Naming improvement: toBeSentPayouts is more descriptive than dubProcessingPayouts

This aligns well with the grouped payout processing approach where multiple payouts share a single Stripe transfer.


53-59: LGTM: Reserved balance logic properly updated.

The comment updates accurately reflect the new payout workflow where payouts can be in "processed" status while awaiting transfer creation. The reserved balance calculation ensures sufficient funds remain available for partner payouts triggered by chargeSucceeded webhooks.

✨ Finishing Touches
  • 📝 Generate Docstrings

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.
    • Explain this complex logic.
    • 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. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • 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 src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

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

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai 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

Documentation and Community

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

… payouts with transfers and modify payoutPaid to mark payouts as completed. Add stripePayoutId field to Payout model in Prisma schema.
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: 1

♻️ Duplicate comments (3)
apps/web/app/(ee)/api/cron/payouts/charge-succeeded/send-stripe-payouts.ts (3)

112-114: Critical: Zero balance scenario still lacks proper handling.

The zero balance condition continues without updating payout statuses, leaving them in an inconsistent state. This was previously flagged but remains unaddressed.

Apply this fix to handle the zero balance scenario properly:

 if (updatedBalance <= 0) {
+  await prisma.payout.updateMany({
+    where: {
+      id: {
+        in: payoutIds,
+      },
+    },
+    data: {
+      status: "processed",
+    },
+  });
+
+  console.log(
+    `Skipping payouts for partner ${partner.id} - insufficient balance after withdrawal fee (${currencyFormatter(updatedBalance / 100)})`,
+  );
+
   continue;
 }

117-128: Critical: Stripe transfer creation lacks error handling.

The Stripe API call can fail and leave payouts in an inconsistent state. This was previously flagged but remains unaddressed.

Wrap the transfer creation in proper error handling:

-const transfer = await stripe.transfers.create(
-  {
-    amount: updatedBalance,
-    currency: "usd",
-    transfer_group: invoiceId,
-    destination: partner.stripeConnectId!,
-    description: `Dub Partners payout for ${payouts.map((p) => p.id).join(", ")}`,
-  },
-  {
-    idempotencyKey: `${invoiceId}-${partner.id}`,
-  },
-);
+let transfer;
+try {
+  transfer = await stripe.transfers.create(
+    {
+      amount: updatedBalance,
+      currency: "usd",
+      transfer_group: invoiceId,
+      destination: partner.stripeConnectId!,
+      description: `Dub Partners payout for ${payouts.map((p) => p.id).join(", ")}`,
+    },
+    {
+      idempotencyKey: `${invoiceId}-${partner.id}`,
+    },
+  );
+} catch (error) {
+  console.error(
+    `Failed to create Stripe transfer for partner ${partner.id}:`,
+    error,
+  );
+  continue;
+}

137-174: Consider adding transaction safety for database operations.

While Promise.allSettled handles individual promise failures, wrapping the payout and commission updates in a database transaction would ensure atomicity and prevent partial updates.

+await prisma.$transaction(async (tx) => {
   await Promise.allSettled([
-    prisma.payout.updateMany({
+    tx.payout.updateMany({
       where: {
         id: {
           in: payoutIds,
         },
       },
       data: {
         stripeTransferId: transfer.id,
         status: "sent",
         paidAt: new Date(),
       },
     }),

-    prisma.commission.updateMany({
+    tx.commission.updateMany({
       where: {
         payoutId: {
           in: payoutIds,
         },
       },
       data: {
         status: "paid",
       },
     }),
+  ]);
+});

+await Promise.allSettled([
     partner.email
       ? sendEmail({
           variant: "notifications",
           subject: "You've been paid!",
           email: partner.email,
           react: PartnerPayoutProcessed({
             email: partner.email,
             program: latestInvoicePayout.program,
             payout: latestInvoicePayout,
           }),
         })
       : Promise.resolve(),
-  ]);
+]);
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6b03cf2 and 1dc4d14.

📒 Files selected for processing (5)
  • apps/web/app/(ee)/api/cron/payouts/charge-succeeded/route.ts (2 hunks)
  • apps/web/app/(ee)/api/cron/payouts/charge-succeeded/send-stripe-payouts.ts (1 hunks)
  • apps/web/app/(ee)/api/paypal/webhook/payouts-item-succeeded.ts (1 hunks)
  • apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-stats.tsx (1 hunks)
  • packages/email/src/templates/partner-payout-processed.tsx (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • apps/web/app/(ee)/api/cron/payouts/charge-succeeded/route.ts
  • apps/web/app/(ee)/api/paypal/webhook/payouts-item-succeeded.ts
  • apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-stats.tsx
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: devkiran
PR: dubinc/dub#2635
File: packages/prisma/schema/payout.prisma:24-25
Timestamp: 2025-07-11T16:28:55.654Z
Learning: In the Dub codebase, multiple payout records can now share the same stripeTransferId because payouts are grouped by partner and processed as single Stripe transfers. This is why the @unique constraint was removed from the stripeTransferId field in the Payout model - a single transfer can include multiple payouts for the same partner.
Learnt from: steven-tey
PR: dubinc/dub#0
File: :0-0
Timestamp: 2025-06-25T21:20:59.837Z
Learning: In the Dub codebase, payout limit validation uses a two-stage pattern: server actions perform quick sanity checks (payoutsUsage > payoutsLimit) for immediate user feedback, while the cron job (/cron/payouts) performs authoritative validation (payoutsUsage + payoutAmount > payoutsLimit) with actual calculated amounts before processing. This design provides fast user feedback while ensuring accurate limit enforcement at transaction time.
packages/email/src/templates/partner-payout-processed.tsx (3)
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.
Learnt from: devkiran
PR: dubinc/dub#2635
File: packages/prisma/schema/payout.prisma:24-25
Timestamp: 2025-07-11T16:28:55.654Z
Learning: In the Dub codebase, multiple payout records can now share the same stripeTransferId because payouts are grouped by partner and processed as single Stripe transfers. This is why the @unique constraint was removed from the stripeTransferId field in the Payout model - a single transfer can include multiple payouts for the same partner.
Learnt from: devkiran
PR: dubinc/dub#2448
File: packages/email/src/templates/partner-program-summary.tsx:254-254
Timestamp: 2025-05-29T04:49:42.842Z
Learning: In the Dub codebase, it's acceptable to keep `partners.dub.co` hardcoded rather than making it configurable for different environments.
apps/web/app/(ee)/api/cron/payouts/charge-succeeded/send-stripe-payouts.ts (2)
Learnt from: devkiran
PR: dubinc/dub#2635
File: packages/prisma/schema/payout.prisma:24-25
Timestamp: 2025-07-11T16:28:55.654Z
Learning: In the Dub codebase, multiple payout records can now share the same stripeTransferId because payouts are grouped by partner and processed as single Stripe transfers. This is why the @unique constraint was removed from the stripeTransferId field in the Payout model - a single transfer can include multiple payouts for the same partner.
Learnt from: steven-tey
PR: dubinc/dub#0
File: :0-0
Timestamp: 2025-06-25T21:20:59.837Z
Learning: In the Dub codebase, payout limit validation uses a two-stage pattern: server actions perform quick sanity checks (payoutsUsage > payoutsLimit) for immediate user feedback, while the cron job (/cron/payouts) performs authoritative validation (payoutsUsage + payoutAmount > payoutsLimit) with actual calculated amounts before processing. This design provides fast user feedback while ensuring accurate limit enforcement at transaction time.
⏰ 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 (8)
packages/email/src/templates/partner-payout-processed.tsx (5)

17-17: Function name correctly reflects the new payout lifecycle.

The rename from PartnerPayoutSent to PartnerPayoutProcessed aligns with the new payout status workflow introduced in this PR, where "processed" represents an intermediate state before "sent".


73-73: Logo standardization improves consistency.

Using the fixed DUB_WORDMARK instead of the dynamic program logo ensures consistent branding across email templates and eliminates potential issues with missing or invalid program logos.


95-100: Enhanced messaging provides clearer user guidance.

The updated text effectively explains the Stripe Express account behavior and minimum withdrawal balance, providing users with actionable information about when transfers will occur and how to adjust settings.


105-105: URL change aligns with UI restructuring.

The link to the general payouts overview page (/payouts) is consistent with the URL standardization mentioned in the PR objectives and reflects the restructured payout components.


107-107: Link text matches the destination page.

The change from "View payout" to "View payouts" correctly reflects that users are now directed to the general payouts overview rather than a specific payout detail page.

apps/web/app/(ee)/api/cron/payouts/charge-succeeded/send-stripe-payouts.ts (3)

22-54: LGTM: Well-structured payout fetching logic.

The internal payout fetching improves encapsulation and ensures fresh data during processing. The filtering criteria correctly target payouts that need Stripe processing.


63-74: LGTM: Efficient payout grouping by partner.

The grouping logic correctly consolidates payouts by partner ID, which will reduce API calls and transaction fees as intended by the refactoring.


83-102: LGTM: Proper handling of minimum withdrawal amounts.

The logic correctly updates payouts to "processed" status when below the minimum threshold, maintaining state consistency without creating unnecessary transfers.

}

const { invoiceId, chargeId, achCreditTransfer } = payload;
const latestInvoicePayout = payouts.find((p) => p.invoiceId === invoiceId)!;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Potential issue: Unsafe non-null assertion.

The non-null assertion ! assumes an invoice payout exists, but this could fail if the invoice contains no payouts.

Add proper validation:

-const latestInvoicePayout = payouts.find((p) => p.invoiceId === invoiceId)!;
+const latestInvoicePayout = payouts.find((p) => p.invoiceId === invoiceId);
+if (!latestInvoicePayout) {
+  console.log(`No payout found for invoice ${invoiceId}, skipping...`);
+  return;
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const latestInvoicePayout = payouts.find((p) => p.invoiceId === invoiceId)!;
const latestInvoicePayout = payouts.find((p) => p.invoiceId === invoiceId);
if (!latestInvoicePayout) {
console.log(`No payout found for invoice ${invoiceId}, skipping...`);
return;
}
🤖 Prompt for AI Agents
In apps/web/app/(ee)/api/cron/payouts/charge-succeeded/send-stripe-payouts.ts at
line 61, the code uses a non-null assertion operator (!) on the result of
payouts.find, which can cause runtime errors if no matching payout is found.
Replace the non-null assertion with a check to verify that latestInvoicePayout
is not undefined before using it, and handle the case where no payout is found
appropriately, such as returning early or throwing a descriptive error.

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

🧹 Nitpick comments (3)
apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/partner-payout-settings-sheet.tsx (3)

147-148: TypeScript ignore comment is acceptable but document the reasoning.

The @ts-ignore for trailingZeroDisplay is reasonable since it's a newer API not yet reflected in TypeScript definitions. Consider adding a brief comment explaining this is a legitimate newer option.

-                    // @ts-ignore – trailingZeroDisplay is a valid option but TS is outdated
+                    // @ts-ignore – trailingZeroDisplay is a valid Intl.NumberFormat option but TS definitions are outdated
                     trailingZeroDisplay: "stripIfInteger",

205-205: Consider removing autoFocus for better accessibility.

Auto-focusing the business name input when the sheet opens can be jarring for users, especially those using screen readers or keyboard navigation. Consider letting users naturally tab to the first field they want to interact with.

-                    autoFocus
                     className="block w-full rounded-md border-neutral-300 text-neutral-900 placeholder-neutral-400 focus:border-neutral-500 focus:outline-none focus:ring-neutral-500 sm:text-sm"

268-284: Simplify the hook implementation.

The current pattern with useCallback wrapping the component and then useMemo for the return value is overly complex. This can be simplified while maintaining the same functionality.

 export function usePartnerPayoutSettingsSheet() {
   const [showPartnerPayoutSettingsSheet, setShowPartnerPayoutSettingsSheet] =
     useState(false);

-  const PartnerPayoutSettingsSheetCallback = useCallback(() => {
-    return (
-      <PartnerPayoutSettingsSheet
-        showPartnerPayoutSettingsSheet={showPartnerPayoutSettingsSheet}
-        setShowPartnerPayoutSettingsSheet={setShowPartnerPayoutSettingsSheet}
-      />
-    );
-  }, [showPartnerPayoutSettingsSheet, setShowPartnerPayoutSettingsSheet]);
-
-  return useMemo(
-    () => ({
+  const PartnerPayoutSettingsSheetComponent = () => (
+    <PartnerPayoutSettingsSheet
+      showPartnerPayoutSettingsSheet={showPartnerPayoutSettingsSheet}
+      setShowPartnerPayoutSettingsSheet={setShowPartnerPayoutSettingsSheet}
+    />
+  );
+
+  return {
     setShowPartnerPayoutSettingsSheet,
-    PartnerPayoutSettingsSheet: PartnerPayoutSettingsSheetCallback,
-  }),
-  [setShowPartnerPayoutSettingsSheet, PartnerPayoutSettingsSheetCallback],
-);
+    PartnerPayoutSettingsSheet: PartnerPayoutSettingsSheetComponent,
+  };
 }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e111702 and ca03b06.

📒 Files selected for processing (3)
  • apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/partner-payout-settings-button.tsx (1 hunks)
  • apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/partner-payout-settings-sheet.tsx (1 hunks)
  • apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-stats.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/partner-payout-settings-button.tsx
  • apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-stats.tsx
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: devkiran
PR: dubinc/dub#2635
File: packages/prisma/schema/payout.prisma:24-25
Timestamp: 2025-07-11T16:28:55.654Z
Learning: In the Dub codebase, multiple payout records can now share the same stripeTransferId because payouts are grouped by partner and processed as single Stripe transfers. This is why the @unique constraint was removed from the stripeTransferId field in the Payout model - a single transfer can include multiple payouts for the same partner.
Learnt from: steven-tey
PR: dubinc/dub#0
File: :0-0
Timestamp: 2025-06-25T21:20:59.837Z
Learning: In the Dub codebase, payout limit validation uses a two-stage pattern: server actions perform quick sanity checks (payoutsUsage > payoutsLimit) for immediate user feedback, while the cron job (/cron/payouts) performs authoritative validation (payoutsUsage + payoutAmount > payoutsLimit) with actual calculated amounts before processing. This design provides fast user feedback while ensuring accurate limit enforcement at transaction time.
⏰ 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 (5)
apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/partner-payout-settings-sheet.tsx (5)

162-172: Slider snap-to-value logic is well implemented.

The closest value calculation correctly snaps the slider to predefined withdrawal amounts. The logic uses proper math and maintains form state correctly with shouldDirty: true.


175-183: Dynamic hint display enhances user understanding.

The conditional rendering based on withdrawal amount clearly communicates fee structure to users. The visual distinction between fee-based and free payouts with the PartyPopper icon is effective UX.


237-240: Scroll gradient effect implementation is clean.

The scroll progress-based gradient provides good visual feedback. The opacity calculation using Math.pow(scrollProgress, 2) creates a smooth fade effect that enhances the scrolling experience.


41-53: Component architecture follows good separation of concerns.

The separation between the outer Sheet wrapper and inner form component is clean. This pattern allows for proper state management while keeping the Sheet API encapsulated.


75-91: Form submission and error handling are properly implemented.

The async action handling with success/error callbacks, toast notifications, and cache invalidation using mutatePrefix follows established patterns in the codebase. The form state management with isDirty for the save button is appropriate.

@steven-tey
Copy link
Collaborator

@CodeRabbit final review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jul 12, 2025

Tip

For best results, initiate chat on the files or code changes.

@steven-tey This is an exemplary refactoring that significantly improves the payout system across multiple dimensions:

Key Strengths

Architecture Excellence:

  • Database schema evolution enables efficient grouped payouts while maintaining data integrity
  • New payout lifecycle (processingprocessedsentcompleted) provides clear state tracking
  • Webhook-driven approach with proper event handling ensures robust payout completion

Performance & Efficiency:

  • Grouping payouts by partner reduces Stripe API calls and transaction fees
  • Internal data fetching within processing functions improves encapsulation
  • Simplified balance handling removes unnecessary complexity

User Experience:

  • Enhanced UI with focused components and status-specific cards
  • Consistent URL patterns across all interfaces
  • Clearer email communications with updated templates

Code Quality:

  • Clean separation of concerns with proper error handling
  • Backward compatibility maintained during schema evolution
  • Comprehensive test coverage implied by the scope of changes

🎯 Final Assessment

This refactoring demonstrates excellent software engineering practices:

  • Scope: Substantial but well-bounded changes with clear objectives
  • Implementation: Each change serves a specific purpose in the overall improvement
  • Architecture: Sound decisions that improve both performance and maintainability
  • User Impact: Enhanced experience through better status visibility and processing efficiency

✅ APPROVED - No blockers identified. This is production-ready and represents a significant improvement to the payout system. The changes are well-structured, thoroughly implemented, and deliver meaningful value to both the platform and its users.

    ∩───∩
   ( ◕   ◕ )
    >  ♪  <
   ♪       ♪
Ready to hop into production! 🚀

🧠 Learnings used
Learnt from: devkiran
PR: dubinc/dub#2635
File: packages/prisma/schema/payout.prisma:24-25
Timestamp: 2025-07-11T16:28:55.654Z
Learning: In the Dub codebase, multiple payout records can now share the same stripeTransferId because payouts are grouped by partner and processed as single Stripe transfers. This is why the @unique constraint was removed from the stripeTransferId field in the Payout model - a single transfer can include multiple payouts for the same partner.

Learnt from: steven-tey
PR: dubinc/dub#0
File: :0-0
Timestamp: 2025-06-25T21:20:59.837Z
Learning: In the Dub codebase, payout limit validation uses a two-stage pattern: server actions perform quick sanity checks (payoutsUsage > payoutsLimit) for immediate user feedback, while the cron job (/cron/payouts) performs authoritative validation (payoutsUsage + payoutAmount > payoutsLimit) with actual calculated amounts before processing. This design provides fast user feedback while ensuring accurate limit enforcement at transaction time.

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

♻️ Duplicate comments (1)
apps/web/app/(ee)/api/stripe/connect/webhook/balance-available.ts (1)

99-119: Consider handling pagination for transfers.

The code fetches only up to 100 transfers. If a partner has more than 100 transfers, some payouts might not get their stripePayoutId updated.

Implement pagination to handle all transfers:

-const transfers = await stripe.transfers.list({
-  destination: stripeAccount,
-  limit: 100,
-});
+const allTransfers = [];
+let hasMore = true;
+let startingAfter = undefined;
+
+while (hasMore) {
+  const transfers = await stripe.transfers.list({
+    destination: stripeAccount,
+    limit: 100,
+    starting_after: startingAfter,
+  });
+  
+  allTransfers.push(...transfers.data);
+  hasMore = transfers.has_more;
+  if (hasMore && transfers.data.length > 0) {
+    startingAfter = transfers.data[transfers.data.length - 1].id;
+  }
+}
+
 await prisma.payout.updateMany({
   where: {
     status: "sent",
     stripePayoutId: null,
     stripeTransferId: {
-      in: transfers.data.map(({ id }) => id),
+      in: allTransfers.map(({ id }) => id),
     },
   },
   data: {
     stripePayoutId: payout.id,
   },
 });
🧹 Nitpick comments (1)
apps/web/app/(ee)/api/stripe/connect/webhook/balance-available.ts (1)

12-14: Minor: Improve error message clarity.

The error message refers to stripeAccount in the message but it could be null, which might be confusing in logs.

-    console.error(
-      `Stripe connect account ${stripeAccount} not found. Skipping...`,
-    );
+    console.error(
+      "Stripe connect account not found in webhook event. Skipping...",
+    );
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5d4977e and 8dff05c.

📒 Files selected for processing (4)
  • apps/web/app/(ee)/api/paypal/webhook/payouts-item-succeeded.ts (0 hunks)
  • apps/web/app/(ee)/api/stripe/connect/webhook/balance-available.ts (4 hunks)
  • apps/web/ui/layout/sidebar/payout-stats.tsx (2 hunks)
  • packages/email/src/templates/partner-payout-processed.tsx (5 hunks)
💤 Files with no reviewable changes (1)
  • apps/web/app/(ee)/api/paypal/webhook/payouts-item-succeeded.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/web/ui/layout/sidebar/payout-stats.tsx
  • packages/email/src/templates/partner-payout-processed.tsx
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: devkiran
PR: dubinc/dub#2635
File: packages/prisma/schema/payout.prisma:24-25
Timestamp: 2025-07-11T16:28:55.654Z
Learning: In the Dub codebase, multiple payout records can now share the same stripeTransferId because payouts are grouped by partner and processed as single Stripe transfers. This is why the @unique constraint was removed from the stripeTransferId field in the Payout model - a single transfer can include multiple payouts for the same partner.
Learnt from: steven-tey
PR: dubinc/dub#0
File: :0-0
Timestamp: 2025-06-25T21:20:59.837Z
Learning: In the Dub codebase, payout limit validation uses a two-stage pattern: server actions perform quick sanity checks (payoutsUsage > payoutsLimit) for immediate user feedback, while the cron job (/cron/payouts) performs authoritative validation (payoutsUsage + payoutAmount > payoutsLimit) with actual calculated amounts before processing. This design provides fast user feedback while ensuring accurate limit enforcement at transaction time.
apps/web/app/(ee)/api/stripe/connect/webhook/balance-available.ts (3)
Learnt from: devkiran
PR: dubinc/dub#2635
File: packages/prisma/schema/payout.prisma:24-25
Timestamp: 2025-07-11T16:28:55.654Z
Learning: In the Dub codebase, multiple payout records can now share the same stripeTransferId because payouts are grouped by partner and processed as single Stripe transfers. This is why the @unique constraint was removed from the stripeTransferId field in the Payout model - a single transfer can include multiple payouts for the same partner.
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.
Learnt from: steven-tey
PR: dubinc/dub#0
File: :0-0
Timestamp: 2025-06-25T21:20:59.837Z
Learning: In the Dub codebase, payout limit validation uses a two-stage pattern: server actions perform quick sanity checks (payoutsUsage > payoutsLimit) for immediate user feedback, while the cron job (/cron/payouts) performs authoritative validation (payoutsUsage + payoutAmount > payoutsLimit) with actual calculated amounts before processing. This design provides fast user feedback while ensuring accurate limit enforcement at transaction time.
⏰ 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 (4)
apps/web/app/(ee)/api/stripe/connect/webhook/balance-available.ts (4)

2-3: LGTM! New email notification capability added.

The import of the new PartnerPayoutWithdrawalInitiated email template aligns with the enhanced notification system for payout processing.


56-68: LGTM! Simplified balance calculation logic.

The removal of complex currency conversion and withdrawal fee calculations makes the logic much cleaner while still properly accounting for pending/in-transit payouts.


77-81: Good handling of currency-specific requirements.

The special handling for HUF and TWD currencies that require amounts divisible by 100 is correctly implemented and necessary for Stripe compliance.


121-131: LGTM! Well-implemented email notification system.

The email notification properly uses the new template and includes the necessary payout details. The conditional check for partner email existence is good defensive programming.

@steven-tey steven-tey merged commit cb9d4e8 into main Jul 12, 2025
9 checks passed
@steven-tey steven-tey deleted the payout-updates branch July 12, 2025 01:16
@coderabbitai coderabbitai bot mentioned this pull request Nov 6, 2025
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.

3 participants