Skip to content

Schedule stablecoin payouts after Stripe available_on#3564

Open
devkiran wants to merge 4 commits intomainfrom
fix-global-payouts
Open

Schedule stablecoin payouts after Stripe available_on#3564
devkiran wants to merge 4 commits intomainfrom
fix-global-payouts

Conversation

@devkiran
Copy link
Collaborator

@devkiran devkiran commented Mar 11, 2026

Summary by CodeRabbit

  • New Features
    • Stablecoin payouts now assess fund availability and will schedule delayed payouts when funds aren’t yet available, or execute immediately when ready.
    • Stripe payouts can be conditionally skipped based on scheduling decisions to avoid premature attempts.
    • Payout processing is rate-limited to improve stability and prevent duplicate or overloaded queuing.

@vercel
Copy link
Contributor

vercel bot commented Mar 11, 2026

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

Project Deployment Actions Updated (UTC)
dub Ready Ready Preview Mar 11, 2026 11:56am

Request Review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 11, 2026

📝 Walkthrough

Walkthrough

Introduces conditional stablecoin payout scheduling: a new utility inspects Stripe balance transactions and either executes funding immediately or schedules a delayed retry via Qstash; a skip flag is threaded into payout queuing to optionally exclude stablecoin payouts, and Qstash flowControl is added to webhook publishing.

Changes

Cohort / File(s) Summary
Stablecoin scheduling utility
apps/web/app/(ee)/api/cron/payouts/charge-succeeded/utils.ts
New file: validates charge metadata, fetches Stripe balance transactions, decides nextAction ("executeNow" or "skip"), and schedules delayed callback via Qstash when needed.
Charge-succeeded cron route
apps/web/app/(ee)/api/cron/payouts/charge-succeeded/route.ts
Calls scheduleDelayedStablecoinPayouts, uses its nextAction to conditionally fund financial account, sets skipStablecoinPayouts accordingly, and passes flag into queueStripePayouts.
Stripe payout queuing
apps/web/app/(ee)/api/cron/payouts/charge-succeeded/queue-stripe-payouts.ts
Signature extended to accept skipStablecoinPayouts; payout-method filter made dynamic to exclude stablecoin when skip flag is true.
Webhook rate limiting
apps/web/app/(ee)/api/stripe/webhook/charge-succeeded.ts
Added Qstash flowControl (keyed by invoice.id, rate 1) to publishJSON when enqueuing processPayoutInvoice.

Sequence Diagram(s)

sequenceDiagram
    participant Route as Route<br/>(charge-succeeded)
    participant Utils as scheduleDelayed<br/>StablecoinPayouts
    participant Stripe as Stripe API
    participant Qstash as Qstash
    participant Queue as queueStripePayouts

    Route->>Utils: check invoice.stripeChargeMetadata
    Utils->>Stripe: fetch balance transactions by charge
    Stripe-->>Utils: balance transaction(s)

    alt available_on <= now
        Utils-->>Route: nextAction: "executeNow"
        Route->>Stripe: fund financial account
        Stripe-->>Route: funding result
        Route->>Queue: queueStripePayouts(invoice, skip=false)
    else available_on > now
        Utils->>Qstash: schedule callback (15 min after available_on)
        Qstash-->>Utils: messageId
        Utils-->>Route: nextAction: "skip"
        Route->>Queue: queueStripePayouts(invoice, skip=true)
    else no transaction
        Utils-->>Route: nextAction: "skip"
        Route->>Queue: queueStripePayouts(invoice, skip=true)
    end

    Queue->>Queue: filter payout methods (exclude stablecoin if skip=true)
    Queue-->>Route: queued partner payouts
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

  • #3138 — Modifies queueStripePayouts filtering logic (adds stripeConnectId check) and touches same function.
  • #3058 — Also changes queueStripePayouts implementation for partner Stripe payouts.
  • #3021 — Updates Stripe charge-succeeded webhook processing; touches related payout flow.

Suggested reviewers

  • steven-tey

Poem

🐰
I hopped through ledgers, sniffed the charge's trail,
If funds are ripe I'll sprint — else I mail a trail.
Qstash hums a lullaby for coins that sleep,
Then off they hop to partners — neat and fleet.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ 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 'Schedule stablecoin payouts after Stripe available_on' directly and clearly describes the main change across the pull request—implementing scheduled execution of stablecoin payouts based on Stripe's available_on timestamp.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix-global-payouts

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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/app/`(ee)/api/cron/payouts/charge-succeeded/route.ts:
- Around line 90-106: When nextAction === "executeNow" and the call to
fundFinancialAccount({ amount: stablecoinFundingAmount, idempotencyKey:
invoiceId }) fails, the catch only logs the error but does not prevent
subsequent stablecoin payout queuing; update the catch block to also set
skipStablecoinPayouts = true (in addition to calling log) so that downstream
logic that uses skipStablecoinPayouts will skip enqueuing stablecoin payouts
when fundFinancialAccount throws.

In `@apps/web/app/`(ee)/api/cron/payouts/charge-succeeded/utils.ts:
- Around line 14-16: Update the mismatched comment so it reflects the
implemented 15-minute delay for stablecoin payouts: change the text that
currently reads "`available_on + 10 minutes`" to "`available_on + 15 minutes`"
to match the calculation using `15 * 60 * 1000` (used for stablecoin payouts /
cron scheduling).
- Around line 72-85: The function currently returns { nextAction: "skip" }
silently when qstashResponse.messageId is falsy; update it to log an error that
includes qstashResponse and scheduleTimeMs for debugging. Specifically, where
qstashResponse is checked, add a console.error or processLogger.error call when
qstashResponse.messageId is falsy that includes a clear message ("Qstash
scheduling failed"), the entire qstashResponse object and scheduleTimeMs (or its
derived scheduledAt ISO string), then still return { nextAction: "skip" } so
callers behave the same but failures are visible.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 11c4f841-664c-48f8-a2be-07802c78b27d

📥 Commits

Reviewing files that changed from the base of the PR and between f9b9e50 and 308008e.

📒 Files selected for processing (4)
  • apps/web/app/(ee)/api/cron/payouts/charge-succeeded/queue-stripe-payouts.ts
  • apps/web/app/(ee)/api/cron/payouts/charge-succeeded/route.ts
  • apps/web/app/(ee)/api/cron/payouts/charge-succeeded/utils.ts
  • apps/web/app/(ee)/api/stripe/webhook/charge-succeeded.ts

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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/app/`(ee)/api/cron/payouts/charge-succeeded/route.ts:
- Around line 91-103: The catch block around the fundFinancialAccount call
swallows errors and sets skipStablecoinPayouts, preventing QStash from retrying;
update the catch in the route handling the charge-succeeded cron (the try/catch
that calls fundFinancialAccount with idempotencyKey: invoiceId) to log the error
via log(...) and then rethrow the original error (or explicitly enqueue a QStash
retry) instead of setting skipStablecoinPayouts to true so QStash's native retry
mechanism can handle the failure.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 973d9116-9e1b-4c39-8c05-cf64e8f08a0f

📥 Commits

Reviewing files that changed from the base of the PR and between 308008e and 05bf6ab.

📒 Files selected for processing (2)
  • apps/web/app/(ee)/api/cron/payouts/charge-succeeded/route.ts
  • apps/web/app/(ee)/api/cron/payouts/charge-succeeded/utils.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/app/(ee)/api/cron/payouts/charge-succeeded/utils.ts

Comment on lines +91 to +103
try {
await fundFinancialAccount({
amount: stablecoinFundingAmount,
idempotencyKey: invoiceId,
});
} catch (error) {
await log({
message: `Failed to fund Dub's financial account for stablecoin payouts: ${error.message}`,
type: "errors",
});

skipStablecoinPayouts = true;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Preserve QStash retries when funding fails.

This catch turns an executeNow funding failure into a successful cron run, so the stablecoin payouts stay in processing with no retry path. Either rethrow after logging or explicitly enqueue a retry before continuing.

🐛 Minimal fix
       if (nextAction === "executeNow") {
         try {
           await fundFinancialAccount({
             amount: stablecoinFundingAmount,
             idempotencyKey: invoiceId,
           });
         } catch (error) {
           await log({
             message: `Failed to fund Dub's financial account for stablecoin payouts: ${error.message}`,
             type: "errors",
           });
-
-          skipStablecoinPayouts = true;
+          throw error;
         }
       }

Based on learnings, cron endpoints under apps/web/app/(ee)/api/cron should allow QStash to retry cron errors via its native retry mechanism rather than swallowing them.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/app/`(ee)/api/cron/payouts/charge-succeeded/route.ts around lines 91
- 103, The catch block around the fundFinancialAccount call swallows errors and
sets skipStablecoinPayouts, preventing QStash from retrying; update the catch in
the route handling the charge-succeeded cron (the try/catch that calls
fundFinancialAccount with idempotencyKey: invoiceId) to log the error via
log(...) and then rethrow the original error (or explicitly enqueue a QStash
retry) instead of setting skipStablecoinPayouts to true so QStash's native retry
mechanism can handle the failure.

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.

1 participant