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

Skip to content

Conversation

@steven-tey
Copy link
Collaborator

@steven-tey steven-tey commented Oct 9, 2025

Summary by CodeRabbit

  • New Features

    • Confirm a single payout directly from the dashboard via the updated confirmation sheet.
    • Filter eligible payouts by a specific payout when selected.
    • Exclude individual payouts during confirmation; exclusions persist while reviewing totals.
  • Style

    • Updated button label to “Confirm payout” for clarity.
    • Replaced the old payout invoice sheet with a streamlined confirmation sheet in stats and details views.

@vercel
Copy link
Contributor

vercel bot commented Oct 9, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Updated (UTC)
dub Ready Ready Preview Oct 9, 2025 11:55pm

@steven-tey
Copy link
Collaborator Author

@coderabbitai full review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 9, 2025

Walkthrough

Introduces optional selectedPayoutId across payout eligibility, confirmation UI, action schema, and cron processing. Adjusts query schemas, routing params, and Prisma where clauses to conditionally filter by a single payout or exclude specified payouts. Updates UI components to ConfirmPayoutsSheet and synchronizes state via router query parameters.

Changes

Cohort / File(s) Summary
Cron processing pipeline
apps/web/app/(ee)/api/cron/payouts/process/process-payouts.ts, apps/web/app/(ee)/api/cron/payouts/process/split-payouts.ts, apps/web/app/(ee)/api/cron/payouts/process/route.ts
Add optional selectedPayoutId parameter and schema field; conditionally filter payouts by id or by notIn excludedPayoutIds (only when provided); propagate selectedPayoutId through route to splitPayouts and processPayouts.
Eligible payouts API
apps/web/app/(ee)/api/programs/[programId]/payouts/eligible/route.ts
Rename schema to eligiblePayoutsQuerySchema; add optional selectedPayoutId; if present, filter query by id.
Dashboard UI — sheets and views
apps/web/ui/partners/confirm-payouts-sheet.tsx, apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-details-sheet.tsx, apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-stats.tsx, apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-table.tsx
Rename PayoutInvoiceSheet → ConfirmPayoutsSheet; derive selectedPayoutId/excludedPayoutIds from router; update toggling to modify query params; include selectedPayoutId in API calls; adjust button label and query param handling; expand table exclude list to include selectedPayoutId and excludedPayoutIds.
Actions
apps/web/lib/actions/partners/confirm-payouts.ts
Extend confirmPayoutsSchema with optional selectedPayoutId; include it in parsed input and QStash payload.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant U as User
    participant UI as Dashboard UI (ConfirmPayoutsSheet)
    participant A as Action: confirmPayouts
    participant Q as Queue (QStash)
    participant R as API Route /api/cron/payouts/process
    participant S as splitPayouts
    participant P as processPayouts
    participant DB as Database

    U->>UI: Open confirm payouts (optionally selects payout)
    UI->>A: submit { selectedPayoutId?, excludedPayoutIds? }
    A->>Q: enqueue payload { selectedPayoutId?, excludedPayoutIds? }
    Q->>R: POST payload
    R->>S: splitPayouts({ selectedPayoutId?, excludedPayoutIds? })
    alt selectedPayoutId provided
        S->>DB: findMany({ id: selectedPayoutId })
    else excludedPayoutIds provided
        S->>DB: findMany({ id: { notIn: excludedPayoutIds } })
    else
        S->>DB: findMany({})
    end
    R->>P: processPayouts({ selectedPayoutId?, excludedPayoutIds? })
    alt selectedPayoutId provided
        P->>DB: update/process where { id: selectedPayoutId }
    else excludedPayoutIds provided
        P->>DB: update/process where { id: { notIn: excludedPayoutIds } }
    else
        P->>DB: update/process where {}
    end
    P-->>R: result
    R-->>Q: ack
    Q-->>A: delivery complete
    A-->>UI: success status
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • TWilson023

Poem

A rabbit taps the payout key,
One hop selects, or many flee—
IDs in tow, exclusions tight,
The queue sets off into the night.
Sheets renamed, the routes align,
Confirm!—and all the ledgers shine. 🐇💸

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title “Add ability to confirm selected payout” directly reflects the primary change of introducing a selectedPayoutId parameter and updating both backend and frontend logic to confirm a specific payout rather than all pending payouts, making it clear and concise.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch confirm-selected-payout

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 9, 2025

✅ Actions performed

Full review triggered.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
apps/web/ui/partners/confirm-payouts-sheet.tsx (2)

320-347: Consider disabling the cutoff period selector when a specific payout is selected.

When selectedPayoutId is present, the cutoff period selector (lines 321-347) remains enabled, but the selected payout likely has a fixed cutoff period. This could confuse users who might expect changing the cutoff period to affect the selected payout.

If the cutoff period should not apply when a specific payout is selected, consider applying this diff:

  {
    key: "Cutoff Period",
    value: (
      <div className="w-full">
        <Combobox
          options={cutoffPeriodOptions}
          selected={selectedCutoffPeriodOption}
          setSelected={(option: ComboboxOption) => {
            if (!option) {
              return;
            }

            setCutoffPeriod(option.value as CUTOFF_PERIOD_TYPES);
          }}
          placeholder="Select cutoff period"
          buttonProps={{
            className:
              "h-auto border border-neutral-200 px-3 py-1.5 text-xs focus:border-neutral-600 focus:ring-neutral-600",
+           disabled: !!selectedPayoutId,
          }}
          matchTriggerWidth
          hideSearch
          caret
        />
      </div>
    ),
    tooltipContent:
      "Cutoff period in UTC. If set, only commissions accrued up to the cutoff period will be included in the payout invoice.",
  },

441-471: Hide exclude/include buttons when a payout is selected
The exclusion controls never take effect when selectedPayoutId is set (finalEligiblePayouts bypasses exclusions), so wrap the button container in a !selectedPayoutId check to prevent confusing UX.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2152145 and 6df433c.

📒 Files selected for processing (9)
  • apps/web/app/(ee)/api/cron/payouts/process/process-payouts.ts (3 hunks)
  • apps/web/app/(ee)/api/cron/payouts/process/route.ts (4 hunks)
  • apps/web/app/(ee)/api/cron/payouts/process/split-payouts.ts (1 hunks)
  • apps/web/app/(ee)/api/programs/[programId]/payouts/eligible/route.ts (3 hunks)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-details-sheet.tsx (1 hunks)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-stats.tsx (2 hunks)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-table.tsx (1 hunks)
  • apps/web/lib/actions/partners/confirm-payouts.ts (3 hunks)
  • apps/web/ui/partners/confirm-payouts-sheet.tsx (9 hunks)
⏰ 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 (15)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-details-sheet.tsx (1)

254-259: LGTM!

The button text change accurately reflects the single payout confirmation behavior, and the addition of selectedPayoutId to the query parameters properly integrates with the new confirmation flow.

apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-stats.tsx (1)

6-6: LGTM!

The component rename from PayoutInvoiceSheet to ConfirmPayoutsSheet aligns with the PR's refactoring of the payout confirmation workflow.

Also applies to: 71-71

apps/web/app/(ee)/api/cron/payouts/process/route.ts (1)

19-19: LGTM!

The addition of selectedPayoutId to the schema and its propagation through both splitPayouts and processPayouts is correctly implemented and consistent with the PR objectives.

Also applies to: 38-38, 58-59, 70-70

apps/web/lib/actions/partners/confirm-payouts.ts (1)

21-21: LGTM!

The addition of selectedPayoutId to the schema and its inclusion in the Qstash payload properly integrates with the payout processing workflow.

Also applies to: 37-37, 138-138

apps/web/app/(ee)/api/programs/[programId]/payouts/eligible/route.ts (2)

13-16: LGTM!

The new eligiblePayoutsQuerySchema properly defines the expected query parameters for the eligible payouts endpoint, including the optional selectedPayoutId.


32-33: LGTM!

The schema usage and conditional filtering by selectedPayoutId are correctly implemented, enabling the endpoint to fetch a specific payout when needed.

Also applies to: 46-46

apps/web/app/(ee)/api/cron/payouts/process/process-payouts.ts (1)

36-36: LGTM!

The conditional filtering logic is well-structured and correctly handles the three cases:

  1. When selectedPayoutId is provided, filter by that specific ID
  2. When excludedPayoutIds is provided, exclude those IDs
  3. Otherwise, apply no ID-based filter

This approach enables both single-payout processing and batch processing with exclusions.

Also applies to: 54-54, 63-67

apps/web/app/(ee)/api/cron/payouts/process/split-payouts.ts (1)

13-13: LGTM!

The conditional filtering logic mirrors the implementation in process-payouts.ts, ensuring consistency across the payout processing flow. The three-case handling is appropriate for this context as well.

Also applies to: 18-18, 23-27

apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-table.tsx (1)

71-71: Verify all required query parameters are included
Manual check needed: confirm pagination (page, pageSize), filtering (search, invoiceId), etc., are forwarded alongside partnerId, status, sortBy, and sortOrder in the getQueryString({ include: […] }) call.

apps/web/ui/partners/confirm-payouts-sheet.tsx (6)

61-61: LGTM! Component renamed for clarity.

The rename from PayoutInvoiceSheetContent to ConfirmPayoutsSheetContent better reflects the component's purpose.


224-251: LGTM! Correctly uses filtered payouts.

The amount calculation now uses finalEligiblePayouts instead of eligiblePayouts, ensuring that excluded payouts are not included in the total. The dependency array at line 251 is properly updated.


678-706: LGTM! Proper export rename and URL cleanup.

The component export is renamed to ConfirmPayoutsSheet, matching the internal component rename. The cleanup logic at line 699 correctly removes all related query parameters (confirmPayouts, selectedPayoutId, excludedPayoutIds) when the sheet closes.


101-111: Handle empty string edge case for excludedPayoutIds.

When searchParamsObj.excludedPayoutIds is empty, split(",") yields [""]. Filter out falsy values:

- const excludedPayoutIds = searchParamsObj.excludedPayoutIds?.split(",") || [];
+ const excludedPayoutIds =
+   searchParamsObj.excludedPayoutIds?.split(",").filter(Boolean) || [];

Could not locate the eligible payouts API implementation; please confirm that when selectedPayoutId is provided, the API returns only that payout.


570-580: selectedPayoutId optionality is already handled by the schema
The confirmPayoutsSchema marks selectedPayoutId as .optional(), so passing undefined is acceptable.


84-96: Ensure API handles missing or undefined selectedPayoutId
Omit or default selectedPayoutId before building the query string, and verify the /payouts/eligible GET handler properly validates or filters out undefined parameters.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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)

88-99: Prevent undefined selectedPayoutId from becoming "undefined" string.

When selectedPayoutId is undefined, URLSearchParams converts it to the string "undefined", which is then sent to the backend as ?selectedPayoutId=undefined. This causes the backend to receive "undefined" as a string value instead of the parameter being absent.

Apply this diff to conditionally include the parameter only when it has a value:

  const {
    data: eligiblePayouts,
    error: eligiblePayoutsError,
    isLoading: eligiblePayoutsLoading,
  } = useSWR<PayoutResponse[]>(
-   `/api/programs/${defaultProgramId}/payouts/eligible?${new URLSearchParams({
+   `/api/programs/${defaultProgramId}/payouts/eligible?${new URLSearchParams({
      workspaceId,
      cutoffPeriod,
-     selectedPayoutId,
+     ...(selectedPayoutId && { selectedPayoutId }),
    } as Record<string, any>).toString()}`,
    fetcher,
  );
🧹 Nitpick comments (3)
apps/web/app/(ee)/api/programs/[programId]/payouts/eligible/route.ts (1)

44-73: Consider explicit error handling when selectedPayoutId doesn't match eligible criteria.

When selectedPayoutId is provided but the payout doesn't exist or doesn't meet the eligibility criteria (status, amount, payoutsEnabled), the endpoint returns an empty array. This could be confusing to callers who expect a specific payout.

Consider returning a 404 or a descriptive error message when:

  1. The selectedPayoutId doesn't exist, or
  2. The payout exists but doesn't meet eligibility criteria

This would provide clearer feedback to the UI layer.

Example implementation:

   let payouts = await prisma.payout.findMany({
     where: {
       ...(selectedPayoutId && { id: selectedPayoutId }),
       programId,
       status: "pending",
       amount: {
         gte: minPayoutAmount,
       },
       partner: {
         payoutsEnabledAt: {
           not: null,
         },
       },
     },
     // ... rest of the query
   });

+  // If a specific payout was requested but not found or ineligible, return error
+  if (selectedPayoutId && payouts.length === 0) {
+    return NextResponse.json(
+      { error: "Selected payout not found or not eligible for processing" },
+      { status: 404 }
+    );
+  }
+
   if (cutoffPeriodValue) {
     // ... existing cutoff logic
   }
apps/web/ui/partners/confirm-payouts-sheet.tsx (2)

101-111: Refine empty array/string handling in query parameters.

When excludedPayoutIds is an empty string, split(",") returns [""] (an array with one empty string) rather than an empty array. While this doesn't break functionality (since no payout ID will match an empty string), it's semantically incorrect and creates an unnecessary filter iteration.

Apply this diff to handle empty strings explicitly:

- const excludedPayoutIds = searchParamsObj.excludedPayoutIds?.split(",") || [];
+ const excludedPayoutIds = searchParamsObj.excludedPayoutIds?.split(",").filter(Boolean) || [];

This ensures that empty strings are filtered out, resulting in a clean empty array when no IDs are excluded.


446-471: Ensure consistent empty array handling when toggling exclusions.

When the last excluded payout is included, the resulting empty array is joined to an empty string ([].join(",")""), which then causes the split issue mentioned earlier. For consistency, consider handling empty arrays explicitly.

Apply this diff to set the parameter only when the array has elements:

                <Button
                  variant="secondary"
                  text={
                    excludedPayoutIds.includes(row.original.id)
                      ? "Include"
                      : "Exclude"
                  }
                  className="h-6 w-fit px-2"
                  onClick={() =>
-                   // Toggle excluded
-                   queryParams({
+                   {
+                     const nextExcluded = excludedPayoutIds.includes(row.original.id)
+                       ? excludedPayoutIds.filter((id) => id !== row.original.id)
+                       : [...excludedPayoutIds, row.original.id];
+                     
+                     queryParams({
                      set: {
-                       excludedPayoutIds: excludedPayoutIds.includes(
-                         row.original.id,
-                       )
-                         ? excludedPayoutIds.filter(
-                             (id) => id !== row.original.id,
-                           )
-                         : [...excludedPayoutIds, row.original.id],
+                       ...(nextExcluded.length > 0 && {
+                         excludedPayoutIds: nextExcluded.join(","),
+                       }),
                      },
+                     ...(nextExcluded.length === 0 && { del: ["excludedPayoutIds"] }),
                      replace: true,
-                   })
+                   });
+                   }
                  }
                />
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2152145 and 6df433c.

📒 Files selected for processing (9)
  • apps/web/app/(ee)/api/cron/payouts/process/process-payouts.ts (3 hunks)
  • apps/web/app/(ee)/api/cron/payouts/process/route.ts (4 hunks)
  • apps/web/app/(ee)/api/cron/payouts/process/split-payouts.ts (1 hunks)
  • apps/web/app/(ee)/api/programs/[programId]/payouts/eligible/route.ts (3 hunks)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-details-sheet.tsx (1 hunks)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-stats.tsx (2 hunks)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-table.tsx (1 hunks)
  • apps/web/lib/actions/partners/confirm-payouts.ts (3 hunks)
  • apps/web/ui/partners/confirm-payouts-sheet.tsx (9 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-stats.tsx (1)
apps/web/ui/partners/confirm-payouts-sheet.tsx (1)
  • ConfirmPayoutsSheet (678-706)
⏰ 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 (11)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-stats.tsx (1)

6-6: LGTM!

The component replacement from PayoutInvoiceSheet to ConfirmPayoutsSheet is straightforward and aligns with the broader refactoring to support selected payout confirmation.

Also applies to: 71-71

apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-details-sheet.tsx (1)

252-265: LGTM!

The button text change from "Confirm all pending payouts" to "Confirm payout" accurately reflects the new single-payout confirmation behavior. The addition of selectedPayoutId to the query parameters correctly enables the targeted payout flow.

apps/web/app/(ee)/api/cron/payouts/process/route.ts (1)

19-19: LGTM!

The selectedPayoutId parameter is properly validated through the schema, extracted from the request body, and correctly propagated to both splitPayouts and processPayouts functions. This enables the targeted payout processing flow.

Also applies to: 38-40, 58-59, 70-71

apps/web/app/(ee)/api/cron/payouts/process/process-payouts.ts (1)

61-104: LGTM! Filtering logic correctly implements selectedPayoutId precedence.

The conditional filtering logic is sound:

  1. When selectedPayoutId is provided, filter to that specific payout
  2. Otherwise, exclude excludedPayoutIds if provided
  3. Otherwise, no additional id-based filtering

This precedence behavior (selectedPayoutId overrides excludedPayoutIds) is consistent with split-payouts.ts and aligns with the single-payout confirmation flow introduced in this PR.

apps/web/app/(ee)/api/cron/payouts/process/split-payouts.ts (1)

21-46: LGTM! Filtering logic is consistent with process-payouts.ts.

The conditional filtering implementation matches the pattern in process-payouts.ts, ensuring consistent behavior across the payout processing pipeline. The parameter is properly threaded through the function signature and type definition.

apps/web/lib/actions/partners/confirm-payouts.ts (1)

21-22: Enforce mutual exclusivity in confirmPayoutsSchema
Add a .refine() to prevent providing both selectedPayoutId and excludedPayoutIds, and verify downstream logic correctly prioritises selectedPayoutId when both are set.

apps/web/ui/partners/confirm-payouts-sheet.tsx (5)

61-61: LGTM! Component rename is consistent.

The component rename from PayoutInvoiceSheet to ConfirmPayoutsSheet accurately reflects the new functionality and is consistently applied throughout the file.

Also applies to: 678-678, 703-703


224-251: LGTM! Calculations correctly use filtered payouts.

The amount calculation properly uses finalEligiblePayouts, which respects both selectedPayoutId filtering and excludedPayoutIds exclusions. The memoization dependencies are accurate.


697-701: LGTM! Proper cleanup of query parameters on close.

The sheet correctly clears all related query parameters (confirmPayouts, selectedPayoutId, excludedPayoutIds) when closing, maintaining clean URL state.


84-111: LGTM! URL-based state management improves UX.

The migration from local state (useState) to URL query parameters for excludedPayoutIds is a solid architectural choice. This enables shareable URLs, proper browser history integration, and state persistence across refreshes.


570-580: No changes needed: splitPayouts gives selectedPayoutId precedence over excludedPayoutIds
The backend’s splitPayouts uses the same ternary logic as the UI—when selectedPayoutId is provided, excludedPayoutIds is ignored.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
apps/web/ui/partners/confirm-payouts-sheet.tsx (2)

103-111: Clarify comment and consider UI implications.

The comment "return the payout directly" is misleading—it returns eligiblePayouts, which should contain only the selected payout based on the API call. Consider clarifying this comment.

Additionally, when selectedPayoutId is present, the exclude/include toggle buttons in the table remain visible but have no effect on finalEligiblePayouts. This could confuse users.

Consider one of the following:

  1. Update the comment to clarify the expected API behavior:
-  // if there's a selected payout id, return the payout directly
+  // if there's a selected payout id, return eligiblePayouts (API should return only the selected payout)
   if (selectedPayoutId) return eligiblePayouts;
  1. Optionally hide or disable the exclude/include buttons when selectedPayoutId is present to avoid user confusion.

349-353: Consider showing included partners count.

The "Partners" field displays eligiblePayouts?.length, which includes both included and excluded payouts. Since finalEligiblePayouts determines the actual payouts in the invoice, consider showing the count of included partners instead.

Update to use finalEligiblePayouts:

 {
   key: "Partners",
-  value: eligiblePayouts?.length ?? (
+  value: finalEligiblePayouts?.length ?? (
     <div className="h-4 w-24 animate-pulse rounded-md bg-neutral-200" />
   ),
 },

Alternatively, show both counts (e.g., "3/5 partners") if displaying excluded payouts in the UI is intentional.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6df433c and ce6c970.

📒 Files selected for processing (3)
  • apps/web/app/(ee)/api/cron/payouts/process/process-payouts.ts (3 hunks)
  • apps/web/app/(ee)/api/cron/payouts/process/split-payouts.ts (1 hunks)
  • apps/web/ui/partners/confirm-payouts-sheet.tsx (9 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/web/app/(ee)/api/cron/payouts/process/split-payouts.ts
  • apps/web/app/(ee)/api/cron/payouts/process/process-payouts.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (1)
apps/web/ui/partners/confirm-payouts-sheet.tsx (1)

457-469: Verify array-to-string conversion and handle empty array.

This logic updates excludedPayoutIds as an array, which must be converted to a comma-separated string for the URL parameter. Additionally, when the last excluded payout is re-included, the resulting empty array should delete the query parameter rather than setting it to an empty string.

Verify that queryParams correctly converts arrays to comma-separated strings. Consider explicitly handling the conversion and deletion:

-                  queryParams({
-                    set: {
-                      excludedPayoutIds: excludedPayoutIds.includes(
-                        row.original.id,
-                      )
-                        ? excludedPayoutIds.filter(
-                            (id) => id !== row.original.id,
-                          )
-                        : [...excludedPayoutIds, row.original.id],
-                    },
-                    replace: true,
-                  })
+                  {
+                    const newExcludedIds = excludedPayoutIds.includes(
+                      row.original.id,
+                    )
+                      ? excludedPayoutIds.filter(
+                          (id) => id !== row.original.id,
+                        )
+                      : [...excludedPayoutIds, row.original.id];
+
+                    queryParams(
+                      newExcludedIds.length > 0
+                        ? {
+                            set: {
+                              excludedPayoutIds: newExcludedIds.join(","),
+                            },
+                            replace: true,
+                          }
+                        : {
+                            del: ["excludedPayoutIds"],
+                            replace: true,
+                          },
+                    );
+                  }

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/app/(ee)/api/cron/payouts/process/split-payouts.ts (1)

21-46: Handle missing selected payout and clarify filtering logic

  • When selectedPayoutId is provided but no eligible payout is found, splitPayouts returns early with no indication (split-payouts.ts at if (payouts.length === 0)). In route.ts the response is still “Payouts confirmed…”, so callers can’t distinguish a missing or ineligible selection. Either throw or return an explicit error when a selected payout isn’t found, or update the route handler to detect and handle the empty case.
  • Refactor the nested ternary in the where clause for readability (e.g., extract into a variable or add a clarifying comment).
🧹 Nitpick comments (3)
apps/web/app/(ee)/api/cron/payouts/process/split-payouts.ts (1)

10-20: Consider adding JSDoc to document the new parameter.

The addition of selectedPayoutId is clean and backward-compatible. However, documenting its purpose and interaction with excludedPayoutIds (specifically that selectedPayoutId takes precedence) would improve clarity for future maintainers.

Example JSDoc:

+/**
+ * Splits payouts based on commission creation dates relative to cutoff period.
+ * @param selectedPayoutId - If provided, processes only this specific payout (ignores excludedPayoutIds)
+ * @param excludedPayoutIds - If provided (and selectedPayoutId is not), excludes these payouts from processing
+ */
 export async function splitPayouts({
apps/web/ui/partners/confirm-payouts-sheet.tsx (2)

86-86: Consider validating selectedPayoutId.

The selectedPayoutId is extracted from search params without validation. If an invalid or non-existent payout ID is provided via URL manipulation, the UI might display incorrect data or empty results without clear feedback.

Consider adding validation:

const selectedPayoutId = searchParamsObj.selectedPayoutId || undefined;

// After eligiblePayouts loads, validate the ID
useEffect(() => {
  if (selectedPayoutId && eligiblePayouts && !eligiblePayouts.find(p => p.id === selectedPayoutId)) {
    toast.error("Selected payout not found");
    queryParams({ del: ["selectedPayoutId"] });
  }
}, [selectedPayoutId, eligiblePayouts]);

103-111: Update comment for singular/plural consistency

Replace the existing comment with one that reflects the API-filtered array:

- // if there's a selected payout id, return the payout directly
+ // when `selectedPayoutId` is set, return the API-filtered payouts (usually a single item)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6df433c and 5eff61e.

📒 Files selected for processing (4)
  • apps/web/app/(ee)/api/cron/payouts/process/process-payouts.ts (3 hunks)
  • apps/web/app/(ee)/api/cron/payouts/process/split-payouts.ts (1 hunks)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-table.tsx (1 hunks)
  • apps/web/ui/partners/confirm-payouts-sheet.tsx (9 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-table.tsx
  • apps/web/app/(ee)/api/cron/payouts/process/process-payouts.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (4)
apps/web/ui/partners/confirm-payouts-sheet.tsx (4)

84-86: LGTM!

Router-driven state for selectedPayoutId is correctly extracted from search params. The || undefined fallback appropriately handles both missing and empty string cases.


96-96: LGTM!

Conditional inclusion of selectedPayoutId in the API query is correctly implemented.


575-575: LGTM!

The selectedPayoutId is correctly passed to the confirmPayoutsAction payload.


678-706: LGTM!

The component export and query param cleanup logic are correct. Clearing confirmPayouts, selectedPayoutId, and excludedPayoutIds on sheet close ensures proper state reset.

@steven-tey
Copy link
Collaborator Author

@coderabbitai full review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 9, 2025

✅ Actions performed

Full review triggered.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
apps/web/ui/partners/confirm-payouts-sheet.tsx (2)

104-112: Consider adding a clarifying comment.

The conditional logic is correct, but a brief comment would help explain why selectedPayoutId returns the payouts directly (backend-filtered) versus client-side exclusion filtering.

Example:

  const finalEligiblePayouts = useMemo(() => {
+   // When selectedPayoutId is set, backend returns only that payout;
+   // otherwise, apply client-side exclusion filtering
    if (selectedPayoutId) return eligiblePayouts;
-
-   // else, we need to filter out the excluded payout ids (if specified)
    return eligiblePayouts?.filter(
      (payout) => !excludedPayoutIds.includes(payout.id),
    );
  }, [eligiblePayouts, selectedPayoutId, excludedPayoutIds]);

461-467: Consider defensive filtering in the toggle operation.

While line 102 filters empty strings on read, adding a final .filter(Boolean) or .filter(id => id !== "") when building the new array would provide defense-in-depth against edge cases.

Apply this diff for additional safety:

  queryParams({
    set: {
-     excludedPayoutIds: excludedPayoutIds.includes(
+     excludedPayoutIds: (excludedPayoutIds.includes(
        row.original.id,
      )
        ? excludedPayoutIds.filter(
            (id) => id !== row.original.id,
          )
-       : [...excludedPayoutIds, row.original.id],
+       : [...excludedPayoutIds, row.original.id]
+     ).filter(id => id !== ""),
    },
    replace: true,
  })
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5eff61e and 0ee4237.

📒 Files selected for processing (1)
  • apps/web/ui/partners/confirm-payouts-sheet.tsx (9 hunks)
⏰ 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/ui/partners/confirm-payouts-sheet.tsx (3)

101-102: LGTM! Empty string handling addressed.

The .filter(Boolean) correctly removes empty strings from the split result, addressing the edge case where an empty query parameter would produce [""].


447-474: LGTM! Conditional rendering addresses past review concern.

The toggle button is now only rendered when !selectedPayoutId, which effectively prevents toggling exclusions for a single pre-selected payout. This addresses the past review comment about disabling the button when selectedPayoutId is present.


702-702: LGTM! Proper query param cleanup.

Deleting all relevant query parameters (confirmPayouts, selectedPayoutId, excludedPayoutIds) on sheet close ensures clean state management.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-table.tsx (1)

68-73: Excluding the new payout selection parameters is correct.

The updated exclude array properly prevents selectedPayoutId and excludedPayoutIds from being sent to the general payouts list endpoint, since these are UI state parameters specific to the confirmation flow.

However, the past review comment about potentially missing query parameters (eligibility, invoiceId, page, pageSize) in the URL generation strategy remains valid and should be addressed separately.

🧹 Nitpick comments (2)
apps/web/app/(ee)/api/programs/[programId]/payouts/eligible/route.ts (1)

44-57: Consider adding invoiceId: null filter for consistency.

When selectedPayoutId is provided, the query filters by id but doesn't explicitly check invoiceId: null. While the status: "pending" filter on line 48 likely prevents payouts with invoices from being returned (since their status would have changed), it would be safer and more consistent with process-payouts.ts (line 70) to explicitly include invoiceId: null in the where clause.

Apply this diff to add the explicit filter:

   let payouts = await prisma.payout.findMany({
     where: {
       ...(selectedPayoutId && { id: selectedPayoutId }),
       programId,
       status: "pending",
+      invoiceId: null,
       amount: {
         gte: minPayoutAmount,
       },
apps/web/ui/partners/confirm-payouts-sheet.tsx (1)

461-467: Consider defensive filtering of empty strings.

While the root cause has been addressed at line 102, adding a defensive filter here would guard against edge cases where query parameters might be manually corrupted:

  queryParams({
    set: {
-     excludedPayoutIds: excludedPayoutIds.includes(
+     excludedPayoutIds: (excludedPayoutIds.includes(
        row.original.id,
      )
        ? excludedPayoutIds.filter(
            (id) => id !== row.original.id,
          )
-       : [...excludedPayoutIds, row.original.id],
+       : [...excludedPayoutIds, row.original.id])
+         .filter(Boolean),
    },
    replace: true,
  })
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2152145 and 0ee4237.

📒 Files selected for processing (9)
  • apps/web/app/(ee)/api/cron/payouts/process/process-payouts.ts (3 hunks)
  • apps/web/app/(ee)/api/cron/payouts/process/route.ts (4 hunks)
  • apps/web/app/(ee)/api/cron/payouts/process/split-payouts.ts (1 hunks)
  • apps/web/app/(ee)/api/programs/[programId]/payouts/eligible/route.ts (3 hunks)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-details-sheet.tsx (1 hunks)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-stats.tsx (2 hunks)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-table.tsx (1 hunks)
  • apps/web/lib/actions/partners/confirm-payouts.ts (3 hunks)
  • apps/web/ui/partners/confirm-payouts-sheet.tsx (9 hunks)
⏰ 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 (16)
apps/web/app/(ee)/api/cron/payouts/process/split-payouts.ts (1)

10-27: LGTM! Filtering logic is sound.

The conditional filtering correctly prioritizes selectedPayoutId over excludedPayoutIds. When targeting a specific payout, exclusions are irrelevant, which makes sense. The implementation is clean and the logic matches the corresponding changes in process-payouts.ts.

apps/web/app/(ee)/api/cron/payouts/process/process-payouts.ts (1)

29-67: LGTM! Consistent filtering implementation.

The conditional filtering logic matches split-payouts.ts exactly, ensuring consistent behavior when processing payouts with a selected payout ID.

apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-stats.tsx (2)

6-6: LGTM! Component import updated.

The import change from PayoutInvoiceSheet to ConfirmPayoutsSheet aligns with the renamed component that now handles the selected payout confirmation flow.


71-71: LGTM! Component usage updated.

The component replacement is consistent with the import change and the broader refactor.

apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/payouts/payout-details-sheet.tsx (1)

252-265: LGTM! Button text and flow updated for single-payout confirmation.

The button label change from "Confirm all pending payouts" to "Confirm payout" accurately reflects that clicking it now confirms a single, specific payout. Setting selectedPayoutId in the query params correctly passes the selected payout to the ConfirmPayoutsSheet.

apps/web/lib/actions/partners/confirm-payouts.ts (2)

17-27: LGTM! Schema extended for single-payout confirmation.

Adding the optional selectedPayoutId field to the schema is straightforward and consistent with the broader PR changes.


34-43: LGTM! Parameter properly destructured and propagated.

The selectedPayoutId is correctly extracted from the parsed input and included in the Qstash payload sent to the payout processing cron job.

apps/web/app/(ee)/api/cron/payouts/process/route.ts (2)

13-21: LGTM! Schema updated consistently.

The schema change matches the corresponding update in confirm-payouts.ts, ensuring the cron job can accept and process the selected payout ID.


32-72: LGTM! Parameter threaded through the processing pipeline.

The selectedPayoutId is correctly extracted from the request body and passed to both splitPayouts (when a cutoff period is present) and processPayouts, ensuring consistent filtering throughout the payout processing flow.

apps/web/app/(ee)/api/programs/[programId]/payouts/eligible/route.ts (1)

13-16: LGTM! Schema updated for single-payout filtering.

The schema rename to eligiblePayoutsQuerySchema and the addition of selectedPayoutId are clear and consistent with the broader PR changes.

apps/web/ui/partners/confirm-payouts-sheet.tsx (6)

84-86: LGTM! Router state management added correctly.

The addition of useRouterStuff and derivation of selectedPayoutId from search params is implemented correctly, enabling router-driven state for payout selection.


101-102: Empty string handling fixed.

The addition of .filter(Boolean) correctly addresses the critical issue flagged in previous reviews where empty strings would result from splitting an empty query parameter.


104-112: LGTM! Filtering logic is correct.

The finalEligiblePayouts memo correctly handles both scenarios:

  • When selectedPayoutId is set, returns the backend-filtered single payout directly
  • Otherwise, applies client-side filtering to exclude specified payouts

The dependency array is complete and the logic is sound.


441-474: LGTM! Conditional rendering handles single-payout mode correctly.

The exclude/include toggle is appropriately hidden when selectedPayoutId is set, which makes sense since you're working with a pre-selected single payout. This addresses the concern raised in previous reviews and provides clearer UX than disabling the button.


578-578: LGTM! Payload includes selectedPayoutId correctly.

The addition of selectedPayoutId to the confirmation payload properly passes the selected payout context to the backend action.


681-706: LGTM! Component renaming and cleanup handled correctly.

The renaming from PayoutInvoiceSheet to ConfirmPayoutsSheet is consistent, and the cleanup properly removes all related query parameters (confirmPayouts, selectedPayoutId, excludedPayoutIds) when the sheet closes.

@steven-tey steven-tey merged commit ce029d0 into main Oct 10, 2025
8 of 9 checks passed
@steven-tey steven-tey deleted the confirm-selected-payout branch October 10, 2025 00:12
@coderabbitai coderabbitai bot mentioned this pull request Nov 5, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants