Skip to content

Fix failed payout status update#3458

Merged
steven-tey merged 1 commit intomainfrom
fix-failed-payout
Feb 12, 2026
Merged

Fix failed payout status update#3458
steven-tey merged 1 commit intomainfrom
fix-failed-payout

Conversation

@steven-tey
Copy link
Collaborator

@steven-tey steven-tey commented Feb 11, 2026

Summary by CodeRabbit

  • Bug Fixes
    • Payout reconciliation now also processes previously failed payouts so they aren't skipped.
    • Improved partner-specific identification so balance and payout updates target the correct partner.
    • Combined handling of newly sent and failed payouts in a single pass, ensuring payout IDs are recorded where available.

@vercel
Copy link
Contributor

vercel bot commented Feb 11, 2026

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

Project Deployment Actions Updated (UTC)
dub Ready Ready Preview Feb 11, 2026 6:36pm

Request Review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 11, 2026

📝 Walkthrough

Walkthrough

Payout balance cron route now scopes updates to a specific partner (partner.id added) and broadens the payout update filter to an OR of: (status "sent" && stripePayoutId null && stripeTransferId in transfers) OR (status "failed" && stripePayoutId not null).

Changes

Cohort / File(s) Summary
Payout Update Logic
apps/web/app/(ee)/api/cron/payouts/balance-available/route.ts
Selects partner.id in partner lookup. Changes prisma.payout.updateMany where-clause to include partnerId: partner.id and an OR combining: 1) status: "sent", stripePayoutId: null, stripeTransferId in transfers; 2) status: "failed", stripePayoutId not null. Updates stripePayoutId for matched records.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • devkiran

Poem

🐇✨ I hop through code with nimble feet,
Partner ids and payouts now meet.
Two paths join under one OR gate,
Sending fixes — isn't that great? 🥕

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: fixing the failed payout status update issue by enabling automatic retry of failed Stripe payouts.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 fix-failed-payout

No actionable comments were generated in the recent review. 🎉

🧹 Recent nitpick comments
apps/web/app/(ee)/api/cron/payouts/balance-available/route.ts (2)

165-187: Good fix: partner scoping and failed-payout retry logic look correct.

Adding partnerId prevents cross-partner updates, and the OR branch for "failed" payouts with an existing stripePayoutId aligns with Stripe's retry semantics — funds remain in the connected account's available balance after a bank-payout failure, so re-associating them with the new stripePayout.id is appropriate.

One minor observation: the "failed" branch (lines 176–181) matches all failed payouts for this partner that have a stripePayoutId, without verifying they also have a stripeTransferId. Given the expected lifecycle (transfer → sent → payout → completed/failed), a failed payout should always have a stripeTransferId, but you could add it defensively:

Optional: tighten the failed-payout filter
           {
             status: "failed",
             stripePayoutId: {
               not: null,
             },
+            stripeTransferId: {
+              not: null,
+            },
           },

155-158: Pre-existing: transfers.list capped at 100 may leave "sent" payouts unmatched.

If a partner has more than 100 transfers, the stripeTransferId: { in: transfers.data.map(...) } filter won't cover older ones. This is a pre-existing limitation and not introduced by this PR, but worth being aware of for high-volume partners. Pagination or a higher limit could address it in the future.


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.

@steven-tey
Copy link
Collaborator Author

@coderabbitai full review, any breaking changes?

@steven-tey steven-tey requested a review from devkiran February 12, 2026 03:38
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 12, 2026

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read the modified file to see the actual changes
cat apps/web/app/\(ee\)/api/cron/payouts/balance-available/route.ts

Length of output: 7521


🏁 Script executed:

#!/bin/bash
# Search for any references to this cron endpoint
rg -n "balance-available" --type ts --type tsx -C 2

Length of output: 27


🏁 Script executed:

#!/bin/bash
# Search for payout status updates to understand the pattern
rg -n "status.*failed.*payout" --type ts -C 2

Length of output: 1772


🏁 Script executed:

#!/bin/bash
# Look for other files that might interact with payout status
rg -n "stripePayoutId|stripeTransferId" --type ts -g '!node_modules' -g '!*.test.*' -C 2 | head -100

Length of output: 6335


🏁 Script executed:

#!/bin/bash
# Check the payout-failed and payout-paid webhook handlers
cat apps/web/app/\(ee\)/api/cron/payouts/payout-failed/route.ts

Length of output: 2672


🏁 Script executed:

#!/bin/bash
cat apps/web/app/\(ee\)/api/cron/payouts/payout-paid/route.ts

Length of output: 2656


🏁 Script executed:

#!/bin/bash
# Check for Stripe webhook handlers that might trigger this cron
rg -n "balance\.available" --type ts -C 3

Length of output: 4005


🏁 Script executed:

#!/bin/bash
# Look at the Payout schema to understand the fields
fd -e prisma | xargs rg -n "model Payout" -A 30

Length of output: 2631


@steven-tey I've completed a comprehensive analysis of PR #3458. Here's my full review:

🎯 Summary

This PR fixes a critical bug and adds automatic retry logic for failed Stripe payouts. No breaking changes detected.

📋 Changes Overview

File: apps/web/app/(ee)/api/cron/payouts/balance-available/route.ts

Three key modifications:

  1. Added partner.id to select clause (Line ~40)

    • Required for the new partnerId filter below
  2. Added partnerId filter (Line ~167) ⚠️ Critical Bug Fix

    • Before: Query didn't filter by partner, theoretically allowing cross-partner payout updates
    • After: Properly scoped to specific partner
  3. Added OR condition for failed payout retry (Lines ~169-181) ✨ New Feature

    • Condition 1 (existing): Status "sent" + no stripePayoutId + has stripeTransferId
    • Condition 2 (new): Status "failed" + has stripePayoutId

🔄 Payout Flow Context

Based on webhook handlers I reviewed:

graph LR
    A[Stripe Transfer] -->|stripeTransferId set| B[Status: sent]
    B -->|Stripe Payout Created| C[stripePayoutId set]
    C -->|payout.paid webhook| D[Status: completed]
    C -->|payout.failed webhook| E[Status: failed]
    E -->|Balance available again| F[NEW: Automatic Retry]
    F -->|New stripePayoutId| C
Loading

🔍 Breaking Changes Analysis

NO BREAKING CHANGES

Category Impact Reasoning
Database Schema None No schema modifications
Public API None Internal cron endpoint only
Query Behavior Safer More restrictive (partnerId filter)
Failed Payouts Enhanced Automatic retry now enabled
Data Migration Not needed Handles existing data correctly

🐛 Bug Fixed

Missing partnerId filter: According to Stripe documentation, when a payout fails, Stripe reattempts the payout at the next scheduled payout interval. The previous code could theoretically match payouts from different partners if transfer IDs coincidentally overlapped (extremely unlikely but possible in edge cases).

✨ Feature Added

Automatic failed payout retry: The new OR condition allows payouts with status "failed" and existing stripePayoutId to be automatically retried when new balance becomes available. This aligns with the pattern where failed payouts get a new stripePayoutId for the retry attempt.

Behavior:

  • When balance.available webhook fires → creates new Stripe payout
  • Updates both:
    1. Payouts with status "sent" that haven't been withdrawn yet
    2. Payouts with status "failed" that failed previously ← NEW
  • All matching payouts get the new stripePayoutId

⚠️ Considerations

  1. Multiple failed payouts: If a partner has multiple failed payouts, they all get updated with the same new stripePayoutId. This appears intentional - one withdrawal attempt covers all pending/failed payouts.

  2. Transfer limit: The code fetches up to 100 transfers (limit: 100). If there are more, some "sent" payouts won't be matched. This is a pre-existing limitation, not introduced by this PR.

  3. Race conditions: Multiple simultaneous webhooks for the same partner could theoretically conflict, but Prisma's updateMany should handle this atomically.

✅ Recommendation

Approve and merge. This is a well-implemented bug fix with an important enhancement for handling failed payouts. The changes are:

  • Backwards compatible
  • More restrictive (safer)
  • Additive (enables retry functionality)
  • Properly scoped to prevent cross-partner issues
✅ Actions performed

Full review triggered.

@steven-tey steven-tey merged commit 9c1b30b into main Feb 12, 2026
11 checks passed
@steven-tey steven-tey deleted the fix-failed-payout branch February 12, 2026 04:16
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