-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Add Instant payouts feature #2984
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
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
💡 Enable Vercel Agent with $100 free credit for automated AI reviews |
WalkthroughRemoved partner-level minWithdrawalAmount from schema, APIs, and UI; replaced with global constants and a force-withdrawal flow (server action + confirm modal). Updated transfer logic to use MIN_WITHDRAWAL_AMOUNT_CENTS and a forceWithdrawal flag, adjusted payout UI/layouts, and updated related email and descriptions. Changes
Sequence Diagram(s)sequenceDiagram
actor Partner
participant UI as Payout Stats UI
participant Modal as Confirm Modal
participant Action as forceWithdrawalAction
participant DB as Database
participant Transfer as createStripeTransfer / Stripe
Partner->>UI: Click "Pay out now"
UI->>Modal: Open force-withdrawal modal
Modal->>Partner: Show processed amount + fee
Partner->>Modal: Confirm
Modal->>Action: Execute forceWithdrawalAction
Action->>DB: permission & payout config checks
Action->>DB: enforce 5-per-24h rate limit
Action->>DB: fetch processed payouts
Action->>Transfer: createStripeTransfer(forceWithdrawal: true)
alt success
Transfer-->>Action: success
Action-->>UI: show success toast
else failure
Transfer-->>Action: error
Action-->>UI: show error toast
end
sequenceDiagram
actor Partner
participant RowMenu as Payout Row Menu
participant ConfirmModal as Retry Confirm Modal
participant RetryAction as Retry Payout Action
Partner->>RowMenu: Select "Retry payout"
RowMenu->>ConfirmModal: Open confirmation modal
Partner->>ConfirmModal: Confirm
ConfirmModal->>RetryAction: executeRetryPayout(payoutId)
RetryAction-->>Partner: show success/error toast
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
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: 4
🧹 Nitpick comments (2)
apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-stats.tsx (1)
95-102: Verify button placement and consider accessibility.The "Pay out now" button is conditionally rendered inline with the amount display. While the logic is correct (only showing for "Processed" status with amount > 0), consider:
- The inline placement with
ml-2might cause layout shifts on smaller screens.- Adding an
aria-labelwould improve accessibility for screen readers.Consider this enhancement:
{label === "Processed" && amount > 0 && ( <Button variant="secondary" text="Pay out now" className="ml-2 h-7 px-2 py-1" onClick={() => setShowForceWithdrawalModal(true)} + aria-label="Initiate instant payout for processed funds" /> )}apps/web/lib/actions/partners/force-withdrawal.ts (1)
35-48: Add check for empty processed payouts.If there are no processed payouts available, the user will receive a generic "Failed to force withdrawal" error. A more specific error message would improve the user experience.
const previouslyProcessedPayouts = await prisma.payout.findMany({ where: { partnerId: partner.id, status: "processed", stripeTransferId: null, }, include: { program: { select: { name: true, }, }, }, }); +if (previouslyProcessedPayouts.length === 0) { + throw new Error( + "No processed payouts available for withdrawal. Please wait for your payouts to be processed.", + ); +} + try {
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (14)
apps/web/app/(ee)/api/cron/payouts/charge-succeeded/send-stripe-payouts.ts(0 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-details-sheet.tsx(1 hunks)apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-stats.tsx(5 hunks)apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-table.tsx(2 hunks)apps/web/lib/actions/partners/force-withdrawal.ts(1 hunks)apps/web/lib/actions/partners/update-partner-payout-settings.ts(2 hunks)apps/web/lib/partners/constants.ts(1 hunks)apps/web/lib/partners/create-stripe-transfer.ts(2 hunks)apps/web/lib/zod/schemas/partners.ts(1 hunks)apps/web/ui/partners/payout-row-menu.tsx(2 hunks)apps/web/ui/partners/payout-status-descriptions.ts(1 hunks)packages/email/src/templates/partner-payout-processed.tsx(2 hunks)packages/prisma/schema/partner.prisma(0 hunks)
💤 Files with no reviewable changes (2)
- apps/web/app/(ee)/api/cron/payouts/charge-succeeded/send-stripe-payouts.ts
- packages/prisma/schema/partner.prisma
🧰 Additional context used
🧬 Code graph analysis (7)
packages/email/src/templates/partner-payout-processed.tsx (1)
packages/email/src/react-email.d.ts (3)
Text(15-15)Section(8-8)Link(14-14)
apps/web/ui/partners/payout-status-descriptions.ts (1)
apps/web/lib/partners/constants.ts (1)
MIN_WITHDRAWAL_AMOUNT_CENTS(11-11)
apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-details-sheet.tsx (2)
packages/ui/src/sheet.tsx (1)
Sheet(74-78)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-details-sheet.tsx (1)
PayoutDetailsSheetContent(40-272)
apps/web/lib/actions/partners/force-withdrawal.ts (4)
apps/web/lib/actions/safe-action.ts (1)
authPartnerActionClient(84-124)apps/web/lib/auth/partner-user-permissions.ts (1)
throwIfNoPermission(35-52)apps/web/lib/upstash/ratelimit.ts (1)
ratelimit(5-21)apps/web/lib/partners/create-stripe-transfer.ts (1)
createStripeTransfer(16-156)
apps/web/lib/partners/create-stripe-transfer.ts (1)
apps/web/lib/partners/constants.ts (1)
MIN_WITHDRAWAL_AMOUNT_CENTS(11-11)
apps/web/ui/partners/payout-row-menu.tsx (2)
apps/web/lib/actions/partners/retry-failed-paypal-payouts.ts (1)
retryFailedPaypalPayoutsAction(16-103)apps/web/ui/modals/confirm-modal.tsx (1)
useConfirmModal(107-120)
apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-stats.tsx (3)
apps/web/lib/actions/partners/force-withdrawal.ts (1)
forceWithdrawalAction(10-62)apps/web/ui/modals/confirm-modal.tsx (1)
useConfirmModal(107-120)apps/web/lib/partners/constants.ts (2)
MIN_WITHDRAWAL_AMOUNT_CENTS(11-11)BELOW_MIN_WITHDRAWAL_FEE_CENTS(12-12)
⏰ 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 (22)
apps/web/lib/actions/partners/update-partner-payout-settings.ts (2)
14-14: No issues found — schema updated correctly and function behavior is appropriate.Verified that
partnerPayoutSettingsSchemaonly includescompanyName,address, andtaxId. The destructuring at line 14 aligns with the updated schema afterminWithdrawalAmountremoval. The function correctly performs a straightforward database update without a return value, which is the expected Server Action pattern. The removed Stripe payout logic is handled by the separateforce-withdrawal.tsaction.
26-34: Original review comment is incorrect.The only caller of
updatePartnerPayoutSettingsAction(inpartner-payout-settings-sheet.tsx) does not rely on a return value. TheonSuccesscallback refreshes the partner profile viamutatePrefix("/api/partner-profile"), properly handling the post-update state without needing the function to return data.Additionally, the function contains no Stripe logic—it only updates invoice settings (
companyName,address,taxId). TheforceWithdrawalActionis a separate, intentional flow that independently handles Stripe transfers. This is appropriate separation of concerns, not removed logic that needs to be handled elsewhere.Likely an incorrect or invalid review comment.
packages/email/src/templates/partner-payout-processed.tsx (2)
44-46: LGTM! Currency formatting improvement.The
trailingZeroDisplay: "stripIfInteger"option improves readability by displaying whole dollar amounts without unnecessary decimal places.
108-110: LGTM! Deep linking improvement.Adding the
payoutIdquery parameter enables direct navigation to the specific payout, and the singular "View payout" text is semantically accurate.apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/partner-payout-settings-sheet.tsx (1)
16-16: LGTM! Clean removal of minimum withdrawal UI.The removal of
minWithdrawalAmountsettings and cleanup of unused imports (NumberFlow,Slider) correctly implements the shift to a constant-based withdrawal threshold approach.apps/web/lib/partners/constants.ts (1)
11-12: Verify the business impact of threshold reduction.The withdrawal threshold has been reduced from $100 to $10 (10x decrease), and the fee from $2.00 to $0.50 (4x decrease). This change will:
- Significantly increase payout frequency and operational overhead
- Potentially reduce fee revenue from force withdrawals
- Improve partner experience with faster access to funds
Ensure this aligns with business model, operational capacity, and transaction cost analysis.
apps/web/ui/partners/payout-row-menu.tsx (1)
27-40: LGTM! Confirmation modal improves safety.The addition of a confirmation modal before retrying payouts prevents accidental actions and appropriately warns users about the 5-retry-per-day rate limit.
apps/web/ui/partners/payout-status-descriptions.ts (1)
1-10: LGTM! Proper use of constants.Correctly uses
MIN_WITHDRAWAL_AMOUNT_CENTSconstant for dynamic description, maintaining consistency with the broader refactoring and ensuring a single source of truth.apps/web/lib/zod/schemas/partners.ts (1)
11-11: LGTM! Schema cleanup aligns with data model changes.The removal of
minWithdrawalAmountfrom schemas and cleanup of unused imports correctly implements the shift to constant-based withdrawal thresholds.apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-table.tsx (2)
40-40: LGTM! Pagination count implementation.Adding
usePartnerPayoutsCounthook enables proper pagination and row count display in the table.
189-197: LGTM! Layout container addition.The wrapper div with flex layout prepares for potential filter bar enhancements while maintaining existing functionality.
apps/web/lib/partners/create-stripe-transfer.ts (3)
21-27: LGTM! API signature updated for force withdrawal.The addition of the
forceWithdrawalparameter with a safe default and the simplified partner type correctly implement the constant-based threshold approach.
57-85: LGTM! Minimum withdrawal logic correctly implements force withdrawal.The conditional logic properly handles two scenarios:
- Below minimum without forcing: updates payouts to "processed" status for accumulation and returns early
- Below minimum with forcing: applies the fee and proceeds with transfer
The implementation correctly uses constants and maintains proper state transitions.
62-73: LGTM! Safe handling of optional payouts.The null check on
currentInvoicePayoutsbefore the batch status update correctly handles the optional parameter.apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-stats.tsx (3)
3-25: LGTM: Imports are well-organized.All necessary imports for the force withdrawal feature are present and correctly sourced.
256-289: LGTM: Consistent prop passing across layouts.The
setShowForceWithdrawalModalprop is correctly passed to allPayoutStatsCardinstances in both mobile and desktop layouts, ensuring consistent behavior.
193-203: Code is correct as-is.The error property path
error.serverErroris the documented and correct approach for next-safe-action v7.10.1. serverError is a property in the action result object that contains the server error value (generic-typed, default string). The code properly handles it with a fallback message.apps/web/lib/actions/partners/force-withdrawal.ts (1)
14-23: LGTM: Proper permission and validation checks.The permission check for
payout_settings.updateis correctly placed before business logic, and the payout enablement validation provides clear user guidance.apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-details-sheet.tsx (4)
178-194: LGTM: Clean sticky header implementation.The refactored sticky header with proper z-index positioning improves the sheet UX by keeping the title and close button accessible during scroll.
195-221: LGTM: Content area properly structured.The content area correctly uses
flex-1to grow and fill available space, with proper loading states and conditional rendering. The grid column change from 3 to 2 provides a cleaner 1:1 label-to-value layout.
223-233: LGTM: Sticky footer enhances navigation.The sticky footer with the "View all" link provides consistent access to the full earnings view, improving the user experience when scrolling through payout details.
238-256: LGTM: Clean component wrapper with proper state management.The exported
PayoutDetailsSheetcomponent properly encapsulates sheet behavior and handles query parameter cleanup on close.
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
1 similar comment
✅ 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: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-details-sheet.tsx (1)
55-106: Remove unnecessary dependency from useMemo.The
useMemohook includesearningsin its dependency array (line 106), butearningsis not actually used within the memoized computation. This causes unnecessary recalculations wheneverearningschanges.Apply this diff:
- }, [payout, earnings]); + }, [payout]);
♻️ Duplicate comments (4)
packages/email/src/templates/partner-payout-processed.tsx (1)
99-101: Replace hardcoded values with constants.The hardcoded threshold
1000and string literals"$10"and"$0.50"should referenceMIN_WITHDRAWAL_AMOUNT_CENTSandBELOW_MIN_WITHDRAWAL_FEE_CENTSconstants to maintain consistency with the updated values inapps/web/lib/partners/constants.ts.Apply this diff:
+import { MIN_WITHDRAWAL_AMOUNT_CENTS, BELOW_MIN_WITHDRAWAL_FEE_CENTS } from "@dub/utils"; {variant === "stripe" - ? payout.amount >= 1000 + ? payout.amount >= MIN_WITHDRAWAL_AMOUNT_CENTS ? "The funds will begin transferring to your connected bank account shortly. You will receive another email when the funds are on their way." - : "Since this payout is below the minimum withdrawal amount of $10, it will remain in processed status. If you'd like to receive your payout now, you can do so with a $0.50 withdrawal fee." + : `Since this payout is below the minimum withdrawal amount of ${currencyFormatter(MIN_WITHDRAWAL_AMOUNT_CENTS / 100, { trailingZeroDisplay: "stripIfInteger" })}, it will remain in processed status. If you'd like to receive your payout now, you can do so with a ${currencyFormatter(BELOW_MIN_WITHDRAWAL_FEE_CENTS / 100, { trailingZeroDisplay: "stripIfInteger" })} withdrawal fee.` : "Your payout is on its way to your PayPal account. You'll receive an email from PayPal when it's complete."}apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-stats.tsx (1)
205-247: Fix NaN in currency calculations - critical UX issue.The calculation of
processedPayoutAmountInUsd(lines 206-207) and the modal description amounts (lines 234-236) will produceNaNifpayoutStatusMap?.processed?.amountis undefined. This results in user-facing text like "Pay out NaN" and breaks the modal's readability.Apply safe fallbacks:
const processedPayoutAmountInUsd = currencyFormatter( - payoutStatusMap?.processed?.amount / 100, + (payoutStatusMap?.processed?.amount ?? 0) / 100, { trailingZeroDisplay: "stripIfInteger", }, );And in the modal description:
{currencyFormatter( - (payoutStatusMap?.processed?.amount - + ((payoutStatusMap?.processed?.amount ?? 0) - BELOW_MIN_WITHDRAWAL_FEE_CENTS) / 100, { trailingZeroDisplay: "stripIfInteger" }, )}apps/web/lib/actions/partners/force-withdrawal.ts (2)
25-33: Clarify rate limit error message.The error message refers to "retry attempts," but this rate limit applies to force withdrawal initiations, not retries of failed operations. Users will be confused about what action is being limited.
if (!success) { throw new Error( - "You've reached the maximum number of retry attempts for the past 24 hours. Please wait and try again later.", + "You've reached the maximum number of instant payout requests (5) for the past 24 hours. Please wait and try again later.", ); }
50-60: Log the original error before throwing generic message.The catch block discards the original error, making debugging production issues nearly impossible. The error should be logged with context (partner ID, payout details) before throwing the user-facing message.
try { await createStripeTransfer({ partner, previouslyProcessedPayouts, forceWithdrawal: true, }); } catch (error) { + console.error( + `Force withdrawal failed for partner ${partner.id}:`, + error, + ); throw new Error( "Failed to force withdrawal. Please try again or contact support.", ); }
🧹 Nitpick comments (1)
apps/web/lib/partners/create-stripe-transfer.ts (1)
88-96: Consider logging withdrawal fee details.When a withdrawal fee is applied (line 84), the subsequent logs (lines 91-93, 124-129) don't indicate that a fee was deducted. This could make debugging fee-related issues difficult.
Add fee information to the log:
if (finalTransferableAmount <= 0) { console.log( - `Final transferable amount after deducting withdrawal fee (${currencyFormatter(finalTransferableAmount / 100)}) is less than or equal to 0, skipping...`, + `Final transferable amount (${currencyFormatter(finalTransferableAmount / 100)}) after deducting ${currencyFormatter(withdrawalFee / 100)} fee is less than or equal to 0, skipping...`, ); return; }And enhance the success log around line 124:
console.log( - `Transfer of ${currencyFormatter(finalTransferableAmount / 100)} (${transfer.id}) created for partner ${partner.id} for ${pluralize( + `Transfer of ${currencyFormatter(finalTransferableAmount / 100)}${withdrawalFee > 0 ? ` (after ${currencyFormatter(withdrawalFee / 100)} fee)` : ""} (${transfer.id}) created for partner ${partner.id} for ${pluralize( "payout", allPayouts.length, )} ${allPayouts.map((p) => p.id).join(", ")}`, );
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (14)
apps/web/app/(ee)/api/cron/payouts/charge-succeeded/send-stripe-payouts.ts(0 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-details-sheet.tsx(1 hunks)apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-stats.tsx(5 hunks)apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-table.tsx(2 hunks)apps/web/lib/actions/partners/force-withdrawal.ts(1 hunks)apps/web/lib/actions/partners/update-partner-payout-settings.ts(2 hunks)apps/web/lib/partners/constants.ts(1 hunks)apps/web/lib/partners/create-stripe-transfer.ts(2 hunks)apps/web/lib/zod/schemas/partners.ts(1 hunks)apps/web/ui/partners/payout-row-menu.tsx(2 hunks)apps/web/ui/partners/payout-status-descriptions.ts(1 hunks)packages/email/src/templates/partner-payout-processed.tsx(2 hunks)packages/prisma/schema/partner.prisma(0 hunks)
💤 Files with no reviewable changes (2)
- packages/prisma/schema/partner.prisma
- apps/web/app/(ee)/api/cron/payouts/charge-succeeded/send-stripe-payouts.ts
🔇 Additional comments (13)
packages/email/src/templates/partner-payout-processed.tsx (2)
44-46: LGTM! Currency formatting enhancement.Good addition of
trailingZeroDisplay: "stripIfInteger"to avoid unnecessary trailing zeros in currency display.
108-110: LGTM! Improved payout navigation.The addition of
payoutIdquery parameter and the updated link text from "View payouts" to "View payout" creates a better user experience by directly navigating to the specific payout details.apps/web/ui/partners/payout-row-menu.tsx (2)
3-3: LGTM! Modal-based confirmation improves UX.Good refactor from
window.confirmtouseConfirmModal. The modal provides better UX with clear messaging about the 5 retry attempts per day limit, helping users make informed decisions before retrying failed payouts.Also applies to: 27-40
16-25: LGTM! Improved variable naming.Renaming
executeAsynctoexecuteRetryPayoutandisPendingtoisRetryPayoutPendingimproves code clarity, especially since multiple actions may be introduced in future iterations.apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-table.tsx (2)
5-5: LGTM! Pagination enhancement.Good addition of
usePartnerPayoutsCountto provide accurate row count for pagination. This aligns with the table's pagination functionality and improves the user experience by showing correct page counts.Also applies to: 40-40, 173-173
189-197: LGTM! Layout wrapper addition.The wrapper div with flex layout classes provides consistent spacing and alignment for the filter controls without affecting the
Filter.Selectcomponent's existing behavior.apps/web/ui/partners/payout-status-descriptions.ts (1)
1-2: LGTM! Proper use of constants.Excellent implementation using
MIN_WITHDRAWAL_AMOUNT_CENTSconstant andcurrencyFormatterto dynamically generate the payout status description. This ensures consistency across the codebase and eliminates the maintenance burden of hardcoded values.Also applies to: 10-10
apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/partner-payout-settings-sheet.tsx (1)
16-16: LGTM! Clean removal of minWithdrawalAmount UI.The removal of
minWithdrawalAmountfrom the payout settings UI is consistent with the PR's objective to replace partner-level withdrawal amounts with fixed constants. The form structure remains intact with business details (company name, address, tax ID) and connected payout account settings.Also applies to: 68-73
apps/web/lib/actions/partners/update-partner-payout-settings.ts (1)
14-14: LGTM! Simplified action aligns with schema changes.The removal of
minWithdrawalAmounthandling and post-update Stripe payout logic is consistent with the PR's approach of using fixed constants and dedicated force withdrawal flows. The action now focuses solely on updating business/invoice settings, which improves separation of concerns.Also applies to: 26-34
apps/web/lib/zod/schemas/partners.ts (1)
11-11: LGTM! Schema updated to reflect constant-based approach.The removal of
minWithdrawalAmountfrompartnerPayoutSettingsSchemaand the cleanup of related imports (currencyFormatter,ALLOWED_MIN_WITHDRAWAL_AMOUNTS) correctly implements the shift to fixed withdrawal constants. This ensures type safety across the stack.Also applies to: 755-759
apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-details-sheet.tsx (1)
179-234: LGTM! Clean sticky header/footer layout.The restructured layout with distinct sticky header and footer regions provides a clear visual hierarchy and keeps key actions accessible during scrolling. The conditional rendering logic properly handles loading and empty states.
apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-stats.tsx (1)
95-102: Verify "Pay out now" button visibility logic.The button appears whenever
amount > 0(line 95), but the force withdrawal flow is specifically designed for amounts below the minimum threshold. Should this button only show whenamount > 0 && amount < MIN_WITHDRAWAL_AMOUNT_CENTS?Consider adding a boundary check:
- {label === "Processed" && amount > 0 && ( + {label === "Processed" && amount > 0 && amount < MIN_WITHDRAWAL_AMOUNT_CENTS && ( <Button variant="secondary" text="Pay out now"Or clarify if the instant payout feature is intended to be available for all processed amounts.
apps/web/lib/partners/create-stripe-transfer.ts (1)
57-85: LGTM! Proper handling of force withdrawal logic.The refactored minimum withdrawal check correctly:
- Uses the new constant
MIN_WITHDRAWAL_AMOUNT_CENTSinstead of the removed partner field- Updates current invoice payouts to "processed" status when below threshold and not forcing
- Applies the withdrawal fee only when forcing a below-minimum withdrawal
- Maintains clear control flow with early returns
| const previouslyProcessedPayouts = await prisma.payout.findMany({ | ||
| where: { | ||
| partnerId: partner.id, | ||
| status: "processed", | ||
| stripeTransferId: null, | ||
| }, | ||
| include: { | ||
| program: { | ||
| select: { | ||
| name: true, | ||
| }, | ||
| }, | ||
| }, | ||
| }); |
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.
Handle empty processed payouts case explicitly.
If previouslyProcessedPayouts is empty, createStripeTransfer returns early (line 44-46 in create-stripe-transfer.ts), but the user receives no feedback. The success toast (line 197 in payout-stats.tsx) will still fire, misleading users that a withdrawal was initiated.
Add validation before calling the transfer function:
const previouslyProcessedPayouts = await prisma.payout.findMany({
where: {
partnerId: partner.id,
status: "processed",
stripeTransferId: null,
},
include: {
program: {
select: {
name: true,
},
},
},
});
+ if (previouslyProcessedPayouts.length === 0) {
+ throw new Error(
+ "No processed payouts found to withdraw.",
+ );
+ }
try {📝 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.
| const previouslyProcessedPayouts = await prisma.payout.findMany({ | |
| where: { | |
| partnerId: partner.id, | |
| status: "processed", | |
| stripeTransferId: null, | |
| }, | |
| include: { | |
| program: { | |
| select: { | |
| name: true, | |
| }, | |
| }, | |
| }, | |
| }); | |
| const previouslyProcessedPayouts = await prisma.payout.findMany({ | |
| where: { | |
| partnerId: partner.id, | |
| status: "processed", | |
| stripeTransferId: null, | |
| }, | |
| include: { | |
| program: { | |
| select: { | |
| name: true, | |
| }, | |
| }, | |
| }, | |
| }); | |
| if (previouslyProcessedPayouts.length === 0) { | |
| throw new Error( | |
| "No processed payouts found to withdraw.", | |
| ); | |
| } | |
| try { |
🤖 Prompt for AI Agents
In apps/web/lib/actions/partners/force-withdrawal.ts around lines 35 to 48, the
code fetches previouslyProcessedPayouts but doesn’t handle the empty-array case,
causing createStripeTransfer to return early and the UI to still show a success
toast; add an explicit check for previouslyProcessedPayouts.length === 0 and
return or throw a clear error/result (e.g., throw new Error or return { success:
false, message: 'No processed payouts to transfer' }) so the caller/UI can show
a failure/notification instead of the success toast.
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: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-details-sheet.tsx (1)
106-106: Remove unused dependency from useMemo.The
earningsvariable is included in the dependency array but is never used in the calculation ofinvoiceData. This causes unnecessary recomputation wheneverearningschanges.Apply this diff to fix the dependency array:
- }, [payout, earnings]); + }, [payout]);apps/web/lib/partners/create-stripe-transfer.ts (3)
16-28: Return a boolean to signal whether a transfer was created.Callers need a definitive success/failure to drive UI toasts.
-export const createStripeTransfer = async ({ +export const createStripeTransfer = async ({ partner, previouslyProcessedPayouts, currentInvoicePayouts, chargeId, forceWithdrawal = false, }: { partner: Pick<Partner, "id" | "stripeConnectId">; previouslyProcessedPayouts: PayoutWithProgramName[]; currentInvoicePayouts?: PayoutWithProgramName[]; chargeId?: string; forceWithdrawal?: boolean; -}) => { +}): Promise<boolean> => {
29-46: Early exits should return false (not silent void).Avoid ambiguous success in callers.
if (!partner.stripeConnectId) { console.log(`Partner ${partner.id} has no stripeConnectId, skipping...`); - return; + return false; } @@ if (allPayouts.length === 0) { console.log(`No payouts found for partner ${partner.id}, skipping...`); - return; + return false; }
90-96: Return false when net amount ≤ 0.Prevents callers from assuming success.
if (finalTransferableAmount <= 0) { console.log( `Final transferable amount after deducting withdrawal fee (${currencyFormatter(finalTransferableAmount / 100)}) is less than or equal to 0, skipping...`, ); - return; + return false; }
♻️ Duplicate comments (6)
apps/web/lib/actions/partners/force-withdrawal.ts (4)
29-33: Clarify rate‑limit message to reference instant payout requests.Current copy says “retry attempts,” which is misleading.
- throw new Error( - "You've reached the maximum number of retry attempts for the past 24 hours. Please wait and try again later.", - ); + throw new Error( + "You've reached the maximum number of instant payout requests (5) in the past 24 hours. Please wait and try again later.", + );
50-60: Log original error for debuggability.The catch swallows root cause.
} catch (error) { - throw new Error( + console.error(`Force withdrawal failed for partner ${partner.id}:`, { + error, + processedPayoutCount: previouslyProcessedPayouts.length, + }); + throw new Error( "Failed to force withdrawal. Please try again or contact support.", ); }
35-48: Guard empty processed payouts to avoid false “success.”If none exist, the transfer call is a no‑op and the UI shows a success toast.
const previouslyProcessedPayouts = await prisma.payout.findMany({ @@ }); + if (previouslyProcessedPayouts.length === 0) { + throw new Error("No processed payouts found to withdraw."); + }
50-56: Surface transfer outcome; don’t toast success on no‑op.createStripeTransfer can early‑return (e.g., final amount ≤ 0) without throwing, but this action treats it as success. Make the helper return a boolean and act accordingly.
- try { - await createStripeTransfer({ + try { + const transferred = await createStripeTransfer({ partner, previouslyProcessedPayouts, forceWithdrawal: true, }); + if (!transferred) { + throw new Error("Nothing to transfer."); + }Follow-up change needed in create-stripe-transfer.ts to return boolean; see related comment. Based on learnings.
packages/email/src/templates/partner-payout-processed.tsx (1)
98-101: Replace hardcoded $10 / $0.50 and 1000 with shared constants.Use MIN_WITHDRAWAL_AMOUNT_CENTS and BELOW_MIN_WITHDRAWAL_FEE_CENTS with currencyFormatter for consistency and single‑source‑of‑truth.
@@ - {variant === "stripe" - ? payout.amount >= 1000 - ? "The funds will begin transferring to your connected bank account shortly. You will receive another email when the funds are on their way." - : "Since this payout is below the minimum withdrawal amount of $10, it will remain in processed status. If you'd like to receive your payout now, you can do so with a $0.50 withdrawal fee." + {variant === "stripe" + ? payout.amount >= MIN_WITHDRAWAL_AMOUNT_CENTS + ? "The funds will begin transferring to your connected bank account shortly. You will receive another email when the funds are on their way." + : `Since this payout is below the minimum withdrawal amount of ${currencyFormatter( + MIN_WITHDRAWAL_AMOUNT_CENTS / 100, + { trailingZeroDisplay: "stripIfInteger" }, + )}, it will remain in processed status. If you'd like to receive your payout now, you can do so with a ${currencyFormatter( + BELOW_MIN_WITHDRAWAL_FEE_CENTS / 100, + { trailingZeroDisplay: "stripIfInteger" }, + )} withdrawal fee.` : "Your payout is on its way to your PayPal account. You'll receive an email from PayPal when it's complete."}Add import at top (if not already exported here, re‑export from @dub/utils as a follow‑up):
+import { MIN_WITHDRAWAL_AMOUNT_CENTS, BELOW_MIN_WITHDRAWAL_FEE_CENTS } from "@dub/utils";Please confirm these constants are exported from @dub/utils for the email package; if not, move them to @dub/utils (or re-export) to avoid apps/web cross‑package coupling.
apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-stats.tsx (1)
93-103: Fix NaN risk, correct copy, show net amount, and gate button appropriately.
- Use null‑safe amounts to avoid NaN in currencyFormatter.
- If processed ≥ MIN, no fee and copy should not claim “below minimum.”
- Confirm button should show the net amount when fee applies.
- Disable actions while pending; refresh stats after success.
- {label === "Processed" && amount > 0 && ( + {label === "Processed" && amount > 0 && amount < MIN_WITHDRAWAL_AMOUNT_CENTS && ( <Button variant="secondary" text="Pay out now" className="ml-2 h-7 px-2 py-1" onClick={() => setShowForceWithdrawalModal(true)} /> )} @@ - const { executeAsync: executeForceWithdrawal } = useAction( + const { executeAsync: executeForceWithdrawal, isPending: isWithdrawing } = useAction( forceWithdrawalAction, { onSuccess: () => { toast.success("Withdrawal initiated successfully"); + // TODO: revalidate payout counts; replace key with the one used by usePartnerPayoutsCount + // mutatePrefix("/api/partner-payouts"); }, onError: ({ error }) => { toast.error(error.serverError || "Failed to initiate withdrawal"); }, }, ); - const processedPayoutAmountInUsd = currencyFormatter( - payoutStatusMap?.processed?.amount / 100, + const processedAmountCents = payoutStatusMap?.processed?.amount ?? 0; + const isBelowMin = processedAmountCents > 0 && processedAmountCents < MIN_WITHDRAWAL_AMOUNT_CENTS; + const netAmountCents = isBelowMin + ? Math.max(processedAmountCents - BELOW_MIN_WITHDRAWAL_FEE_CENTS, 0) + : processedAmountCents; + const processedPayoutAmountInUsd = currencyFormatter( + processedAmountCents / 100, { trailingZeroDisplay: "stripIfInteger", }, ); + const netPayoutAmountInUsd = currencyFormatter(netAmountCents / 100, { + trailingZeroDisplay: "stripIfInteger", + }); @@ } = useConfirmModal({ title: "Pay out funds instantly", - description: ( + description: ( <> - Since your total processed earnings ( - <strong className="text-black">{processedPayoutAmountInUsd}</strong>) - are below the minimum requirement of{" "} - <strong className="text-black"> - {currencyFormatter(MIN_WITHDRAWAL_AMOUNT_CENTS / 100, { - trailingZeroDisplay: "stripIfInteger", - })} - </strong> - , you will be charged a fee of{" "} - <strong className="text-black"> - {currencyFormatter(BELOW_MIN_WITHDRAWAL_FEE_CENTS / 100)} - </strong>{" "} - for this payout, which means you will receive{" "} - <strong className="text-black"> - {currencyFormatter( - (payoutStatusMap?.processed?.amount - - BELOW_MIN_WITHDRAWAL_FEE_CENTS) / - 100, - { trailingZeroDisplay: "stripIfInteger" }, - )} - </strong> - . + {isBelowMin ? ( + <> + Since your total processed earnings ( + <strong className="text-black">{processedPayoutAmountInUsd}</strong> + ) are below the minimum requirement of{" "} + <strong className="text-black"> + {currencyFormatter(MIN_WITHDRAWAL_AMOUNT_CENTS / 100, { + trailingZeroDisplay: "stripIfInteger", + })} + </strong> + , a{" "} + <strong className="text-black"> + {currencyFormatter(BELOW_MIN_WITHDRAWAL_FEE_CENTS / 100)} + </strong>{" "} + fee will apply. You will receive{" "} + <strong className="text-black">{netPayoutAmountInUsd}</strong>. + </> + ) : ( + <> + Your processed earnings ( + <strong className="text-black">{processedPayoutAmountInUsd}</strong> + ) will be paid out now. No fee applies. + </> + )} </> ), onConfirm: async () => { - await executeForceWithdrawal(); + await executeForceWithdrawal(); }, - confirmText: `Pay out ${processedPayoutAmountInUsd}`, + confirmText: `Pay out ${isBelowMin ? netPayoutAmountInUsd : processedPayoutAmountInUsd}`, + confirmDisabled: isWithdrawing, });Optionally, pass loading/disabled to the “Pay out now” button using isWithdrawing. Based on learnings.
Also applies to: 193-247
🧹 Nitpick comments (8)
apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-table.tsx (2)
189-197: Remove unnecessary flexbox classes or clarify intent.The wrapper
divusesjustify-betweenwith only a single child (Filter.Select). Flexbox properties likejustify-between,items-center, andgap-2have no effect with a single child element.If no additional elements are planned, simplify to:
-<div className="flex items-center justify-between gap-2"> - <Filter.Select - className="w-full md:w-fit" - filters={filters} - activeFilters={activeFilters} - onSelect={onSelect} - onRemove={onRemove} - /> -</div> +<Filter.Select + className="w-full md:w-fit" + filters={filters} + activeFilters={activeFilters} + onSelect={onSelect} + onRemove={onRemove} +/>Otherwise, if additional UI elements (e.g., action buttons) are planned for this row, please clarify with a comment.
40-40: DestructureerrorfromusePartnerPayoutsCountfor consistency and visibility.The hook returns
{ payoutsCount, error, loading }, but onlypayoutsCountis destructured. While the Table component safely handles undefinedrowCountvia its!!rowCountcheck, other files using this hook (e.g.,payout-stats.tsx) destructure theerrorstate. This pattern inconsistency masks potential API failures silently.Recommendation: Align with the pattern used in
payout-stats.tsxby adding:const { payoutsCount, error } = usePartnerPayoutsCount<number>();This improves error visibility for debugging and maintains consistency across the codebase.
packages/email/src/templates/partner-payout-processed.tsx (1)
44-46: Nit: rename saleAmountInDollars for clarity.It’s a payout amount, not sale amount.
-const saleAmountInDollars = currencyFormatter(payout.amount / 100, { +const payoutAmountInUsd = currencyFormatter(payout.amount / 100, { trailingZeroDisplay: "stripIfInteger", }); @@ -<strong className="text-black">{saleAmountInDollars}</strong> +<strong className="text-black">{payoutAmountInUsd}</strong>Also applies to: 84-85
apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/partner-payout-settings-sheet.tsx (3)
16-17: Avoid deep import; use stable utils surface.Import COUNTRY_CURRENCY_CODES from @dub/utils, not @dub/utils/src, to prevent bundling/resolution issues.
-import { CONNECT_SUPPORTED_COUNTRIES, COUNTRIES } from "@dub/utils"; -import { COUNTRY_CURRENCY_CODES } from "@dub/utils/src"; +import { CONNECT_SUPPORTED_COUNTRIES, COUNTRIES, COUNTRY_CURRENCY_CODES } from "@dub/utils";
61-73: Keep form in sync with async partner data; remove unused vars.defaultValues are only used on mount; without reset the form can render stale/empty values. Also watch and setValue are unused.
- const { - register, - handleSubmit, - watch, - setValue, - formState: { isDirty }, - } = useForm<PartnerPayoutSettingsFormData>({ + const { + register, + handleSubmit, + reset, + formState: { isDirty }, + } = useForm<PartnerPayoutSettingsFormData>({ defaultValues: { companyName: partner?.companyName || undefined, address: partner?.invoiceSettings?.address || undefined, taxId: partner?.invoiceSettings?.taxId || undefined, }, }); + + // Sync when partner loads/changes + useEffect(() => { + if (!partner) return; + reset({ + companyName: partner.companyName || undefined, + address: partner.invoiceSettings?.address || undefined, + taxId: partner.invoiceSettings?.taxId || undefined, + }); + }, [partner, reset]);
226-232: Disable Save while pending.Prevent double submits.
- <Button + <Button text="Save" className="h-8 w-fit px-3" loading={isPending} - disabled={!isDirty} + disabled={isPending || !isDirty} type="submit" />apps/web/lib/partners/create-stripe-transfer.ts (1)
102-122: Guard invoiceId presence and return true on success.Avoid non‑null assertion risk; finish with an explicit true.
- const finalPayoutInvoiceId = allPayouts[allPayouts.length - 1].invoiceId; + const finalPayoutInvoiceId = allPayouts[allPayouts.length - 1].invoiceId; + if (!finalPayoutInvoiceId) { + console.log(`Missing invoiceId on payouts for partner ${partner.id}, skipping...`); + return false; + } @@ - const transfer = await stripe.transfers.create( + const transfer = await stripe.transfers.create( { amount: finalTransferableAmount, currency: "usd", @@ - transfer_group: finalPayoutInvoiceId!, + transfer_group: finalPayoutInvoiceId, @@ ); @@ await Promise.allSettled([ @@ ]); + return true;Also applies to: 124-156
apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-stats.tsx (1)
16-17: Optional: allow instant payout even when ≥ min (if intended).If you want the button always visible, keep the button condition but use the modal copy above (no fee when ≥ min). Otherwise, gating to < MIN (diff above) matches current copy.
Confirm the intended UX: Should “Pay out now” be offered when processed ≥ minimum (to avoid waiting background runs)?
Also applies to: 96-102
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (14)
apps/web/app/(ee)/api/cron/payouts/charge-succeeded/send-stripe-payouts.ts(0 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-details-sheet.tsx(1 hunks)apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-stats.tsx(5 hunks)apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-table.tsx(2 hunks)apps/web/lib/actions/partners/force-withdrawal.ts(1 hunks)apps/web/lib/actions/partners/update-partner-payout-settings.ts(2 hunks)apps/web/lib/partners/constants.ts(1 hunks)apps/web/lib/partners/create-stripe-transfer.ts(2 hunks)apps/web/lib/zod/schemas/partners.ts(1 hunks)apps/web/ui/partners/payout-row-menu.tsx(2 hunks)apps/web/ui/partners/payout-status-descriptions.ts(1 hunks)packages/email/src/templates/partner-payout-processed.tsx(2 hunks)packages/prisma/schema/partner.prisma(0 hunks)
💤 Files with no reviewable changes (2)
- packages/prisma/schema/partner.prisma
- apps/web/app/(ee)/api/cron/payouts/charge-succeeded/send-stripe-payouts.ts
🔇 Additional comments (9)
apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-details-sheet.tsx (1)
179-234: LGTM! Well-structured layout with sticky header and footer.The layout refactor properly implements a sticky header and footer pattern with a scrollable content area. The z-index layering, flex properties, and border styling are all appropriate. This improves UX by keeping navigation elements visible while scrolling through payout details.
apps/web/ui/partners/payout-row-menu.tsx (3)
3-3: Excellent UX improvement—modal-based confirmation is clearer than inline confirm.The refactor from an inline confirmation (likely
window.confirm) to a dedicated modal provides better context and aligns with modern UX patterns. The renamed action hook outputs (executeRetryPayout,isRetryPayoutPending) improve code clarity.Also applies to: 16-25, 27-40, 48-81
60-64: Clean integration—disabled state prevents double-execution.The
MenuItemcorrectly usesisRetryPayoutPendingto prevent users from triggering multiple retry attempts while one is in flight. The flow (menu click → modal → confirm → execute) is intuitive.
27-40: All concerns verified—no action required.
Rate limit enforcement (lines 32-33): Confirmed via
retry-failed-paypal-payouts.tsline 37 — the server-side action enforces exactly 5 attempts per 24h using Upstash ratelimit. The messaging is accurate.Async onConfirm handling (lines 34-38): Confirmed via
confirm-modal.tsx— theuseConfirmModalproperly:
- Manages loading state during async execution
- Disables the confirm button while
onConfirm()is pending (vialoading={isLoading}prop)- Auto-closes the modal after success (
setShowConfirmModal(false)in line 47)The implementation correctly prevents UX confusion from error states while the modal remains open.
apps/web/lib/actions/partners/update-partner-payout-settings.ts (3)
21-24: LGTM!The
invoiceSettingsconstruction correctly handles optional fields and properly types the object for Prisma's JSON column.
26-34: No changes required—Stripe logic is properly migrated and callers don't depend on return value.Verification confirmed:
- Return value removal: Not a breaking change. The single caller in
partner-payout-settings-sheet.tsxuses the action through theuseActionhook and relies only on theonSuccesscallback; it does not capture or reference any returned value.- Stripe logic migration: Complete.
createStripeTransfer()increate-stripe-transfer.tshandles transfer creation and status updates.forceWithdrawalActioninforce-withdrawal.tshandles forced withdrawals. No remainingwaitUntilpayout logic found.The refactoring is safe and ready.
14-14: No issues found. Schema updated correctly; return value removal is appropriate; Stripe logic properly isolated.Verification complete:
- Schema consistency: ✅
partnerPayoutSettingsSchemacorrectly removedminWithdrawalAmountand now only validatescompanyName,address,taxId- Return value: ✅ Caller (
partner-payout-settings-sheet.tsx) usesuseActionhook without capturing return value; side effects handled viaonSuccess/onErrorcallbacks- Stripe logic: ✅ Withdrawal processing properly separated into
force-withdrawal.tsaction;minWithdrawalAmountremoved entirely (no references remain in codebase)The refactoring cleanly separates invoice settings updates from withdrawal processing concerns. No breaking changes or inconsistencies detected.
apps/web/ui/partners/payout-status-descriptions.ts (1)
1-11: LGTM: dynamic minimum amount formatting.Description now references MIN_WITHDRAWAL_AMOUNT_CENTS via currencyFormatter.
apps/web/lib/zod/schemas/partners.ts (1)
11-12: LGTM: removed minWithdrawalAmount from schemas.Imports cleaned; payout settings schema reflects only invoice details.
Also applies to: 755-759
| export const MIN_WITHDRAWAL_AMOUNT_CENTS = 1000; // $10 | ||
| export const BELOW_MIN_WITHDRAWAL_FEE_CENTS = 50; // $0.50 |
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.
🧩 Analysis chain
LGTM; constants align with new instant‑payout flow.
Ensure help docs and UI copy reference these values and avoid hardcoded strings elsewhere.
Run a quick scan to catch stray hardcoded $10 / $0.50:
🏁 Script executed:
#!/bin/bash
rg -nC2 '\$10|\$0\.50|\b1000\b|\b50\b' -g '!**/node_modules/**' -g '*.ts' -g '*.tsx'Length of output: 290537
Critical: Remove hardcoded monetary values from email template and import from constants
The constants are correctly defined, but the email template at packages/email/src/templates/partner-payout-processed.tsx:101 contains hardcoded strings: "Since this payout is below the minimum withdrawal amount of $10, it will remain in processed status. If you'd like to receive your payout now, you can do so with a $0.50 withdrawal fee." These should reference the constants instead to maintain a single source of truth. Update the template to import MIN_WITHDRAWAL_AMOUNT_CENTS and BELOW_MIN_WITHDRAWAL_FEE_CENTS and compute or format the display values dynamically rather than hardcoding them.
🤖 Prompt for AI Agents
In packages/email/src/templates/partner-payout-processed.tsx around line 101,
the email currently hardcodes "$10" and "$0.50"; import
MIN_WITHDRAWAL_AMOUNT_CENTS and BELOW_MIN_WITHDRAWAL_FEE_CENTS from
apps/web/lib/partners/constants and replace the hardcoded strings by computing
display values (e.g., cents / 100 formatted to two decimal places or using the
project’s currency formatter helper) so the message shows the dynamic dollar
amounts derived from those constants; ensure imports are added at the top and
the JSX uses the formatted variables.
| // If the total transferable amount is less than the minimum withdrawal amount | ||
| if (totalTransferableAmount < MIN_WITHDRAWAL_AMOUNT_CENTS) { | ||
| // if we're not forcing a withdrawal | ||
| if (!forceWithdrawal) { | ||
| // we need to update current invoice payouts to "processed" status | ||
| if (currentInvoicePayouts) { | ||
| await prisma.payout.updateMany({ | ||
| where: { | ||
| id: { | ||
| in: currentInvoicePayouts.map((p) => p.id), | ||
| }, | ||
| }, | ||
| }, | ||
| data: { | ||
| status: "processed", | ||
| }, | ||
| }); | ||
| } | ||
| data: { | ||
| status: "processed", | ||
| }, | ||
| }); | ||
| } | ||
|
|
||
| console.log( | ||
| `Total processed payouts (${currencyFormatter(totalTransferableAmount / 100)}) for partner ${partner.id} are below the minWithdrawalAmount (${currencyFormatter(partner.minWithdrawalAmount / 100)}), skipping...`, | ||
| ); | ||
| console.log( | ||
| `Total processed payouts (${currencyFormatter(totalTransferableAmount / 100)}) for partner ${partner.id} are below ${currencyFormatter(MIN_WITHDRAWAL_AMOUNT_CENTS / 100)}, skipping...`, | ||
| ); | ||
|
|
||
| return; | ||
| } | ||
| // skip creating a transfer | ||
| return; | ||
| } |
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.
🛠️ Refactor suggestion | 🟠 Major
Below‑minimum branch should return false when skipping.
Currently logs and returns undefined; standardize outcome.
if (totalTransferableAmount < MIN_WITHDRAWAL_AMOUNT_CENTS) {
if (!forceWithdrawal) {
@@
- // skip creating a transfer
- return;
+ // skip creating a transfer
+ return false;
}
// else, if we're forcing a withdrawal, we need to charge a withdrawal fee
withdrawalFee = BELOW_MIN_WITHDRAWAL_FEE_CENTS;
}📝 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.
| // If the total transferable amount is less than the minimum withdrawal amount | |
| if (totalTransferableAmount < MIN_WITHDRAWAL_AMOUNT_CENTS) { | |
| // if we're not forcing a withdrawal | |
| if (!forceWithdrawal) { | |
| // we need to update current invoice payouts to "processed" status | |
| if (currentInvoicePayouts) { | |
| await prisma.payout.updateMany({ | |
| where: { | |
| id: { | |
| in: currentInvoicePayouts.map((p) => p.id), | |
| }, | |
| }, | |
| }, | |
| data: { | |
| status: "processed", | |
| }, | |
| }); | |
| } | |
| data: { | |
| status: "processed", | |
| }, | |
| }); | |
| } | |
| console.log( | |
| `Total processed payouts (${currencyFormatter(totalTransferableAmount / 100)}) for partner ${partner.id} are below the minWithdrawalAmount (${currencyFormatter(partner.minWithdrawalAmount / 100)}), skipping...`, | |
| ); | |
| console.log( | |
| `Total processed payouts (${currencyFormatter(totalTransferableAmount / 100)}) for partner ${partner.id} are below ${currencyFormatter(MIN_WITHDRAWAL_AMOUNT_CENTS / 100)}, skipping...`, | |
| ); | |
| return; | |
| } | |
| // skip creating a transfer | |
| return; | |
| } | |
| // If the total transferable amount is less than the minimum withdrawal amount | |
| if (totalTransferableAmount < MIN_WITHDRAWAL_AMOUNT_CENTS) { | |
| // if we're not forcing a withdrawal | |
| if (!forceWithdrawal) { | |
| // we need to update current invoice payouts to "processed" status | |
| if (currentInvoicePayouts) { | |
| await prisma.payout.updateMany({ | |
| where: { | |
| id: { | |
| in: currentInvoicePayouts.map((p) => p.id), | |
| }, | |
| }, | |
| data: { | |
| status: "processed", | |
| }, | |
| }); | |
| } | |
| console.log( | |
| `Total processed payouts (${currencyFormatter(totalTransferableAmount / 100)}) for partner ${partner.id} are below ${currencyFormatter(MIN_WITHDRAWAL_AMOUNT_CENTS / 100)}, skipping...`, | |
| ); | |
| // skip creating a transfer | |
| return false; | |
| } |
🤖 Prompt for AI Agents
In apps/web/lib/partners/create-stripe-transfer.ts around lines 57 to 81 the
branch that skips creating a transfer when totalTransferableAmount <
MIN_WITHDRAWAL_AMOUNT_CENTS currently logs and returns undefined; change the
early return to return false so the function consistently returns a boolean
outcome when skipping, and ensure the function signature/return type reflects
Promise<boolean> if necessary.
Summary by CodeRabbit
New Features
Improvements
Other