-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Add initiatedAt column to Payout table #3115
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 17 minutes and 8 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 an optional initiatedAt timestamp to Payout records, sets initiatedAt when payouts move to processing (and includes it in audit log targets), provides a backfill migration to populate existing nulls, and surfaces initiated/paid timestamps in multiple payout UIs. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant Cron as Cron job
participant DB as Database (Payout / Invoice)
participant Audit as Audit service
participant UI as Dashboard UI
Cron->>DB: Query eligible payouts (internal/external)
Cron->>DB: Update payout.status -> processing\nset initiatedAt = now()
Cron->>Audit: Create audit log with target metadata (includes initiatedAt)
DB-->>Cron: Updated payout rows
Note right of UI `#dfe7ff`: UIs read `initiatedAt` / `paidAt` and\nrender via TimestampTooltip
UI->>DB: Fetch payouts -> render Initiated / Paid cells/tooltips
Estimated code review effort๐ฏ 3 (Moderate) | โฑ๏ธ ~25 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: 1
๐งน Nitpick comments (1)
apps/web/app/(ee)/api/cron/payouts/process/process-payouts.ts (1)
338-363: Consider using a shared timestamp for consistency.The audit log creates a new
new Date()at line 357, which may differ slightly from the timestamps recorded in the database at lines 286 and 304. While the difference is minimal, it could cause minor inconsistencies in audit trails.Consider capturing a single timestamp before the updates:
+ const initiatedAt = new Date(); + // Mark internal payouts as processing if (internalPayouts.length > 0) { await prisma.payout.updateMany({ where: { id: { in: internalPayouts.map((p) => p.id), }, }, data: { invoiceId: invoice.id, status: "processing", userId, - initiatedAt: new Date(), + initiatedAt, mode: "internal", }, }); } // Mark external payouts as processing if (externalPayouts.length > 0) { await prisma.payout.updateMany({ where: { id: { in: externalPayouts.map((p) => p.id), }, }, data: { invoiceId: invoice.id, status: "processing", userId, - initiatedAt: new Date(), + initiatedAt, mode: "external", }, }); } // ... later in audit log ... await recordAuditLog( payouts.map((payout) => ({ workspaceId: workspace.id, programId: program.id, action: "payout.confirmed", description: `Payout ${payout.id} confirmed`, actor: userWhoInitiatedPayout ?? { id: userId, name: "Unknown user", }, targets: [ { type: "payout", id: payout.id, metadata: { ...payout, invoiceId: invoice.id, status: "processing", userId, - initiatedAt: new Date(), + initiatedAt, mode: externalPayoutsMap.has(payout.id) ? "external" : "internal", }, }, ], })), );This ensures all payouts and audit logs share the exact same initiation timestamp.
Based on learnings
๐ Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
๐ Files selected for processing (3)
apps/web/app/(ee)/api/cron/payouts/process/process-payouts.ts(3 hunks)apps/web/scripts/migrations/backfill-payout-initiated-at.ts(1 hunks)packages/prisma/schema/payout.prisma(1 hunks)
๐งฐ Additional context used
๐ง Learnings (1)
๐ Learning: 2025-09-17T17:44:03.965Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2857
File: apps/web/lib/actions/partners/update-program.ts:96-0
Timestamp: 2025-09-17T17:44:03.965Z
Learning: In apps/web/lib/actions/partners/update-program.ts, the team prefers to keep the messagingEnabledAt update logic simple by allowing client-provided timestamps rather than implementing server-controlled timestamp logic to avoid added complexity.
Applied to files:
apps/web/app/(ee)/api/cron/payouts/process/process-payouts.ts
๐งฌ Code graph analysis (1)
apps/web/scripts/migrations/backfill-payout-initiated-at.ts (1)
packages/prisma/index.ts (1)
prisma(3-9)
โฐ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
๐ Additional comments (4)
apps/web/scripts/migrations/backfill-payout-initiated-at.ts (1)
14-15: Verify batch processing strategy for complete migration.The script limits processing to 10 invoices per run. If there are more than 10 invoices with payouts needing backfill, this script will need to be run multiple times manually.
Consider one of these approaches:
- If manual batch runs are intended, document this in a comment
- Add a loop to process all invoices in batches:
async function main() { + let processedCount = 0; + let hasMore = true; + + while (hasMore) { const invoices = await prisma.invoice.findMany({ where: { type: "partnerPayout", payouts: { some: { initiatedAt: null, }, }, }, take: 10, }); + + if (invoices.length === 0) { + hasMore = false; + break; + } for (const invoice of invoices) { const res = await prisma.payout.updateMany({ where: { invoiceId: invoice.id, }, data: { initiatedAt: invoice.createdAt, }, }); console.log(`Updated ${res.count} payouts for invoice ${invoice.id}`); + processedCount += res.count; } + } + + console.log(`Migration complete. Total payouts updated: ${processedCount}`); }packages/prisma/schema/payout.prisma (1)
35-37: LGTM! Schema changes are well-structured.The addition of
initiatedAtand reintroduction ofuserIdare properly implemented:
- Both fields are nullable, maintaining backward compatibility with existing records
- The
userIdfield has a proper relation to the User model- An index on
userId(line 50) ensures efficient queriesapps/web/app/(ee)/api/cron/payouts/process/process-payouts.ts (2)
276-289: LGTM! Correctly sets initiation timestamp for internal payouts.The
initiatedAtfield is appropriately set when marking payouts as processing. The update includes all necessary fields:invoiceId,status,userId,initiatedAt, andmode.
294-308: LGTM! Correctly sets initiation timestamp for external payouts.The
initiatedAtfield is appropriately set for external payouts, mirroring the internal payout logic.Note: The
new Date()call at line 304 is separate from line 286, so internal and external payouts may have slightly different timestamps (milliseconds apart). This is acceptable since they are processed in separate database operations.
|
@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/scripts/migrations/backfill-payout-initiated-at.ts (1)
14-14: Consider removing thetake: 10limit or adding batch processing logic.The script only processes 10 invoices per execution, requiring multiple manual runs to backfill all records. For a migration script, consider either removing the limit to process all records in one run, or implementing a loop with pagination to automatically process all invoices in batches.
Example with pagination:
async function main() { - const invoices = await prisma.invoice.findMany({ - where: { - type: "partnerPayout", - payouts: { - some: { - initiatedAt: null, - }, - }, - }, - take: 10, - }); - - for (const invoice of invoices) { - const res = await prisma.payout.updateMany({ - where: { - invoiceId: invoice.id, - }, - data: { - initiatedAt: invoice.createdAt, - }, - }); - console.log(`Updated ${res.count} payouts for invoice ${invoice.id}`); + let totalUpdated = 0; + let hasMore = true; + + while (hasMore) { + const invoices = await prisma.invoice.findMany({ + where: { + type: "partnerPayout", + payouts: { + some: { + initiatedAt: null, + }, + }, + }, + take: 10, + }); + + if (invoices.length === 0) { + hasMore = false; + break; + } + + for (const invoice of invoices) { + const res = await prisma.payout.updateMany({ + where: { + invoiceId: invoice.id, + }, + data: { + initiatedAt: invoice.createdAt, + }, + }); + totalUpdated += res.count; + console.log(`Updated ${res.count} payouts for invoice ${invoice.id}`); + } } + + console.log(`Migration complete. Total payouts updated: ${totalUpdated}`); }apps/web/app/(ee)/api/cron/payouts/process/process-payouts.ts (1)
286-286: Consider using a single timestamp variable for consistency.The
initiatedAtfield is set usingnew Date()at three different points (lines 286, 304, 357), which could result in slightly different timestamps. For better consistency and accuracy, consider capturing the timestamp once at the beginning of the operation.Example:
+ const initiatedAt = new Date(); + // Mark internal payouts as processing if (internalPayouts.length > 0) { await prisma.payout.updateMany({ where: { id: { in: internalPayouts.map((p) => p.id), }, }, data: { invoiceId: invoice.id, status: "processing", userId, - initiatedAt: new Date(), + initiatedAt, mode: "internal", }, }); } // Mark external payouts as processing if (externalPayouts.length > 0) { await prisma.payout.updateMany({ where: { id: { in: externalPayouts.map((p) => p.id), }, }, data: { invoiceId: invoice.id, status: "processing", userId, - initiatedAt: new Date(), + initiatedAt, mode: "external", }, }); } // ... later in audit log await recordAuditLog( payouts.map((payout) => ({ // ... targets: [ { type: "payout", id: payout.id, metadata: { ...payout, invoiceId: invoice.id, status: "processing", userId, - initiatedAt: new Date(), + initiatedAt, mode: externalPayoutsMap.has(payout.id) ? "external" : "internal", }, }, ], })), );Also applies to: 304-304, 357-357
packages/prisma/schema/payout.prisma (1)
37-37: Consider adding an index if filtering byinitiatedAtis planned.While not currently needed based on the provided code, if you plan to add queries that filter or sort by
initiatedAtin the future, consider adding an index.Example:
@@index(programId) @@index(partnerId) @@index(invoiceId) @@index(status) @@index(userId) + @@index(initiatedAt)
๐ Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
๐ Files selected for processing (3)
apps/web/app/(ee)/api/cron/payouts/process/process-payouts.ts(3 hunks)apps/web/scripts/migrations/backfill-payout-initiated-at.ts(1 hunks)packages/prisma/schema/payout.prisma(1 hunks)
๐ Additional comments (2)
apps/web/app/(ee)/api/cron/payouts/process/process-payouts.ts (1)
358-358: LGTM: Mode determination is correct.The logic correctly determines the payout mode by checking if the payout ID exists in the
externalPayoutsMap, which was built from theexternalPayoutsarray on line 336.packages/prisma/schema/payout.prisma (1)
35-37: LGTM: Schema changes are correct and backward compatible.The addition of the optional
initiatedAtfield is properly implemented as nullable, ensuring backward compatibility with existing records. The repositioning of theuserIdfield is a cosmetic change with no functional impact.
|
@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
Caution
Some comments are outside the diff and canโt be posted inline due to platform limitations.
โ ๏ธ Outside diff range comments (2)
apps/web/lib/partners/create-stripe-transfer.ts (1)
57-86: RemovepaidAtfrom "processed" payouts that haven't been paid yet.Setting
paidAtat line 71 for payouts with status "processed" is semantically incorrect. These payouts have not been paid out yetโthey're merely marked as processed because they're below the minimum withdrawal threshold. They will be accumulated with future payouts and only actually paid when the combined total exceedsMIN_WITHDRAWAL_AMOUNT_CENTS.Only payouts with status "sent" (line 141) should receive a
paidAttimestamp, as those represent actual fund transfers.Apply this diff to remove the incorrect
paidAtassignment:data: { status: "processed", - paidAt: new Date(), },apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-table.tsx (1)
144-223: Handle payouts with paidAt but missing initiatedAt, and align Paid column semanticsRight now the "Paid" cell returns
"-"wheneverinitiatedAtis null, even ifpaidAtis set, and the column is still sorted bypaidAtwhile visually showinginitiatedAt. That can hide useful data for legacy payouts and make the sort order feel inconsistent with the displayed date.You can make the cell more robust by:
- Treating the presence of either
initiatedAtorpaidAtas enough to render the tooltip.- Only rendering the "Payment initiated at" row when
initiatedAtexists.- Using
initiatedAt ?? paidAtfor the short date in the trigger.For example:
- { - header: "Paid", - cell: ({ row }) => { - const ProcessingIcon = PayoutStatusBadges.processing.icon; - const CompletedIcon = PayoutStatusBadges.completed.icon; - - return row.original.initiatedAt ? ( + { + header: "Paid", + cell: ({ row }) => { + const ProcessingIcon = PayoutStatusBadges.processing.icon; + const CompletedIcon = PayoutStatusBadges.completed.icon; + + const { initiatedAt, paidAt, user } = row.original; + const hasTimestamp = initiatedAt || paidAt; + + if (!hasTimestamp) { + return "-"; + } + + return ( <Tooltip content={ <div className="flex flex-col gap-1 p-2.5"> - {row.original.user && ( + {user && ( <div className="flex flex-col gap-2"> <img src={ - row.original.user.image || - `${OG_AVATAR_URL}${row.original.user.name}` + user.image || `${OG_AVATAR_URL}${user.name}` } - alt={row.original.user.name ?? row.original.user.id} + alt={user.name ?? user.id} className="size-6 shrink-0 rounded-full" /> <p className="text-sm font-medium"> - {row.original.user.name} + {user.name} </p> </div> )} - <div className="flex items-center gap-1.5 text-xs text-neutral-500"> - <ProcessingIcon className="size-3 shrink-0 text-blue-600" /> - <span> - Payment initiated at{" "} - <span className="font-medium text-neutral-700"> - {formatDateTime(row.original.initiatedAt, { - month: "short", - day: "numeric", - year: "numeric", - hour: "numeric", - minute: "numeric", - })} - </span> - </span> - </div> - {row.original.paidAt && ( - <div className="flex items-center gap-1.5 text-xs text-neutral-500"> - <CompletedIcon className="size-3 shrink-0 text-green-600" /> - <span> - Payment completed at{" "} - <span className="font-medium text-neutral-700"> - {formatDateTime(row.original.paidAt, { - month: "short", - day: "numeric", - year: "numeric", - hour: "numeric", - minute: "numeric", - })} - </span> - </span> - </div> - )} + {initiatedAt && ( + <div className="flex items-center gap-1.5 text-xs text-neutral-500"> + <ProcessingIcon className="size-3 shrink-0 text-blue-600" /> + <span> + Payment initiated at{" "} + <span className="font-medium text-neutral-700"> + {formatDateTime(initiatedAt, { + month: "short", + day: "numeric", + year: "numeric", + hour: "numeric", + minute: "numeric", + })} + </span> + </span> + </div> + )} + {paidAt && ( + <div className="flex items-center gap-1.5 text-xs text-neutral-500"> + <CompletedIcon className="size-3 shrink-0 text-green-600" /> + <span> + Payment completed at{" "} + <span className="font-medium text-neutral-700"> + {formatDateTime(paidAt, { + month: "short", + day: "numeric", + year: "numeric", + hour: "numeric", + minute: "numeric", + })} + </span> + </span> + </div> + )} </div> } > <div className="flex items-center gap-2"> - {row.original.user && ( + {user && ( <img src={ - row.original.user.image || - `${OG_AVATAR_URL}${row.original.user.name}` + user.image || `${OG_AVATAR_URL}${user.name}` } - alt={row.original.user.name ?? row.original.user.id} + alt={user.name ?? user.id} className="size-5 shrink-0 rounded-full" /> )} - {formatDate(row.original.initiatedAt, { + {formatDate(initiatedAt ?? paidAt, { month: "short", year: undefined, })} </div> </Tooltip> - ) : ( - "-" - ); - }, - }, + ); + }, + },You may also want to revisit whether this "Paid" column should sort by
initiatedAtinstead ofpaidAt, to better match the primary date being displayed.
๐งน Nitpick comments (1)
apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-table.tsx (1)
15-16: Initiated/Paid columns for partner payouts are well-structuredThe new "Initiated" column and the updated "Paid" column use
TimestampTooltip+formatDateSmartconsistently, with sensible header tooltips and"-"fallbacks when timestamps are absent. If you later want to sort by initiation time, you could consider addinginitiatedAtto the sortable columns and backing API schema, but the current behavior is coherent as-is.Also applies to: 25-27, 97-118, 120-139
๐ Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
๐ Files selected for processing (7)
apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/partner-payout-details-sheet.tsx(4 hunks)apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-table.tsx(3 hunks)apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/payouts-card.tsx(2 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-details-sheet.tsx(7 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-table.tsx(3 hunks)apps/web/lib/partners/create-stripe-transfer.ts(1 hunks)apps/web/lib/zod/schemas/payouts.ts(1 hunks)
๐งฐ Additional context used
๐ง Learnings (4)
๐ Learning: 2025-09-17T17:44:03.965Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2857
File: apps/web/lib/actions/partners/update-program.ts:96-0
Timestamp: 2025-09-17T17:44:03.965Z
Learning: In apps/web/lib/actions/partners/update-program.ts, the team prefers to keep the messagingEnabledAt update logic simple by allowing client-provided timestamps rather than implementing server-controlled timestamp logic to avoid added complexity.
Applied to files:
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/payouts-card.tsxapps/web/lib/partners/create-stripe-transfer.tsapps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/partner-payout-details-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/lib/partners/create-stripe-transfer.ts
๐ Learning: 2025-10-15T01:05:43.266Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 2958
File: apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx:432-457
Timestamp: 2025-10-15T01:05:43.266Z
Learning: In apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx, defer refactoring the custom MenuItem component (lines 432-457) to use the shared dub/ui MenuItem component to a future PR, as requested by steven-tey.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-details-sheet.tsxapps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-table.tsx
๐ Learning: 2025-05-29T04:45:18.504Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2448
File: packages/email/src/templates/partner-program-summary.tsx:0-0
Timestamp: 2025-05-29T04:45:18.504Z
Learning: In the PartnerProgramSummary email template (packages/email/src/templates/partner-program-summary.tsx), the stat titles are hardcoded constants ("Clicks", "Leads", "Sales", "Earnings") that will always match the ICONS object keys after toLowerCase() conversion, so icon lookup failures are not possible.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-table.tsx
๐งฌ Code graph analysis (5)
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/payouts-card.tsx (1)
packages/ui/src/timestamp-tooltip.tsx (1)
TimestampTooltip(28-50)
apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/partner-payout-details-sheet.tsx (2)
packages/ui/src/timestamp-tooltip.tsx (1)
TimestampTooltip(28-50)packages/ui/src/tooltip.tsx (1)
Tooltip(64-115)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-details-sheet.tsx (5)
packages/ui/src/timestamp-tooltip.tsx (1)
TimestampTooltip(28-50)packages/ui/src/tooltip.tsx (1)
Tooltip(64-115)packages/prisma/client.ts (1)
PayoutStatus(27-27)packages/ui/src/icons/nucleo/circle-arrow-right.tsx (1)
CircleArrowRight(3-43)apps/web/lib/constants/payouts.ts (1)
INVOICE_AVAILABLE_PAYOUT_STATUSES(98-102)
apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-table.tsx (1)
packages/ui/src/timestamp-tooltip.tsx (1)
TimestampTooltip(28-50)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-table.tsx (1)
packages/ui/src/tooltip.tsx (1)
Tooltip(64-115)
โฐ 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 (7)
apps/web/lib/partners/create-stripe-transfer.ts (1)
132-156: No issues found. The current implementation is correct.The review comment questions whether
initiatedAtshould be set when payouts transition to "sent" status. However, verification shows this concern is unfounded:
initiatedAtis intentionally set only once when payouts enter the "processing" status (inprocess-payouts.ts)paidAtis correctly set when payouts transition to "sent" status (actual payment time)- These fields serve distinct semantic purposes:
initiatedAtmarks process initiation, whilepaidAtmarks actual payment completion- The schema defines both as nullable
DateTime?fields, allowing for historical data without these timestamps- This design pattern is consistent across all payout transitions in the codebase
No changes are needed.
apps/web/lib/zod/schemas/payouts.ts (1)
53-67: initiatedAt added consistently to payout Zod schemaThe new
initiatedAt: z.date().nullable()field is consistent with the other nullable timestamps (periodStart,periodEnd,paidAt) and with the intended optional nature of the initiation time.apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/payouts-card.tsx (1)
8-10: Partner payouts card initiated/paid tooltip UI looks solidThe conditional rendering for
paidAt/initiatedAtwithTimestampTooltipandformatDateSmartis clear, matches other payout UIs, and keeps the oldformatPeriod(payout)fallback for cases with no timestamps.Also applies to: 70-98
apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/partner-payout-details-sheet.tsx (2)
21-32: Initiated/Paid values in partner payout details are wired correctlyUsing
TimestampTooltipplusformatDateTimeSmartforpayout.initiatedAt/payout.paidAt, with a simple"-"fallback, matches patterns elsewhere and safely handles missing timestamps.Also applies to: 87-113
245-262: Helpful tooltips on Initiated/Paid labelsConditionally wrapping the "Initiated" and "Paid" labels in
Tooltipwith clear descriptions improves clarity without impacting other fields that still render as plain labels.apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-details-sheet.tsx (2)
1-4: Program payout invoice data (Initiated/Paid/Amount) is wired cleanlyThe expanded
invoiceDatawithInitiated/PaidusingTimestampTooltip + formatDateTimeSmart, and the richerAmountcell (external payout tooltip, internal invoice icon with APP_DOMAIN URL, and fallback frominvoiceIdtopayout.id) all look consistent and resilient to missing fields.Also applies to: 13-27, 29-38, 67-169
270-285: Sticky header/footer for payout details sheet improve UXThe sticky header with a status-based title and the sticky footer that switches between "Confirm payout" (pending) and "View all" behaviors are wired correctly through
queryParamsand make the sheet easier to use without introducing routing edge cases.Also applies to: 320-340
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
Caution
Some comments are outside the diff and canโt be posted inline due to platform limitations.
โ ๏ธ Outside diff range comments (3)
apps/web/lib/partners/create-stripe-transfer.ts (1)
63-73: RemovepaidAt: new Date()from line 71 or reconsider the field being set.The semantic error is confirmed. When payouts are marked "processed" on line 71, they are queued below the minimum withdrawal thresholdโthey have not been paid or sent. Setting
paidAtat this point contradicts the established pattern throughout the codebase wherepaidAtis only set when payouts transition to "sent" (after successful Stripe transfer) or "completed" (after webhook confirmation). TheinitiatedAtfield is properly set when payouts first become "processing" status (inprocess-payouts.ts), so no timestamp is needed when they transition to "processed" status.apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-table.tsx (2)
233-233: Sorting column mismatch with displayed data.The "Paid" column displays
initiatedAt(line 214) but is sortable bypaidAt(line 233). This creates a UX inconsistency where users see initiated timestamps but sorting is based on paid timestamps.Consider one of these approaches:
- Sort by
initiatedAtto match the displayed data:- sortableColumns: ["periodEnd", "amount", "paidAt"], + sortableColumns: ["periodEnd", "amount", "initiatedAt"],
- Or add both as sortable columns if users need both options:
- sortableColumns: ["periodEnd", "amount", "paidAt"], + sortableColumns: ["periodEnd", "amount", "initiatedAt", "paidAt"],
149-222: AddinitiatedAtto all payout creation paths.Two production payout creation routes are missing
initiatedAt, causing new payouts to display "-" instead of payment status:
apps/web/app/(ee)/api/cron/payouts/process/split-payouts.ts:87โ addinitiatedAt: new Date()to the payout data objectapps/web/app/(ee)/api/cron/payouts/aggregate-due-commissions/route.ts:175โ addinitiatedAt: new Date()to the payout data objectThe backfill migration handles existing payouts, but these routes must set
initiatedAtfor all future payouts to prevent breaking the UI.
โป๏ธ Duplicate comments (1)
apps/web/scripts/migrations/backfill-payout-initiated-at.ts (1)
1-30: Add error handling and ensure Prisma disconnects after the migrationThe script currently calls
main()directly without handling failures or closing the Prisma connection. That can hide migration errors and leave connections open in some environments.Consider wrapping the entrypoint with error handling and disconnect logic:
async function main() { const invoices = await prisma.invoice.findMany({ @@ for (const invoice of invoices) { @@ console.log(`Updated ${res.count} payouts for invoice ${invoice.id}`); } } -main(); +main() + .catch((error) => { + console.error("Migration failed:", error); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + });
๐งน Nitpick comments (2)
apps/web/app/(ee)/api/cron/payouts/process/process-payouts.ts (1)
274-307: Reuse a single initiatedAt timestamp for DB updates and audit logYouโre calling
new Date()three times (internal update, external update, audit metadata). While functionally fine, capturing it once improves consistency and makes it clear that all records/logs share the same initiation instant.For example:
- // Mark internal payouts as processing - if (internalPayouts.length > 0) { - await prisma.payout.updateMany({ + const initiatedAt = new Date(); + + // Mark internal payouts as processing + if (internalPayouts.length > 0) { + await prisma.payout.updateMany({ @@ - userId, - initiatedAt: new Date(), + userId, + initiatedAt, @@ - // Mark external payouts as processing + // Mark external payouts as processing if (externalPayouts.length > 0) { @@ - userId, - initiatedAt: new Date(), + userId, + initiatedAt, @@ - status: "processing", - userId, - initiatedAt: new Date(), + status: "processing", + userId, + initiatedAt, mode: externalPayoutsMap.has(payout.id) ? "external" : "internal",Also applies to: 336-359
packages/prisma/schema/payout.prisma (1)
35-37: Consider adding an index for query performance.The
initiatedAtfield may benefit from a database index if it will be used for sorting or filtering payouts, similar to how other temporal fields are used in the UI.Apply this diff to add an index:
@@index(programId) @@index(partnerId) @@index(invoiceId) @@index(status) @@index(userId) + @@index(initiatedAt) }
๐ Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
๐ Files selected for processing (10)
apps/web/app/(ee)/api/cron/payouts/process/process-payouts.ts(3 hunks)apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/partner-payout-details-sheet.tsx(4 hunks)apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-table.tsx(3 hunks)apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/payouts-card.tsx(2 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-details-sheet.tsx(7 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-table.tsx(3 hunks)apps/web/lib/partners/create-stripe-transfer.ts(1 hunks)apps/web/lib/zod/schemas/payouts.ts(1 hunks)apps/web/scripts/migrations/backfill-payout-initiated-at.ts(1 hunks)packages/prisma/schema/payout.prisma(1 hunks)
๐งฐ Additional context used
๐ง Learnings (4)
๐ Learning: 2025-09-17T17:44:03.965Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2857
File: apps/web/lib/actions/partners/update-program.ts:96-0
Timestamp: 2025-09-17T17:44:03.965Z
Learning: In apps/web/lib/actions/partners/update-program.ts, the team prefers to keep the messagingEnabledAt update logic simple by allowing client-provided timestamps rather than implementing server-controlled timestamp logic to avoid added complexity.
Applied to files:
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/payouts-card.tsxapps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-table.tsxapps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/partner-payout-details-sheet.tsx
๐ Learning: 2025-10-15T01:05:43.266Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 2958
File: apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx:432-457
Timestamp: 2025-10-15T01:05:43.266Z
Learning: In apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx, defer refactoring the custom MenuItem component (lines 432-457) to use the shared dub/ui MenuItem component to a future PR, as requested by steven-tey.
Applied to files:
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/payouts-card.tsxapps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-details-sheet.tsxapps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-table.tsx
๐ Learning: 2025-05-29T04:45:18.504Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2448
File: packages/email/src/templates/partner-program-summary.tsx:0-0
Timestamp: 2025-05-29T04:45:18.504Z
Learning: In the PartnerProgramSummary email template (packages/email/src/templates/partner-program-summary.tsx), the stat titles are hardcoded constants ("Clicks", "Leads", "Sales", "Earnings") that will always match the ICONS object keys after toLowerCase() conversion, so icon lookup failures are not possible.
Applied to files:
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-table.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/lib/partners/create-stripe-transfer.ts
๐ Additional comments (6)
apps/web/lib/zod/schemas/payouts.ts (1)
53-67: initiatedAt field wiring in zod schema looks correctThe new
initiatedAt: z.date().nullable()slotting betweencreatedAtandpaidAtis consistent with the data model and keeps all derived response schemas in sync.apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/payout-table.tsx (1)
15-27: Nice initiated/paid timestamp UX; implementation is solidThe Initiated column and the updated Paid column both correctly guard on nullable timestamps, use
TimestampTooltip+formatDateSmartconsistently, and degrade cleanly to"-"when absent. Header tooltips read well and match the semantics.Also applies to: 97-139
apps/web/app/(ee)/partners.dub.co/(dashboard)/programs/[programSlug]/(enrolled)/payouts-card.tsx (1)
8-9: Clear โPaid/Initiatedโ labeling with sensible fallbacksThe conditional rendering (Paid โ Initiated โ period) is intuitive, uses
TimestampTooltipandformatDateSmartconsistently, and preserves the oldformatPeriodfallback when no timestamps are available.Also applies to: 70-98
apps/web/app/(ee)/partners.dub.co/(dashboard)/payouts/partner-payout-details-sheet.tsx (1)
21-35: Invoice Initiated/Paid fields and label tooltips are well-integratedThe new
Initiated/Paidentries correctly handle nullable timestamps, useTimestampTooltip+formatDateTimeSmartfor detail, and the per-label tooltips in the invoice details grid add useful context without changing existing layout.Also applies to: 87-113, 248-262
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-details-sheet.tsx (2)
95-121: Excellent UX improvement with separate Initiated and Paid fields.The distinct "Initiated" and "Paid" fields with
TimestampTooltipprovide clear visibility into the payout lifecycle. The hover tooltips showing both local and UTC times are helpful for users in different time zones.
170-170: Correct dependency fix.Good catch including
slugin the dependencies. It's used in theConditionalLinkhref on line 73, so this prevents potential stale closure issues.
Summary by CodeRabbit