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

Skip to content

Conversation

@devkiran
Copy link
Collaborator

@devkiran devkiran commented Oct 31, 2025

Summary by CodeRabbit

  • New Features

    • Separate Stripe and PayPal payout endpoints and a dedicated per-partner transfer workflow.
    • Automated domain-renewal processing with owner notifications after successful payment.
  • Refactor

    • Centralized workflow configuration and shared validation schemas.
    • Consolidated payout updates for more efficient batch processing and transfer metadata returned.
  • Behavioral change

    • Webhook now dispatches dedicated processors for payouts and renewals; legacy combined cron payout route removed.

@vercel
Copy link
Contributor

vercel bot commented Oct 31, 2025

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

Project Deployment Preview Updated (UTC)
dub Ready Ready Preview Nov 3, 2025 6:45pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 31, 2025

Walkthrough

Removed legacy in-file payout handlers; introduced batched QStash/Upstash routes and a create-stripe-transfer workflow; extracted invoice-driven payout and domain-renewal processors; consolidated workflow config and payload schemas.

Changes

Cohort / File(s) Summary
Webhook & dispatch
apps/web/app/(ee)/api/stripe/webhook/charge-succeeded.ts
Replaced inline payout/domain processing with invoice update then dispatch to processPayoutInvoice / processDomainRenewalInvoice; stores receipt/metadata and routes by invoice.type.
Removed legacy cron route
apps/web/app/(ee)/api/cron/payouts/charge-succeeded/route.ts
Deleted old POST route that validated Qstash and executed payouts inline.
Deleted old payout senders
apps/web/app/(ee)/api/cron/payouts/charge-succeeded/send-stripe-payouts.ts, apps/web/app/(ee)/api/cron/payouts/charge-succeeded/send-paypal-payouts.ts
Removed previous implementations that performed transfers and emails directly.
New batched send routes
apps/web/app/(ee)/api/cron/payouts/send-stripe-payouts/route.ts, apps/web/app/(ee)/api/cron/payouts/send-paypal-payouts/route.ts
Added POST endpoints: Stripe route pages payouts, triggers per-partner create-stripe-transfer workflows and schedules follow-ups; PayPal route creates PayPal batch payout, updates statuses, and sends notifications.
Create Stripe transfer workflow
apps/web/app/(ee)/api/cron/payouts/create-stripe-transfer/route.ts
New Upstash workflow route: parses payload, validates partner, loads payouts, calls createStripeTransfer, and sends partner batch emails.
Workflow config & trigger refactor
apps/web/lib/cron/qstash-workflow.ts
Consolidated per-workflow metadata into WORKFLOW_CONFIG (url, retries, parallelism, schema); added typed WorkflowIds, WorkflowPayloads, QStashWorkflow and refactored triggerWorkflows.
Schema changes
apps/web/lib/zod/schemas/payouts.ts, apps/web/lib/zod/schemas/partners.ts
createStripeTransferWorkflowSchema now expects partnerId + invoiceId (removed payload arrays); added partnerApprovedWorkflowSchema (programId, partnerId, userId).
Route schema migration
apps/web/app/(ee)/api/workflows/partner-approved/route.ts
Switched to imported partnerApprovedWorkflowSchema and updated POST handler typing and parsing.
Transfer update optimization
apps/web/lib/partners/create-stripe-transfer.ts
Replaced per-item Promise.allSettled updates with updateMany for payouts/commissions and now returns the Stripe transfer object.
New payout & domain utilities
apps/web/app/(ee)/api/stripe/webhook/utils/process-payout-invoice.ts, apps/web/app/(ee)/api/stripe/webhook/utils/process-domain-renewal-invoice.ts
Added processPayoutInvoice to enqueue send routes via QStash and processDomainRenewalInvoice to update domains and notify workspace owners.
Minor edits
apps/web/app/(ee)/api/stripe/webhook/payment-intent-requires-action.ts, apps/web/app/(ee)/api/cron/campaigns/broadcast/route.ts
Removed unused latest_charge extraction; updated an inline comment reference (no runtime change).

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Stripe as Stripe webhook (charge-succeeded)
    participant DB as Database (Invoice)
    participant QStash as QStash
    participant SendStripe as /cron/payouts/send-stripe-payouts
    participant SendPayPal as /cron/payouts/send-paypal-payouts
    participant Trigger as triggerWorkflows
    participant CreateWF as create-stripe-transfer workflow
    participant StripeAPI as Stripe API
    participant PayPalAPI as PayPal API
    participant Email as Email service

    Stripe->>DB: update invoice (receipt, status, paidAt, metadata)
    DB-->>Stripe: ack
    alt invoice.type == partnerPayout
        Stripe->>QStash: enqueue processPayoutInvoice(invoiceId)
        QStash->>SendStripe: POST invoiceId (opt. startingAfter)
        SendStripe->>DB: load batch of processing payouts
        SendStripe->>Trigger: triggerWorkflows(per-partner payloads)
        Trigger->>CreateWF: run workflow per partner
        CreateWF->>DB: validate partner & load payouts
        CreateWF->>StripeAPI: create transfer(s)
        StripeAPI-->>CreateWF: transfer result
        CreateWF->>DB: update payouts & commissions (updateMany)
        CreateWF->>Email: send PartnerPayoutProcessed
        SendStripe->>QStash: schedule next batch (if more)
    else invoice.type == domainRenewal
        Stripe->>QStash: enqueue processDomainRenewal(invoiceId)
        QStash->>DB: load domains, update expiresAt
        QStash->>Email: send DomainRenewed to owners
    end
    alt PayPal path (from processPayoutInvoice)
        QStash->>SendPayPal: POST invoiceId
        SendPayPal->>PayPalAPI: create PayPal batch payout
        PayPalAPI-->>SendPayPal: batch result
        SendPayPal->>DB: update payouts to sent
        SendPayPal->>Email: send PayPal notifications
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Focus areas:
    • send-stripe-payouts batching, pagination (startingAfter) and QStash re-scheduling logic.
    • Type alignment between WORKFLOW_CONFIG, WorkflowPayloads, and workflow route schemas.
    • Correctness and idempotency of updateMany filters in create-stripe-transfer.
    • Error handling and retry/dedup semantics when publishing QStash messages from processPayoutInvoice.
    • Email batching and notification correctness after transfer/payout updates.

Possibly related PRs

Suggested reviewers

  • TWilson023

Poem

πŸ‡ I hopped the webhook meadow wide,

Sent invoices off on batching tide,
Transfers hummed and emails sang,
Batches rolled β€” no panic, no clang,
Carrots for work well tried.

Pre-merge checks and finishing touches

βœ… 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 accurately reflects the main objective of the changeset, which is refactoring Stripe payout processing to use QStash workflow instead of inline cron job handlers.
✨ 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 charge-succeeded-workflow

πŸ“œ Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 7df664f and 7d9872e.

πŸ“’ Files selected for processing (1)
  • apps/web/app/(ee)/api/cron/payouts/send-paypal-payouts/route.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/app/(ee)/api/cron/payouts/send-paypal-payouts/route.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

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.

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

πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 6784533 and 9268f33.

πŸ“’ Files selected for processing (8)
  • apps/web/app/(ee)/api/cron/payouts/charge-succeeded/route.ts (1 hunks)
  • apps/web/app/(ee)/api/cron/payouts/charge-succeeded/send-stripe-payouts.ts (2 hunks)
  • apps/web/app/(ee)/api/cron/payouts/create-stripe-transfer/route.ts (1 hunks)
  • apps/web/app/(ee)/api/workflows/partner-approved/route.ts (3 hunks)
  • apps/web/lib/cron/qstash-workflow.ts (1 hunks)
  • apps/web/lib/partners/create-stripe-transfer.ts (1 hunks)
  • apps/web/lib/zod/schemas/partners.ts (1 hunks)
  • apps/web/lib/zod/schemas/payouts.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (5)
πŸ““ 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.
πŸ“š 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
  • apps/web/lib/zod/schemas/payouts.ts
  • apps/web/app/(ee)/api/cron/payouts/create-stripe-transfer/route.ts
  • apps/web/app/(ee)/api/cron/payouts/charge-succeeded/send-stripe-payouts.ts
πŸ“š Learning: 2025-10-17T08:18:19.278Z
Learnt from: devkiran
Repo: dubinc/dub PR: 0
File: :0-0
Timestamp: 2025-10-17T08:18:19.278Z
Learning: In the apps/web codebase, `@/lib/zod` should only be used for places that need OpenAPI extended zod schema. All other places should import from the standard `zod` package directly using `import { z } from "zod"`.

Applied to files:

  • apps/web/lib/zod/schemas/partners.ts
πŸ“š Learning: 2025-07-30T15:25:13.936Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2673
File: apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx:56-66
Timestamp: 2025-07-30T15:25:13.936Z
Learning: In apps/web/ui/partners/rewards/add-edit-reward-sheet.tsx, the form schema uses partial condition objects to allow users to add empty/unconfigured condition fields without type errors, while submission validation uses strict schemas to ensure data integrity. This two-stage validation pattern improves UX by allowing progressive completion of complex forms.

Applied to files:

  • apps/web/lib/zod/schemas/partners.ts
πŸ“š Learning: 2025-08-25T17:33:45.072Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2736
File: apps/web/app/(ee)/api/stripe/integration/webhook/invoice-paid.ts:12-12
Timestamp: 2025-08-25T17:33:45.072Z
Learning: The WorkflowTrigger enum in packages/prisma/schema/workflow.prisma contains three values: leadRecorded, saleRecorded, and commissionEarned. All three are properly used throughout the codebase.

Applied to files:

  • apps/web/lib/cron/qstash-workflow.ts
🧬 Code graph analysis (6)
apps/web/lib/partners/create-stripe-transfer.ts (1)
packages/prisma/index.ts (1)
  • prisma (3-9)
apps/web/app/(ee)/api/workflows/partner-approved/route.ts (2)
apps/web/app/(ee)/api/cron/payouts/create-stripe-transfer/route.ts (1)
  • serve (11-136)
apps/web/lib/zod/schemas/partners.ts (1)
  • partnerApprovedWorkflowSchema (760-764)
apps/web/lib/zod/schemas/payouts.ts (1)
apps/web/lib/zod/schemas/programs.ts (1)
  • ProgramSchema (23-49)
apps/web/app/(ee)/api/cron/payouts/create-stripe-transfer/route.ts (6)
apps/web/lib/zod/schemas/payouts.ts (1)
  • createStripeTransferWorkflowSchema (107-112)
apps/web/lib/cron/qstash-workflow-logger.ts (1)
  • createWorkflowLogger (13-36)
packages/prisma/index.ts (1)
  • prisma (3-9)
apps/web/lib/partners/create-stripe-transfer.ts (1)
  • createStripeTransfer (16-156)
packages/email/src/index.ts (1)
  • sendBatchEmail (31-67)
packages/email/src/templates/partner-payout-processed.tsx (1)
  • PartnerPayoutProcessed (17-119)
apps/web/lib/cron/qstash-workflow.ts (2)
apps/web/lib/zod/schemas/partners.ts (1)
  • partnerApprovedWorkflowSchema (760-764)
apps/web/lib/zod/schemas/payouts.ts (1)
  • createStripeTransferWorkflowSchema (107-112)
apps/web/app/(ee)/api/cron/payouts/charge-succeeded/send-stripe-payouts.ts (1)
apps/web/lib/cron/qstash-workflow.ts (2)
  • QStashWorkflow (33-36)
  • triggerWorkflows (39-86)
πŸ”‡ Additional comments (18)
apps/web/lib/partners/create-stripe-transfer.ts (3)

131-142: LGTM! Efficient batch update.

Consolidating the payout updates into a single updateMany call is more efficient than individual updates and ensures all payouts are updated atomically with the same transfer ID.


144-153: LGTM! Consistent batch update pattern.

The commission batch update follows the same efficient pattern as the payout update, ensuring all related commissions are marked as paid in a single operation.


155-155: LGTM! Supports workflow integration.

Returning the transfer object enables the new workflow-based architecture to access transfer details for downstream processing and notifications.

apps/web/app/(ee)/api/cron/payouts/charge-succeeded/route.ts (1)

90-117: LGTM! Batching logic is well-structured.

The recursive batching mechanism via QStash is correctly implemented. The error handling checks for messageId and logs failures appropriately. The payload includes only the necessary invoiceId for the next batch.

apps/web/lib/zod/schemas/payouts.ts (1)

93-112: Well-designed workflow schema.

The payoutSchema appropriately captures the minimal fields needed for the Stripe transfer workflow and email notifications. The separation of previouslyProcessedPayouts and currentInvoicePayouts aligns with the business logic of grouping payouts by partner across invoices.

apps/web/lib/zod/schemas/partners.ts (1)

760-764: LGTM! Clean workflow schema definition.

The schema is minimal and appropriate for the partner approval workflow, capturing the essential identifiers needed.

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

1-4: LGTM! Imports correctly reflect the architectural shift.

The import changes appropriately swap direct processing functions for the workflow-based approach using QStashWorkflow and triggerWorkflows.

apps/web/app/(ee)/api/workflows/partner-approved/route.ts (3)

9-12: LGTM! Import consolidation improves maintainability.

Moving from a local schema to the shared partnerApprovedWorkflowSchema improves consistency and reduces duplication.


43-43: LGTM! Type inference ensures type safety.

Using z.infer<typeof partnerApprovedWorkflowSchema> ensures the route's payload type is always in sync with the schema definition.


332-332: LGTM! Parser correctly uses shared schema.

The initialPayloadParser now uses the shared schema, maintaining consistency with the type definition on line 43.

apps/web/app/(ee)/api/cron/payouts/create-stripe-transfer/route.ts (4)

1-8: LGTM! All necessary imports are present.

The imports are well-organized and include all dependencies needed for the workflow implementation.


10-44: LGTM! Route setup follows best practices.

The workflow route correctly uses typed serve, creates a logger for observability, and fetches the partner once to be reused across steps.


46-75: LGTM! Comprehensive validation before transfer creation.

Step 1 includes all necessary validation checks (partner exists, payouts enabled, stripeConnectId present) with appropriate error logging for each failure scenario.


130-134: LGTM! Parser follows standard workflow pattern.

The initialPayloadParser correctly validates the payload using the schema, ensuring type safety.

apps/web/lib/cron/qstash-workflow.ts (4)

3-5: LGTM! Imports support the schema-driven approach.

The new imports enable the config-driven workflow system with type-safe payload validation.


11-25: Excellent config-driven design!

The centralized WORKFLOW_CONFIG improves maintainability by consolidating all workflow metadata in one place. The as const assertion enables proper type inference for the mapped types below. Parallelism values are appropriately tuned (20 for partner-approved, 10 for create-stripe-transfer which involves external API calls).


27-36: Excellent type-safe design!

The mapped type system ensures compile-time safety: QStashWorkflow<T> constrains the body to match the specific workflow's payload type. This prevents mismatches between workflow IDs and their payloads.


40-58: LGTM! Config-driven approach improves maintainability.

The refactored triggerWorkflows now derives all settings from WORKFLOW_CONFIG, eliminating hardcoded values. The type-safe signature ensures only valid workflow IDs and matching payloads can be passed.

… files; implement new routes for sending PayPal and Stripe payouts; update invoice processing logic to handle batch payouts and email notifications.
…processing into separate utility functions for improved organization and maintainability.
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: 6

πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between ce5130d and 1be6480.

πŸ“’ Files selected for processing (9)
  • apps/web/app/(ee)/api/cron/campaigns/broadcast/route.ts (1 hunks)
  • apps/web/app/(ee)/api/cron/payouts/charge-succeeded/route.ts (0 hunks)
  • apps/web/app/(ee)/api/cron/payouts/charge-succeeded/send-paypal-payouts.ts (0 hunks)
  • apps/web/app/(ee)/api/cron/payouts/charge-succeeded/send-stripe-payouts.ts (0 hunks)
  • apps/web/app/(ee)/api/cron/payouts/create-stripe-transfer/route.ts (1 hunks)
  • apps/web/app/(ee)/api/cron/payouts/send-paypal-payouts/route.ts (1 hunks)
  • apps/web/app/(ee)/api/cron/payouts/send-stripe-payouts/route.ts (1 hunks)
  • apps/web/app/(ee)/api/stripe/webhook/charge-succeeded.ts (1 hunks)
  • apps/web/lib/zod/schemas/payouts.ts (1 hunks)
πŸ’€ Files with no reviewable changes (3)
  • apps/web/app/(ee)/api/cron/payouts/charge-succeeded/route.ts
  • apps/web/app/(ee)/api/cron/payouts/charge-succeeded/send-stripe-payouts.ts
  • apps/web/app/(ee)/api/cron/payouts/charge-succeeded/send-paypal-payouts.ts
βœ… Files skipped from review due to trivial changes (1)
  • apps/web/app/(ee)/api/cron/campaigns/broadcast/route.ts
🧰 Additional context used
🧠 Learnings (3)
πŸ““ 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.
πŸ“š 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/send-paypal-payouts/route.ts
  • apps/web/app/(ee)/api/cron/payouts/send-stripe-payouts/route.ts
  • apps/web/app/(ee)/api/stripe/webhook/charge-succeeded.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/lib/zod/schemas/payouts.ts
  • apps/web/app/(ee)/api/stripe/webhook/charge-succeeded.ts
🧬 Code graph analysis (3)
apps/web/app/(ee)/api/cron/payouts/send-paypal-payouts/route.ts (5)
apps/web/app/(ee)/api/cron/payouts/send-stripe-payouts/route.ts (1)
  • POST (26-160)
packages/prisma/index.ts (1)
  • prisma (3-9)
packages/email/src/index.ts (1)
  • sendBatchEmail (31-70)
packages/email/src/templates/partner-payout-processed.tsx (1)
  • PartnerPayoutProcessed (17-119)
apps/web/lib/api/errors.ts (1)
  • handleAndReturnErrorResponse (175-178)
apps/web/app/(ee)/api/cron/payouts/send-stripe-payouts/route.ts (4)
apps/web/app/(ee)/api/cron/payouts/send-paypal-payouts/route.ts (1)
  • POST (21-118)
packages/prisma/index.ts (1)
  • prisma (3-9)
apps/web/lib/cron/qstash-workflow.ts (1)
  • triggerWorkflows (39-87)
apps/web/lib/api/errors.ts (1)
  • handleAndReturnErrorResponse (175-178)
apps/web/app/(ee)/api/cron/payouts/create-stripe-transfer/route.ts (6)
apps/web/lib/zod/schemas/payouts.ts (1)
  • createStripeTransferWorkflowSchema (93-97)
apps/web/lib/cron/qstash-workflow-logger.ts (1)
  • createWorkflowLogger (13-36)
packages/prisma/index.ts (1)
  • prisma (3-9)
apps/web/lib/partners/create-stripe-transfer.ts (1)
  • createStripeTransfer (16-156)
packages/email/src/index.ts (1)
  • sendBatchEmail (31-70)
packages/email/src/templates/partner-payout-processed.tsx (1)
  • PartnerPayoutProcessed (17-119)
⏰ 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/app/(ee)/api/stripe/webhook/charge-succeeded.ts (1)

73-86: LGTM! Good use of parallel execution.

Using Promise.allSettled is appropriate here since Stripe and PayPal payout processing are independent operations. The distinct deduplication IDs prevent duplicate processing per service.

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: 4

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/stripe/webhook/charge-succeeded.ts (1)

32-48: Invoice marked completed before processingβ€”creates inconsistent state on utility failure.

The invoice is marked as status: "completed" at line 38, but the actual payout/domain renewal processing happens afterward (lines 44-48). If either processPayoutInvoice or processDomainRenewalInvoice throws an error, the invoice remains marked as completed while the processing never finished, creating an inconsistent state.

Consider one of these approaches:

  1. Move the status update after successful processing (preferred if utilities are synchronous or throw on failure):
-  invoice = await prisma.invoice.update({
+  const updatedInvoice = await prisma.invoice.update({
     where: {
       id: invoice.id,
     },
     data: {
       receiptUrl: charge.receipt_url,
-      status: "completed",
       paidAt: new Date(),
       stripeChargeMetadata: JSON.parse(JSON.stringify(charge)),
     },
   });

-  if (invoice.type === "partnerPayout") {
-    await processPayoutInvoice({ invoice });
-  } else if (invoice.type === "domainRenewal") {
-    await processDomainRenewalInvoice({ invoice });
+  if (updatedInvoice.type === "partnerPayout") {
+    await processPayoutInvoice({ invoice: updatedInvoice });
+  } else if (updatedInvoice.type === "domainRenewal") {
+    await processDomainRenewalInvoice({ invoice: updatedInvoice });
   }
+
+  // Mark as completed only after successful processing
+  await prisma.invoice.update({
+    where: {
+      id: updatedInvoice.id,
+    },
+    data: {
+      status: "completed",
+    },
+  });
  1. Add explicit error handling to wrap utility calls and update invoice status based on outcome.

  2. Use a "processing" intermediate status before "completed" to track the state more accurately.

πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 1be6480 and af29633.

πŸ“’ Files selected for processing (3)
  • apps/web/app/(ee)/api/stripe/webhook/charge-succeeded.ts (1 hunks)
  • apps/web/app/(ee)/api/stripe/webhook/utils/process-domain-renewal-invoice.ts (1 hunks)
  • apps/web/app/(ee)/api/stripe/webhook/utils/process-payout-invoice.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
πŸ““ 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.
🧬 Code graph analysis (2)
apps/web/app/(ee)/api/stripe/webhook/utils/process-domain-renewal-invoice.ts (4)
packages/prisma/index.ts (1)
  • prisma (3-9)
apps/web/lib/dynadot/set-renew-option.ts (1)
  • setRenewOption (12-61)
packages/email/src/index.ts (1)
  • sendBatchEmail (31-70)
apps/web/app/(ee)/api/stripe/webhook/utils/process-domain-renewal-failure.ts (1)
  • processDomainRenewalFailure (10-121)
apps/web/app/(ee)/api/stripe/webhook/utils/process-payout-invoice.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 (2)
apps/web/app/(ee)/api/stripe/webhook/charge-succeeded.ts (1)

44-48: Error handling is properly implemented at the route handler level.

The chargeSucceeded function is called within a try-catch block in the parent route handler. If any error is thrown, it's caught and logged, then a Response with status 400 is returned, which properly triggers Stripe's retry mechanism.

apps/web/app/(ee)/api/stripe/webhook/utils/process-payout-invoice.ts (1)

7-14: Verify payout status values.

The filter excludes only status: "completed", but the codebase defines additional terminal statuses. Specifically:

  • "failed" payouts can only be retried manually (see payout-row-menu.tsx:42 and retry-failed-paypal-payouts.ts:70), so they should not be automatically requeued.
  • "canceled" payouts are defined in the PayoutStatus enum (payout-status-badges.tsx) but should not be processed.

Consider excluding these statuses from the query to prevent unnecessary queue operations and maintain consistency with the intended payout workflow.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (3)
apps/web/app/(ee)/api/cron/payouts/create-stripe-transfer/route.ts (1)

118-125: Email notification step will not find payouts after successful transfer.

The query filters for status: "processed" but createStripeTransfer() updates successful payouts to "sent", so this returns an empty result set on the happy path and partners never receive email notifications.

Apply this diff to include both statuses:

      const processedPayouts = await prisma.payout.findMany({
        where: {
          invoiceId,
          partnerId: partner.id,
-         status: "processed",
+         status: {
+           in: ["processed", "sent"],
+         },
        },
        include: commonInclude,
      });
apps/web/app/(ee)/api/cron/payouts/send-stripe-payouts/route.ts (2)

114-124: Check workflow trigger result and fail fast on errors.

triggerWorkflows() returns null whenever Upstash rejects the request, but the code currently ignores the result and continues. This leaves payouts stuck in "processing" status indefinitely if the workflow trigger fails.

Apply this diff to validate the result:

     if (stripePayouts.length > 0) {
-      await triggerWorkflows(
+      const workflowResult = await triggerWorkflows(
         stripePayouts.map(({ partnerId }) => ({
           workflowId: "create-stripe-transfer",
           body: {
             partnerId,
             invoiceId,
             chargeId,
           },
         })),
       );
+
+      if (!workflowResult) {
+        throw new Error(
+          `Failed to trigger create-stripe-transfer workflows for invoice ${invoiceId}`,
+        );
+      }
     }

137-142: Treat missing QStash messageId as a hard failure.

If publishJSON returns without a messageId, we log the error but still return success, which drops remaining payouts because no follow-up job runs. This should bubble up as an error so the cron system can retry.

Apply this diff:

       if (!response.messageId) {
         await log({
           message: `Error scheduling next batch of payouts for invoice ${invoiceId}: ${JSON.stringify(response)}`,
           type: "errors",
         });
-      }
-
-      return logAndRespond(
-        `Scheduled next batch of payouts for invoice ${invoiceId} with QStash message ${response.messageId}`,
-      );
+        throw new Error(
+          `Failed to schedule next batch of payouts for invoice ${invoiceId}`,
+        );
+      }
+
+      return logAndRespond(
+        `Scheduled next batch of payouts for invoice ${invoiceId} with QStash message ${response.messageId}`,
+      );
πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between af29633 and 7df664f.

πŸ“’ Files selected for processing (2)
  • apps/web/app/(ee)/api/cron/payouts/create-stripe-transfer/route.ts (1 hunks)
  • apps/web/app/(ee)/api/cron/payouts/send-stripe-payouts/route.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (3)
πŸ““ 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.
πŸ“š Learning: 2025-08-25T17:33:45.072Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2736
File: apps/web/app/(ee)/api/stripe/integration/webhook/invoice-paid.ts:12-12
Timestamp: 2025-08-25T17:33:45.072Z
Learning: The WorkflowTrigger enum in packages/prisma/schema/workflow.prisma contains three values: leadRecorded, saleRecorded, and commissionEarned. All three are properly used throughout the codebase.

Applied to files:

  • apps/web/app/(ee)/api/cron/payouts/send-stripe-payouts/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/create-stripe-transfer/route.ts
🧬 Code graph analysis (2)
apps/web/app/(ee)/api/cron/payouts/send-stripe-payouts/route.ts (5)
apps/web/app/(ee)/api/cron/payouts/send-paypal-payouts/route.ts (1)
  • POST (21-118)
apps/web/app/(ee)/api/cron/campaigns/broadcast/route.ts (1)
  • POST (37-317)
packages/prisma/index.ts (1)
  • prisma (3-9)
apps/web/lib/cron/qstash-workflow.ts (1)
  • triggerWorkflows (39-87)
apps/web/lib/api/errors.ts (1)
  • handleAndReturnErrorResponse (175-178)
apps/web/app/(ee)/api/cron/payouts/create-stripe-transfer/route.ts (5)
apps/web/lib/zod/schemas/payouts.ts (1)
  • createStripeTransferWorkflowSchema (93-97)
packages/prisma/index.ts (1)
  • prisma (3-9)
apps/web/lib/partners/create-stripe-transfer.ts (1)
  • createStripeTransfer (16-156)
packages/email/src/index.ts (1)
  • sendBatchEmail (31-70)
packages/email/src/templates/partner-payout-processed.tsx (1)
  • PartnerPayoutProcessed (17-119)
⏰ 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants