-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Add pagination to eligible payouts in "Confirm payouts" sheet #3121
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.
|
|
Warning Rate limit exceeded@steven-tey has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 0 minutes and 53 seconds before requesting another review. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. 📒 Files selected for processing (2)
WalkthroughAdds a paginated eligible-payouts listing and a separate count endpoint, threads pagination and excluded IDs through backend and UI, introduces payout constants and schema pagination, and rewrites cron payout processing to an invoice-driven, batched flow with a new batched updates endpoint; also reorders a Prisma Partner index. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant UI as ConfirmPayoutsSheet
participant API_Count as GET /api/.../eligible/count
participant API_List as GET /api/.../eligible
participant Lib_Count as Prisma aggregate / getEligiblePayouts (full)
participant Lib_List as getEligiblePayouts (paged)
UI->>API_Count: request commonQuery (workspace, cutoff, excludedPayoutIds)
API_Count->>Lib_Count: resolve cutoff -> either aggregate or fetch-all
Lib_Count-->>API_Count: { count, amount }
API_Count-->>UI: totals
UI->>API_List: request page (commonQuery + page)
API_List->>Lib_List: apply where, skip/take -> fetch page
Lib_List-->>API_List: payouts[]
API_List-->>UI: page of payouts
sequenceDiagram
autonumber
participant Cron as processPayouts
participant DB as Database/Prisma
participant Q as QStash
participant Updates as POST /cron/.../updates
participant Email as PartnerEmailService
Cron->>DB: query payouts for invoice, group by mode -> sums & ids
DB-->>Cron: grouped sums, payout ids
Cron->>DB: update invoice (amounts/fee/total)
Cron->>DB: mark payouts status = processing (single update, set invoiceId)
Cron->>Q: enqueue Updates with invoiceId (+ startingAfter if needed)
Q-->>Updates: POST callback (batched)
Updates->>DB: fetch batch (limit 100), create audit logs
alt internal payouts exist & paymentMethod != card
Updates->>Email: send batch PartnerPayoutConfirmed
end
Updates->>Q: if full batch -> enqueue next batch with cursor
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)
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: 2
🧹 Nitpick comments (1)
apps/web/app/(ee)/api/programs/[programId]/payouts/eligible/route.ts (1)
22-32: Route now supports pagination, but the comment above is staleParsing the full
eligiblePayoutsQuerySchemaand spreading...queryintogetEligiblePayoutsmeans this endpoint now honours pagination parameters (page/pageSize) and any other schema-defined filters, which is great for reuse.The block comment above still states “no pagination or filtering (we retrieve all pending payouts by default)”, which no longer matches the implementation. Consider updating that comment to describe the current behavior (e.g., default sort plus optional pagination and cutoff filtering) to avoid confusion for future readers.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
apps/web/app/(ee)/api/programs/[programId]/payouts/eligible/count/route.ts(1 hunks)apps/web/app/(ee)/api/programs/[programId]/payouts/eligible/route.ts(2 hunks)apps/web/lib/api/payouts/get-eligible-payouts.ts(3 hunks)apps/web/lib/zod/schemas/payouts.ts(1 hunks)apps/web/ui/partners/confirm-payouts-sheet.tsx(5 hunks)
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/profile-details-form.tsx:180-189
Timestamp: 2025-09-24T15:50:16.414Z
Learning: TWilson023 prefers to keep security vulnerability fixes separate from refactoring PRs when the vulnerable code is existing and was only moved/relocated rather than newly introduced.
📚 Learning: 2025-06-25T21:20:59.837Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 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.
Applied to files:
apps/web/ui/partners/confirm-payouts-sheet.tsx
📚 Learning: 2025-09-24T15:47:40.293Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/about-you-form.tsx:152-157
Timestamp: 2025-09-24T15:47:40.293Z
Learning: The Button component from dub/ui automatically adds type="button" when an onClick prop is passed, preventing accidental form submissions without requiring explicit type specification. The implementation uses: type={onClick ? "button" : type}
Applied to files:
apps/web/ui/partners/confirm-payouts-sheet.tsx
📚 Learning: 2025-09-24T15:47:40.293Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/about-you-form.tsx:152-157
Timestamp: 2025-09-24T15:47:40.293Z
Learning: The Button component from dub/ui automatically adds type="button" when an onClick prop is passed, preventing accidental form submissions without requiring explicit type specification.
Applied to files:
apps/web/ui/partners/confirm-payouts-sheet.tsx
🧬 Code graph analysis (5)
apps/web/lib/api/payouts/get-eligible-payouts.ts (1)
apps/web/lib/api/payouts/payout-eligibility-filter.ts (1)
getPayoutEligibilityFilter(3-72)
apps/web/app/(ee)/api/programs/[programId]/payouts/eligible/route.ts (1)
apps/web/lib/zod/schemas/payouts.ts (1)
eligiblePayoutsQuerySchema(117-124)
apps/web/lib/zod/schemas/payouts.ts (1)
apps/web/lib/zod/schemas/misc.ts (1)
getPaginationQuerySchema(32-55)
apps/web/ui/partners/confirm-payouts-sheet.tsx (2)
apps/web/lib/zod/schemas/payouts.ts (1)
ELIGIBLE_PAYOUTS_MAX_PAGE_SIZE(115-115)apps/web/lib/types.ts (1)
PayoutResponse(494-494)
apps/web/app/(ee)/api/programs/[programId]/payouts/eligible/count/route.ts (5)
apps/web/app/(ee)/api/programs/[programId]/payouts/eligible/route.ts (1)
GET(19-35)apps/web/lib/auth/workspace.ts (1)
withWorkspace(55-494)apps/web/lib/zod/schemas/payouts.ts (1)
eligiblePayoutsCountQuerySchema(126-129)apps/web/lib/api/programs/get-program-or-throw.ts (1)
getProgramOrThrow(6-29)apps/web/lib/api/payouts/get-eligible-payouts.ts (1)
getEligiblePayoutsCount(138-152)
⏰ 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/lib/zod/schemas/payouts.ts (1)
115-129: Eligible payouts pagination schema is consistent and well‑factoredUsing
ELIGIBLE_PAYOUTS_MAX_PAGE_SIZEwithgetPaginationQuerySchemakeeps pagination constraints centralized, and derivingeligiblePayoutsCountQuerySchemaby omittingpage/pageSizeensures the count endpoint can’t accidentally receive pagination params. No issues from this change.apps/web/app/(ee)/api/programs/[programId]/payouts/eligible/count/route.ts (1)
1-27: Count route wiring matches list route; confirm ignoring[programId]is intentionalThe handler correctly reuses workspace scoping, schema validation, and the shared
getEligiblePayoutsCounthelper, so the count result should stay aligned with the list filters. It always derivesprogramIdfromgetDefaultProgramIdOrThrow(workspace)and ignores the[programId]path param, mirroring the main/eligibleroute—please confirm that this is the intended behavior for all callers.apps/web/ui/partners/confirm-payouts-sheet.tsx (1)
7-37: New imports for limit error and pagination are aligned with existing patternsImporting
exceededLimitError,ELIGIBLE_PAYOUTS_MAX_PAGE_SIZE, anduseTablePaginationcentralizes limit messaging and keeps the UI in sync with backend pagination constraints for eligible payouts. This reuse matches the payout‑limit pattern used elsewhere.Based on learnings
apps/web/lib/api/payouts/get-eligible-payouts.ts (1)
59-60: Backend assumes 1‑basedpage; ensure all callers follow that convention
getEligiblePayoutsusesskip: (page - 1) * pageSizeand, via the shared Zod pagination schema, enforcespage > 0. This makespagea 1‑based index by design.That’s fine, but it means all callers (such as the confirm‑payouts sheet) must convert any 0‑based UI page indices to 1‑based before invoking this function or its API route. With the suggested frontend fix to send
page = pageIndex + 1, this function’s behavior remains correct; just keep this contract in mind for future consumers.Also applies to: 67-72, 99-100
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 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/ui/partners/confirm-payouts-sheet.tsx (1)
300-338: Fix stale invoice metrics by addingeligiblePayoutsCountto memo depsBoth
useMemoblocks that drive invoice-level metrics depend oneligiblePayoutsCount, but it’s missing from their dependency arrays:const { amount, fee, total, fastAchFee, externalAmount } = useMemo(() => { const amount = eligiblePayoutsCount?.amount ?? 0; // ... }, [finalEligiblePayouts, selectedPaymentMethod, program?.payoutMode]); const invoiceData = useMemo(() => { return [ // ... ...(eligiblePayoutsCount && eligiblePayoutsCount.count <= 1000 ? [/* ... */] : []), { key: "Partners", value: eligiblePayoutsCount !== undefined ? ( nFormatter(eligiblePayoutsCount.count, { full: true }) ) : ( // skeleton ), }, // ... ]; }, [ amount, externalAmount, paymentMethods, selectedPaymentMethod, cutoffPeriod, cutoffPeriodOptions, selectedCutoffPeriodOption, ]);As a result, changes to
eligiblePayoutsCount(e.g., when cutoff period or exclusions change) won’t recomputeamount,Partners, or the cutoff-period visibility unless one of the listed deps also changes.Recommend including
eligiblePayoutsCountin both dependency arrays, e.g.:-}, [finalEligiblePayouts, selectedPaymentMethod, program?.payoutMode]); +}, [ + finalEligiblePayouts, + selectedPaymentMethod, + program?.payoutMode, + eligiblePayoutsCount, +]); -}, [ - amount, - externalAmount, - paymentMethods, - selectedPaymentMethod, - cutoffPeriod, - cutoffPeriodOptions, - selectedCutoffPeriodOption, -]); +}, [ + amount, + externalAmount, + paymentMethods, + selectedPaymentMethod, + cutoffPeriod, + cutoffPeriodOptions, + selectedCutoffPeriodOption, + eligiblePayoutsCount, +]);This will keep the displayed totals and partner count in sync with the latest count endpoint data.
Also applies to: 407-447, 505-513
🧹 Nitpick comments (3)
apps/web/app/(ee)/api/programs/[programId]/payouts/eligible/count/route.ts (1)
1-64: Count route correctly mirrors eligible payouts logicThe count endpoint’s two-path flow (cutoff-aware via
getEligiblePayoutsvs. plain aggregate without cutoff recompute) is consistent with the underlying eligibility logic and correctly honorsselectedPayoutIdandexcludedPayoutIds.The
pageSize: Infinityusage is safe given theisFinite(pageSize)guard ingetEligiblePayouts; just be aware this will load all eligible payouts into memory when a cutoff period is active, which may become expensive for very large programs.apps/web/app/(ee)/api/cron/payouts/process/updates/route.ts (1)
1-148: Batch processing flow is solid; consider tightening logsThe batched processing (cursor pagination, audit logs, partner emails, QStash re-enqueue) looks correct and aligns with the invoice-centric flow.
Two minor cleanups you might consider:
recordAuditLogdoesn’t return anything meaningful, soconsole.log({ auditLogResponse });will always log{ auditLogResponse: undefined }. You can drop that log or replace it with a more explicit success/failure signal.- In the catch block, the message
"Error sending Stripe payout"doesn’t match this route’s responsibility (audit logs + emails). Renaming it to something like"Error processing payout updates"would make debugging clearer.apps/web/app/(ee)/api/cron/payouts/process/process-payouts.ts (1)
2-3: Invoice-centric payout processing and QStash handoff look consistentThe refactor to:
- mark eligible payouts in bulk as
processingand attach them to the invoice,- normalize
modefor external vs hybrid programs (with a follow-up update for hybrid),- compute internal/external totals via
groupBy,- persist
amount,externalAmount,fee, andtotalon the invoice, and- charge
totalToCharge = invoiceTotal - totalExternalPayoutAmountbefore enqueueing the updates job to QStashis coherent and aligns with the new batched updates route. Using
FOREX_MARKUP_RATEin the FX conversion and logging viacurrencyFormatterhelps keep accounting and observability clear.No blocking issues spotted in this segment.
Also applies to: 18-20, 71-107, 119-149, 166-185, 202-204, 250-266
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (8)
apps/web/app/(ee)/api/cron/payouts/process/process-payouts.ts(6 hunks)apps/web/app/(ee)/api/cron/payouts/process/updates/route.ts(1 hunks)apps/web/app/(ee)/api/programs/[programId]/payouts/eligible/count/route.ts(1 hunks)apps/web/lib/api/payouts/get-eligible-payouts.ts(3 hunks)apps/web/lib/constants/payouts.ts(1 hunks)apps/web/lib/zod/schemas/payouts.ts(2 hunks)apps/web/ui/partners/confirm-payouts-sheet.tsx(8 hunks)packages/prisma/schema/partner.prisma(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/web/lib/api/payouts/get-eligible-payouts.ts
🧰 Additional context used
🧠 Learnings (8)
📓 Common learnings
Learnt from: devkiran
Repo: dubinc/dub PR: 2635
File: packages/prisma/schema/payout.prisma:24-25
Timestamp: 2025-07-11T16:28:55.693Z
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: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/profile-details-form.tsx:180-189
Timestamp: 2025-09-24T15:50:16.414Z
Learning: TWilson023 prefers to keep security vulnerability fixes separate from refactoring PRs when the vulnerable code is existing and was only moved/relocated rather than newly introduced.
📚 Learning: 2025-07-11T16:28:55.693Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2635
File: packages/prisma/schema/payout.prisma:24-25
Timestamp: 2025-07-11T16:28:55.693Z
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.
Applied to files:
packages/prisma/schema/partner.prismaapps/web/app/(ee)/api/cron/payouts/process/process-payouts.ts
📚 Learning: 2025-09-24T16:13:00.387Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: packages/prisma/schema/partner.prisma:151-153
Timestamp: 2025-09-24T16:13:00.387Z
Learning: In the Dub codebase, Prisma schemas use single-column indexes without brackets (e.g., `@index(partnerId)`) and multi-column indexes with brackets (e.g., `@index([programId, partnerId])`). This syntax pattern is consistently used throughout their schema files and works correctly with their Prisma version.
Applied to files:
packages/prisma/schema/partner.prisma
📚 Learning: 2025-06-19T01:46:45.723Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 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.
Applied to files:
apps/web/app/(ee)/api/cron/payouts/process/updates/route.ts
📚 Learning: 2025-09-24T15:50:16.414Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/profile-details-form.tsx:180-189
Timestamp: 2025-09-24T15:50:16.414Z
Learning: TWilson023 prefers to keep security vulnerability fixes separate from refactoring PRs when the vulnerable code is existing and was only moved/relocated rather than newly introduced.
Applied to files:
apps/web/ui/partners/confirm-payouts-sheet.tsx
📚 Learning: 2025-06-25T21:20:59.837Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 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.
Applied to files:
apps/web/ui/partners/confirm-payouts-sheet.tsxapps/web/lib/zod/schemas/payouts.ts
📚 Learning: 2025-09-24T15:47:40.293Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/about-you-form.tsx:152-157
Timestamp: 2025-09-24T15:47:40.293Z
Learning: The Button component from dub/ui automatically adds type="button" when an onClick prop is passed, preventing accidental form submissions without requiring explicit type specification. The implementation uses: type={onClick ? "button" : type}
Applied to files:
apps/web/ui/partners/confirm-payouts-sheet.tsx
📚 Learning: 2025-09-24T15:47:40.293Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/about-you-form.tsx:152-157
Timestamp: 2025-09-24T15:47:40.293Z
Learning: The Button component from dub/ui automatically adds type="button" when an onClick prop is passed, preventing accidental form submissions without requiring explicit type specification.
Applied to files:
apps/web/ui/partners/confirm-payouts-sheet.tsx
🧬 Code graph analysis (5)
apps/web/app/(ee)/api/programs/[programId]/payouts/eligible/count/route.ts (7)
apps/web/app/(ee)/api/programs/[programId]/payouts/eligible/route.ts (1)
GET(19-35)apps/web/lib/auth/workspace.ts (1)
withWorkspace(55-494)apps/web/lib/zod/schemas/payouts.ts (1)
eligiblePayoutsCountQuerySchema(125-134)apps/web/lib/api/programs/get-program-or-throw.ts (1)
getProgramOrThrow(6-29)apps/web/lib/api/payouts/get-eligible-payouts.ts (1)
getEligiblePayouts(21-120)packages/prisma/index.ts (1)
prisma(3-9)apps/web/lib/api/payouts/payout-eligibility-filter.ts (1)
getPayoutEligibilityFilter(3-72)
apps/web/app/(ee)/api/cron/payouts/process/updates/route.ts (5)
packages/prisma/index.ts (1)
prisma(3-9)apps/web/lib/api/audit-logs/record-audit-log.ts (1)
recordAuditLog(47-74)packages/email/src/index.ts (1)
sendBatchEmail(31-70)packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(7-22)apps/web/lib/api/errors.ts (1)
handleAndReturnErrorResponse(162-165)
apps/web/ui/partners/confirm-payouts-sheet.tsx (3)
apps/web/lib/constants/payouts.ts (1)
ELIGIBLE_PAYOUTS_MAX_PAGE_SIZE(14-14)apps/web/lib/types.ts (1)
PayoutResponse(494-494)packages/ui/src/combobox/index.tsx (2)
Combobox(86-387)ComboboxOption(29-37)
apps/web/lib/zod/schemas/payouts.ts (2)
apps/web/lib/zod/schemas/misc.ts (1)
getPaginationQuerySchema(32-55)apps/web/lib/constants/payouts.ts (1)
ELIGIBLE_PAYOUTS_MAX_PAGE_SIZE(14-14)
apps/web/app/(ee)/api/cron/payouts/process/process-payouts.ts (3)
packages/prisma/index.ts (1)
prisma(3-9)apps/web/lib/constants/payouts.ts (1)
FOREX_MARKUP_RATE(7-7)packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(7-22)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (3)
packages/prisma/schema/partner.prisma (1)
40-40: Intentional design change to support invoice-centric batched payout processing.Removing the
@uniqueconstraint onpayoutMethodHashis the correct change for allowing multiple payout records to share the same payout method hash under the new batched payout flow. This aligns with the pattern established in the payout schema wherestripeTransferIdis no longer unique. Based on learningsapps/web/lib/constants/payouts.ts (1)
14-15: Centralized eligible payouts page-size limit looks goodDefining
ELIGIBLE_PAYOUTS_MAX_PAGE_SIZE = 500here and reusing it in schemas/UI is a clean way to keep pagination limits consistent.apps/web/ui/partners/confirm-payouts-sheet.tsx (1)
91-135: Pagination + count wiring looks coherentThe way you:
- derive
excludedPayoutIdsfromsearchParamsObj,- centralize query params in
commonQuery,- call both the list and count endpoints with consistent params, and
- feed
pagination,onPaginationChange, androwCount: eligiblePayoutsCount?.count ?? 0intoTableall lines up nicely with the new backend schemas and count route. The exclusions propagated via queryParams and confirmed in the action payload also look correct.
Also applies to: 515-620
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (3)
apps/web/lib/zod/schemas/payouts.ts (1)
1-1: Eligible payouts pagination/count schemas are consistent; consider normalizing excluded IDs shapeThe new
eligiblePayoutsQuerySchema+getPaginationQuerySchema({ pageSize: ELIGIBLE_PAYOUTS_MAX_PAGE_SIZE })wiring looks solid and keeps client/server aligned on page size. MakingeligiblePayoutsCountQuerySchemaextend that base and then omitpage/pageSizeis also a clean approach.For
excludedPayoutIds, the schema:excludedPayoutIds: z .union([z.string(), z.array(z.string())]) .transform((v) => (Array.isArray(v) ? v : v.split(","))) .optional()means:
- When the query param is present, downstream always sees a
string[](nice).- When the param is absent, the field is
undefined(transform is skipped).If the count route and any shared helpers already treat
undefinedas “no exclusions”, this is perfectly fine. If they instead expect to always work with an array, you might want to normalize missing values to[](e.g., by applying.optional()first and transformingnull/undefinedto an empty array).Not a blocker, but worth double‑checking the downstream expectations.
Also applies to: 116-135
apps/web/ui/partners/confirm-payouts-sheet.tsx (2)
300-338: External amount is now page‑local while total amount is global
amountis now taken fromeligiblePayoutsCount.amount(all included payouts across pages), butexternalAmountis still computed fromfinalEligiblePayouts(the current page only) and the “External Amount” row is gated byfinalEligiblePayouts.some(isExternalPayout).For multi‑page results, this means:
- “Amount” reflects the full invoice total.
- “External Amount” reflects only the external portion of the current page, not the entire invoice.
That can be confusing for users interpreting the header as a global summary.
If you care about global accuracy here, consider one of:
- Extending the count endpoint (or adding a companion endpoint) to return global
externalAmountand using that instead of a per‑page reduction, or- Explicitly scoping the label/tooltip to “current page” or hiding the row when there are multiple pages.
Not critical for basic pagination, but worth aligning semantics so the invoice summary is either fully global or clearly labeled as per‑page.
Also applies to: 458-474
300-338: Tighten useMemo dependencies to avoid subtle stalenessBoth the amount/externalAmount computation and
invoiceDataassembly now depend (directly or indirectly) oneligiblePayoutsCount, but that object isn’t in the dependency arrays:
amountis derived fromeligiblePayoutsCount?.amountin auseMemothat currently depends on[finalEligiblePayouts, selectedPaymentMethod, program?.payoutMode].invoiceDatauseseligiblePayoutsCountfor the “Partners” row and Cutoff Period visibility but only lists[amount, externalAmount, paymentMethods, selectedPaymentMethod, cutoffPeriod, cutoffPeriodOptions, selectedCutoffPeriodOption]as dependencies.This will usually work because the same actions that change
eligiblePayoutsCountalso change other dependencies, but it’s brittle and can lead to stale header values if the count is revalidated independently of those other inputs.Consider adding
eligiblePayoutsCount(or at least its.count/.amountfields) to the relevant dependency arrays so React’s memoization reflects the actual data dependencies more explicitly.Also applies to: 340-505
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
apps/web/app/(ee)/api/cron/payouts/process/route.ts(2 hunks)apps/web/lib/zod/schemas/payouts.ts(2 hunks)apps/web/ui/partners/confirm-payouts-sheet.tsx(8 hunks)packages/prisma/schema/partner.prisma(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/prisma/schema/partner.prisma
🧰 Additional context used
🧠 Learnings (6)
📓 Common learnings
Learnt from: devkiran
Repo: dubinc/dub PR: 2635
File: packages/prisma/schema/payout.prisma:24-25
Timestamp: 2025-07-11T16:28:55.693Z
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
Repo: dubinc/dub PR: 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.
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/profile-details-form.tsx:180-189
Timestamp: 2025-09-24T15:50:16.414Z
Learning: TWilson023 prefers to keep security vulnerability fixes separate from refactoring PRs when the vulnerable code is existing and was only moved/relocated rather than newly introduced.
📚 Learning: 2025-09-24T15:50:16.414Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/profile-details-form.tsx:180-189
Timestamp: 2025-09-24T15:50:16.414Z
Learning: TWilson023 prefers to keep security vulnerability fixes separate from refactoring PRs when the vulnerable code is existing and was only moved/relocated rather than newly introduced.
Applied to files:
apps/web/ui/partners/confirm-payouts-sheet.tsx
📚 Learning: 2025-06-25T21:20:59.837Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 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.
Applied to files:
apps/web/ui/partners/confirm-payouts-sheet.tsxapps/web/lib/zod/schemas/payouts.ts
📚 Learning: 2025-09-24T15:47:40.293Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/about-you-form.tsx:152-157
Timestamp: 2025-09-24T15:47:40.293Z
Learning: The Button component from dub/ui automatically adds type="button" when an onClick prop is passed, preventing accidental form submissions without requiring explicit type specification. The implementation uses: type={onClick ? "button" : type}
Applied to files:
apps/web/ui/partners/confirm-payouts-sheet.tsx
📚 Learning: 2025-09-24T15:47:40.293Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/about-you-form.tsx:152-157
Timestamp: 2025-09-24T15:47:40.293Z
Learning: The Button component from dub/ui automatically adds type="button" when an onClick prop is passed, preventing accidental form submissions without requiring explicit type specification.
Applied to files:
apps/web/ui/partners/confirm-payouts-sheet.tsx
📚 Learning: 2025-07-30T15:29:54.131Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2673
File: apps/web/ui/partners/rewards/rewards-logic.tsx:268-275
Timestamp: 2025-07-30T15:29:54.131Z
Learning: In apps/web/ui/partners/rewards/rewards-logic.tsx, when setting the entity field in a reward condition, dependent fields (attribute, operator, value) should be reset rather than preserved because different entities (customer vs sale) have different available attributes. Maintaining existing fields when the entity changes would create invalid state combinations and confusing UX.
Applied to files:
apps/web/lib/zod/schemas/payouts.ts
🧬 Code graph analysis (2)
apps/web/ui/partners/confirm-payouts-sheet.tsx (2)
apps/web/lib/constants/payouts.ts (1)
ELIGIBLE_PAYOUTS_MAX_PAGE_SIZE(14-14)packages/ui/src/combobox/index.tsx (2)
Combobox(86-387)ComboboxOption(29-37)
apps/web/lib/zod/schemas/payouts.ts (2)
apps/web/lib/zod/schemas/misc.ts (1)
getPaginationQuerySchema(32-55)apps/web/lib/constants/payouts.ts (1)
ELIGIBLE_PAYOUTS_MAX_PAGE_SIZE(14-14)
⏰ 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/cron/payouts/process/route.ts (1)
8-8: Centralized success logging/response looks goodSwitching the success path to
logAndRespondkeeps behavior consistent while centralizing logging/response shaping. The new import and usage are coherent with the existing error handling pattern.Also applies to: 76-76
apps/web/ui/partners/confirm-payouts-sheet.tsx (3)
5-8: Pagination wiring with shared page size constant looks solidImporting
ELIGIBLE_PAYOUTS_MAX_PAGE_SIZEfrom the constants module and passing it into bothuseTablePaginationand the backend Zod pagination schema keeps the list and API in sync on page size. TheuseTablePagination({ pageSize, page, onPageChange })setup and passingpagination,onPaginationChange: setPagination, androwCountthrough to<Table />follow the expected pattern and should give consistent pagination behavior across the sheet.Also applies to: 37-38, 115-120, 617-619
91-103: Count endpoint + exclusions integration appears correctThe new
excludedPayoutIdshandling andcommonQuerycomposition:
- Deriving
excludedPayoutIdsfromsearchParamsObjand only adding the param when there are exclusions avoids forcing the API to deal with empty values.- Reusing
commonQueryfor both/eligible/countand/eligiblekeeps the two endpoints aligned on filters (workspaceId, cutoffPeriod, selectedPayoutId, exclusions).- Sourcing
amountfromeligiblePayoutsCount.amountand the “Partners” row fromeligiblePayoutsCount.countensures invoice totals and partner counts reflect all included payouts, not just the current page.- Passing
excludedPayoutIdsas an array intoconfirmPayoutsActionmatches how the backend cron/process route now acceptsexcludedPayoutIds, so the invoice/payout data should stay consistent with the UI summary and filters.- The disabled tooltip logic for limit checks and minimum invoice amount still matches the previous behavior, now based on the count‑backed
amount.Overall this new count + pagination + exclusion flow looks coherent with the backend changes and invoice confirmation semantics.
Also applies to: 104-113, 127-135, 441-447, 687-747
552-589: Exclude/Include behavior and URL syncing are well‑structuredThe updated Exclude/Include button logic:
- Drives exclusions purely from
excludedPayoutIdsinsearchParamsObj, buildingnewExcludedPayoutIdson click and then writing/removing theexcludedPayoutIdsquery param viaqueryParams.- Relies on the next render’s
searchParamsObjto recomputeexcludedPayoutIds, keeping UI, count endpoint, and confirmPayoutsAction all in sync via a single source of truth.- Feeds the same
excludedPayoutIdsarray intoconfirmPayoutsAction, which matches the revised cron/process schema and ensures the invoice/payout processing respects the UI’s exclusions.This is a clean pattern for keeping filters, URL state, and server behavior aligned.
Also applies to: 687-704
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/web/ui/partners/confirm-payouts-sheet.tsx (1)
302-340: IncludeeligiblePayoutsCountin the totals useMemo dependenciesThis block calculates
amount,fee,total, etc. fromeligiblePayoutsCount?.amount:const { amount, fee, total, fastAchFee, externalAmount } = useMemo(() => { const amount = eligiblePayoutsCount?.amount; ... }, [finalEligiblePayouts, selectedPaymentMethod, program?.payoutMode]);but
eligiblePayoutsCountisn’t in the dependency array. If the count/amount SWR updates whilefinalEligiblePayoutsand the other deps stay the same, the memo will keep returning stale totals.Recommend adding
eligiblePayoutsCount(oreligiblePayoutsCount?.amount) as a dependency:- }, [finalEligiblePayouts, selectedPaymentMethod, program?.payoutMode]); + }, [ + finalEligiblePayouts, + selectedPaymentMethod, + program?.payoutMode, + eligiblePayoutsCount, + ]);This ensures invoice totals always reflect the latest backend-computed amount.
♻️ Duplicate comments (1)
apps/web/ui/partners/confirm-payouts-sheet.tsx (1)
97-123: Pagination + common query wiring looks correct (no off-by-one)Building
commonQueryonce and then passing it into:
- the count endpoint, and
- the eligible payouts endpoint with
page: pagination.pageIndex.toString()works fine with your
useTablePaginationhook, since it already uses a 1-basedpageIndex. That keeps the frontend and backend pagination in sync without extra conversion and matches the behavior you confirmed in manual testing.Also applies to: 129-137
🧹 Nitpick comments (5)
apps/web/lib/actions/partners/confirm-payouts.ts (2)
91-108: Align cutoff-period count query with actual eligible-at-cutoff filterThe cutoff-period guard currently counts payouts using only
getPayoutEligibilityFilter(program)plus selection/exclusion:where: { ...(selected/excluded), ...getPayoutEligibilityFilter(program), },but does not apply the cutoff-specific
periodStart/periodEndconditions thatgetEligiblePayoutsuses. That means the error:“You cannot specify a cutoff period when the number of eligible payouts is greater than …”
may trigger based on all pending payouts for the program, not just the subset that would actually be included for the chosen cutoff date.
If the intent is to cap on “eligible under this cutoff,” consider reusing the same cutoff conditions as
getEligiblePayouts(i.e., thecutoffPeriodValue-basedORonperiodStart/periodEnd) so the count matches the eventual invoice set. Otherwise, the current implementation is stricter than the message suggests.
111-119: Consider a lighter check for external payouts instead ofpageSize: InfinityFor non-
internalprograms,getEligiblePayoutsis called withpage: 1andpageSize: Infinitysolely to detect whether any external payout is present:const [eligiblePayouts, payoutWebhooks] = await Promise.all([ getEligiblePayouts({ ..., page: 1, pageSize: Infinity }), getWebhooks(...), ]); const hasExternalPayouts = eligiblePayouts.find((p) => p.mode === "external");If a program accumulates a large number of eligible payouts, this will load the entire set into memory just to compute a boolean. You could instead:
- Add a small helper query (e.g.,
existsExternalEligiblePayout) that mirrors the eligibility filter and returnstrue/false, or- Call
getEligiblePayoutswith a smallpageSize(e.g., 1) and an ordering that surfaces external payouts first, if that’s feasible.Not urgent, but worth considering to keep this path cheap as programs scale.
apps/web/app/(ee)/api/cron/payouts/process/updates/route.ts (1)
140-149: Fix misleading error log message in updates routeThe catch block logs:
await log({ message: `Error sending Stripe payout: ${errorMessage}`, ... });but this route only handles audit logs and emails for already-processed payouts. Renaming the message to something like “Error processing payout updates” will make debugging much clearer and avoid confusion with the actual payout-processing step.
apps/web/app/(ee)/api/cron/payouts/process/process-payouts.ts (1)
157-202: Verify FX quote semantics match the conversion formulaThe non-USD branch:
const fxQuote = await createFxQuote({ fromCurrency: currency, toCurrency: "usd", }); const exchangeRate = fxQuote.rates[currency].exchange_rate; const convertedTotal = Math.round( (totalToCharge / exchangeRate) * (1 + FOREX_MARKUP_RATE), ); totalToCharge = convertedTotal;assumes
exchange_rateis expressed as USD per 1 unit ofcurrency, so dividing a USD-denominatedtotalToChargeby that rate yields the amount incurrency. IfcreateFxQuote(or Stripe’s API) instead returns the inverse (target-per-USD), this would invert the conversion.Worth double-checking
createFxQuote’s contract to ensure the direction matches this formula; if not, the division vs multiplication would need to be swapped.apps/web/ui/partners/confirm-payouts-sheet.tsx (1)
461-477: External Amount remains page-scoped while totals are global
externalAmountis still computed via a reduce overfinalEligiblePayouts, i.e. the currently loaded page:const externalAmount = finalEligiblePayouts?.reduce( (acc, payout) => (isExternalPayout(payout) ? acc + payout.amount : acc), 0, );while
amount/Partnersare now based on the globaleligiblePayoutsCount. If the invoice spans multiple pages, the “External Amount” row will under-report relative to the final invoice.If you’d prefer all figures in the summary to be global, consider extending the count endpoint to return an
externalAmountaggregate as well and using that instead of the per-page reduce.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
apps/web/app/(ee)/api/cron/payouts/process/process-payouts.ts(6 hunks)apps/web/app/(ee)/api/cron/payouts/process/updates/route.ts(1 hunks)apps/web/lib/actions/partners/confirm-payouts.ts(3 hunks)apps/web/lib/constants/payouts.ts(1 hunks)apps/web/ui/partners/confirm-payouts-sheet.tsx(9 hunks)
🧰 Additional context used
🧠 Learnings (7)
📓 Common learnings
Learnt from: devkiran
Repo: dubinc/dub PR: 2635
File: packages/prisma/schema/payout.prisma:24-25
Timestamp: 2025-07-11T16:28:55.693Z
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
Repo: dubinc/dub PR: 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.
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/profile-details-form.tsx:180-189
Timestamp: 2025-09-24T15:50:16.414Z
Learning: TWilson023 prefers to keep security vulnerability fixes separate from refactoring PRs when the vulnerable code is existing and was only moved/relocated rather than newly introduced.
📚 Learning: 2025-06-19T01:46:45.723Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 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.
Applied to files:
apps/web/app/(ee)/api/cron/payouts/process/updates/route.ts
📚 Learning: 2025-06-25T21:20:59.837Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 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.
Applied to files:
apps/web/app/(ee)/api/cron/payouts/process/process-payouts.tsapps/web/lib/actions/partners/confirm-payouts.tsapps/web/ui/partners/confirm-payouts-sheet.tsx
📚 Learning: 2025-07-11T16:28:55.693Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2635
File: packages/prisma/schema/payout.prisma:24-25
Timestamp: 2025-07-11T16:28:55.693Z
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.
Applied to files:
apps/web/app/(ee)/api/cron/payouts/process/process-payouts.ts
📚 Learning: 2025-09-24T15:50:16.414Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/profile-details-form.tsx:180-189
Timestamp: 2025-09-24T15:50:16.414Z
Learning: TWilson023 prefers to keep security vulnerability fixes separate from refactoring PRs when the vulnerable code is existing and was only moved/relocated rather than newly introduced.
Applied to files:
apps/web/ui/partners/confirm-payouts-sheet.tsx
📚 Learning: 2025-09-24T15:47:40.293Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/about-you-form.tsx:152-157
Timestamp: 2025-09-24T15:47:40.293Z
Learning: The Button component from dub/ui automatically adds type="button" when an onClick prop is passed, preventing accidental form submissions without requiring explicit type specification. The implementation uses: type={onClick ? "button" : type}
Applied to files:
apps/web/ui/partners/confirm-payouts-sheet.tsx
📚 Learning: 2025-09-24T15:47:40.293Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/about-you-form.tsx:152-157
Timestamp: 2025-09-24T15:47:40.293Z
Learning: The Button component from dub/ui automatically adds type="button" when an onClick prop is passed, preventing accidental form submissions without requiring explicit type specification.
Applied to files:
apps/web/ui/partners/confirm-payouts-sheet.tsx
🧬 Code graph analysis (4)
apps/web/app/(ee)/api/cron/payouts/process/updates/route.ts (6)
packages/prisma/index.ts (1)
prisma(3-9)apps/web/lib/api/audit-logs/record-audit-log.ts (1)
recordAuditLog(47-74)packages/email/src/index.ts (1)
sendBatchEmail(31-70)packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(7-22)packages/email/src/templates/partner-payout-confirmed.tsx (1)
PartnerPayoutConfirmed(18-147)apps/web/lib/api/errors.ts (1)
handleAndReturnErrorResponse(162-165)
apps/web/app/(ee)/api/cron/payouts/process/process-payouts.ts (4)
packages/prisma/index.ts (1)
prisma(3-9)apps/web/lib/constants/payouts.ts (1)
FOREX_MARKUP_RATE(7-7)apps/web/lib/stripe/index.ts (1)
stripe(4-10)packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(7-22)
apps/web/lib/actions/partners/confirm-payouts.ts (3)
apps/web/lib/constants/payouts.ts (2)
INVOICE_MIN_PAYOUT_AMOUNT_CENTS(11-11)CUTOFF_PERIOD_MAX_PAYOUTS(16-16)apps/web/lib/api/payouts/payout-eligibility-filter.ts (1)
getPayoutEligibilityFilter(3-72)apps/web/lib/api/payouts/get-eligible-payouts.ts (1)
getEligiblePayouts(21-120)
apps/web/ui/partners/confirm-payouts-sheet.tsx (3)
apps/web/lib/constants/payouts.ts (3)
ELIGIBLE_PAYOUTS_MAX_PAGE_SIZE(15-15)CUTOFF_PERIOD_MAX_PAYOUTS(16-16)INVOICE_MIN_PAYOUT_AMOUNT_CENTS(11-11)apps/web/lib/types.ts (1)
PayoutResponse(494-494)packages/ui/src/combobox/index.tsx (2)
Combobox(86-387)ComboboxOption(29-37)
⏰ 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 (9)
apps/web/lib/constants/payouts.ts (1)
11-16: New payout constants are consistent and well-scopedCentralizing the invoice min amount and pagination/threshold limits here keeps the payouts flow configurable and avoids hard-coded numbers in actions/UI. The values and units align with existing payout constants.
apps/web/lib/actions/partners/confirm-payouts.ts (1)
80-84: Server-side min invoice amount check matches shared constantUsing
INVOICE_MIN_PAYOUT_AMOUNT_CENTShere keeps the server validation in sync with the UI disablement logic and avoids hard-coding1000. This is a clean way to enforce the $10 minimum at the action boundary.apps/web/app/(ee)/api/cron/payouts/process/process-payouts.ts (2)
60-141: Invoice-centric batch update and groupBy totals look solidThe flow of:
updateManyto attach all eligible payouts to the invoice and mark them"processing",- hybrid-specific
updateManyto flipmodeto"external"for partners withoutpayoutsEnabledAt,groupBy({ by: ["mode"], where: { invoiceId }, _sum: { amount: true } })to derive internal/external totals,is a clean, DB-driven way to compute invoice amounts and avoid per-payout loops. Combined with the subsequent invoice update and
project.payoutsUsageincrement, this aligns well with the two-stage validation pattern (fast check in the action, authoritative amounts here).
Based on learnings
246-257: QStash trigger wiring for updates endpoint is appropriatePublishing a single JSON message to
/api/cron/payouts/process/updateswith just theinvoiceIdand then letting that route batch over payouts by cursor keeps the main processing flow focused on Stripe+DB work, while delegating emails/audit logs to a separate, retryable path. The console logging onmessageIdis also useful for correlating QStash deliveries in logs.apps/web/ui/partners/confirm-payouts-sheet.tsx (5)
409-452: Cutoff-period visibility gating based on total count is a nice safety valveConditionally showing the “Cutoff Period” combobox only when
eligiblePayoutsCountexists andeligiblePayoutsCount.count <= CUTOFF_PERIOD_MAX_PAYOUTSkeeps the UI aligned with the backend safeguard that rejects cutoffs for very large eligible sets. That should prevent users from selecting a cutoff that the server will immediately refuse.
444-448: Using global count for “Partners” is aligned with paginated listingSwitching the “Partners” value from
eligiblePayouts.length(page-scoped) toeligiblePayoutsCount.countmakes the invoice summary reflect the total number of eligible partners across all pages, which is what users generally care about when confirming an invoice.
555-592: Exclude/Include toggle correctly syncs with URL state and filtersThe Exclude/Include button:
- toggles the row’s ID in
excludedPayoutIds,- writes the updated list back into the
excludedPayoutIdsquery param (or deletes it when empty),- and relies on
commonQuery+ backend filtering for both count and eligible payouts.That keeps the table, invoice summary, and URL state in sync without doing extra filtering work on the client beyond the small
finalEligiblePayoutsfallback.
619-622: Table pagination integration is wired correctlyPassing
pagination,onPaginationChange: setPagination, androwCount: eligiblePayoutsCount?.count ?? 0into theTablecomponent matches how you’ve set upuseTablePaginationand the count endpoint. TheresourceNamecallback is also a nice touch for consistent empty/loading copy.
744-746: Min-invoice tooltip text correctly mirrors server validationThe disabled tooltip branch:
) : amount && amount < INVOICE_MIN_PAYOUT_AMOUNT_CENTS ? ( "Your payout total is less than the minimum invoice amount of $10." ) : (matches the server-side
INVOICE_MIN_PAYOUT_AMOUNT_CENTSguard inconfirmPayoutsAction, so users get a clear, pre-submit explanation of why they can’t confirm. Good reuse of the shared constant to avoid drift between UI and backend behavior.
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: 1
🧹 Nitpick comments (4)
apps/web/app/(ee)/api/cron/payouts/process/updates/route.ts (2)
63-83: Audit‑log actor fallback looks good; consider explicitactor.typefor system eventsUsing
actor: { id: payout.userId ?? "system" }is a solid improvement over the earlier non‑null assertion and makes this batch more tolerant of missinguserId. One small enhancement to keep analytics and querying clean would be to set an explicitactor.typewhen you fall back to"system", e.g."system"or"cron", instead of relying on the default"user"intransformAuditLogTB.
90-151: Align error log message with this route’s responsibilityIn the catch block, the log message is
"Error sending Stripe payout", but this route is only handling audit‑log writes, partner payout emails, and QStash pagination for updates. Renaming this message to something like"Error processing payout updates"would make debugging clearer.apps/web/app/(ee)/api/cron/payouts/process/process-payouts.ts (2)
247-258: Consider adding a comment for clarity and consistencyThe QStash TypeScript client's
publishJSONuses POST by default whenmethodis omitted, so the code is correct. To improve clarity and maintain parity with the explicitmethod: "POST"in the updates route, consider adding a brief comment like// QStash defaults to POSTabove or alongside theqstash.publishJSONcall.
158-203: Optional robustness improvements confirmed by Stripe API documentationStripe's FX Quote API returns a
ratesobject keyed by thefromCurrencywith anexchange_ratefield, so the current code is safe when the API succeeds. However, the two robustness suggestions remain worthwhile:
- Add a guard checking that
currencyexists infxQuote.ratesbefore accessingexchange_rate(line ~178)- Use a typed predicate at line ~175:
if (paymentMethod.type in nonUsdPaymentMethodTypes)instead ofObject.keys(nonUsdPaymentMethodTypes).includes(paymentMethod.type)for better type safetyThese are optional improvements to harden the code against edge cases and improve maintainability.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
apps/web/app/(ee)/api/cron/payouts/process/process-payouts.ts(6 hunks)apps/web/app/(ee)/api/cron/payouts/process/updates/route.ts(1 hunks)apps/web/lib/actions/partners/confirm-payouts.ts(3 hunks)apps/web/lib/api/audit-logs/record-audit-log.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/web/lib/actions/partners/confirm-payouts.ts
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: steven-tey
Repo: dubinc/dub PR: 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.
Learnt from: devkiran
Repo: dubinc/dub PR: 2635
File: packages/prisma/schema/payout.prisma:24-25
Timestamp: 2025-07-11T16:28:55.693Z
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: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/profile-details-form.tsx:180-189
Timestamp: 2025-09-24T15:50:16.414Z
Learning: TWilson023 prefers to keep security vulnerability fixes separate from refactoring PRs when the vulnerable code is existing and was only moved/relocated rather than newly introduced.
📚 Learning: 2025-06-19T01:46:45.723Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 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.
Applied to files:
apps/web/app/(ee)/api/cron/payouts/process/updates/route.ts
📚 Learning: 2025-07-11T16:28:55.693Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2635
File: packages/prisma/schema/payout.prisma:24-25
Timestamp: 2025-07-11T16:28:55.693Z
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.
Applied to files:
apps/web/app/(ee)/api/cron/payouts/process/process-payouts.ts
🧬 Code graph analysis (2)
apps/web/app/(ee)/api/cron/payouts/process/updates/route.ts (6)
packages/prisma/index.ts (1)
prisma(3-9)apps/web/lib/api/audit-logs/record-audit-log.ts (1)
recordAuditLog(47-74)packages/email/src/index.ts (1)
sendBatchEmail(31-70)packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(7-22)packages/email/src/templates/partner-payout-confirmed.tsx (1)
PartnerPayoutConfirmed(18-147)apps/web/lib/api/errors.ts (1)
handleAndReturnErrorResponse(162-165)
apps/web/app/(ee)/api/cron/payouts/process/process-payouts.ts (4)
packages/prisma/index.ts (1)
prisma(3-9)apps/web/lib/constants/payouts.ts (1)
FOREX_MARKUP_RATE(7-7)apps/web/lib/stripe/index.ts (1)
stripe(4-10)packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(7-22)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (3)
apps/web/app/(ee)/api/cron/payouts/process/process-payouts.ts (1)
60-97: ****The concern raised in the original review is incorrect.
getPayoutEligibilityFilterexplicitly enforcesinvoiceId: nullin all payout modes (internal, external, and hybrid) as part of itscommonWherefilter that is spread into every branch. This guarantee means already-invoiced payouts cannot be reassigned to a new invoice. The suggested diff to add explicitinvoiceId: nullat the call site is unnecessary.Likely an incorrect or invalid review comment.
apps/web/app/(ee)/api/cron/payouts/process/updates/route.ts (1)
86-120: Non-null assertion is safe due to intentionalsendBatchEmailfiltering patternThe Partner model's email field is nullable (
email String?), but the review comment overlooks how this codebase is designed:sendBatchEmail(in packages/email/src/send-via-resend.ts) intentionally filters out entries with null/undefinedtoaddresses internally. Call sites safely use non-null assertions because the email sending layer handles null values before sending.apps/web/lib/api/audit-logs/record-audit-log.ts (1)
58-74: All call sites are compatible with the return type change—no fixes neededVerified all 40+ call sites of
recordAuditLog: the vast majority use fire-and-forget pattern (noawait), which is universally compatible with any Promise type. The three call sites that doawaitare all compatible—two don't capture the result, and one logs it (JSON-serializable with no issues). No call site has type annotations expectingvoid, so the change fromPromise<void>toPromise<IngestResult | undefined>is fully backward compatible and safe.
Summary by CodeRabbit
New Features
Improvements