-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Move paid.paid webhook event to use queue
#3197
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughThis PR refactors Stripe payout completion processing from synchronous webhook handling to asynchronous QStash-based queue processing. The webhook now enqueues payout events to a cron endpoint, which performs database updates and sends notifications. Changes
Sequence DiagramsequenceDiagram
actor Stripe
participant Webhook as Webhook Handler<br/>(payout-paid)
participant QStash as QStash Queue
participant Cron as Cron Handler<br/>(payout-paid)
participant DB as Database
participant Email as Email Service
Stripe->>Webhook: POST /api/stripe/connect/webhook/payout-paid
activate Webhook
Webhook->>QStash: Enqueue POST to /api/cron/payouts/payout-paid<br/>(stripeAccount, stripePayout)
QStash-->>Webhook: messageId
Webhook-->>Stripe: 200 OK
deactivate Webhook
QStash->>Cron: POST /api/cron/payouts/payout-paid<br/>(payload + signature)
activate Cron
Cron->>Cron: Validate Qstash signature
Cron->>DB: Lookup partner by stripeConnectId
alt Partner found
Cron->>DB: Update payout records<br/>(status: sent → completed)
alt Partner has email
Cron->>Email: Send notification email<br/>(payout template)
Email-->>Cron: Email sent
end
Cron-->>QStash: 200 OK (summary)
else Partner not found
Cron-->>QStash: Error response
end
deactivate Cron
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes
Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
apps/web/app/(ee)/api/cron/payouts/payout-paid/route.ts(1 hunks)apps/web/app/(ee)/api/stripe/connect/webhook/payout-paid.ts(1 hunks)
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
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.
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/payout-paid/route.tsapps/web/app/(ee)/api/stripe/connect/webhook/payout-paid.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/stripe/connect/webhook/payout-paid.ts
📚 Learning: 2025-06-25T21:20:59.837Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 0
File: :0-0
Timestamp: 2025-06-25T21:20:59.837Z
Learning: In the Dub codebase, payout limit validation uses a two-stage pattern: server actions perform quick sanity checks (payoutsUsage > payoutsLimit) for immediate user feedback, while the cron job (/cron/payouts) performs authoritative validation (payoutsUsage + payoutAmount > payoutsLimit) with actual calculated amounts before processing. This design provides fast user feedback while ensuring accurate limit enforcement at transaction time.
Applied to files:
apps/web/app/(ee)/api/stripe/connect/webhook/payout-paid.ts
🧬 Code graph analysis (1)
apps/web/app/(ee)/api/cron/payouts/payout-paid/route.ts (5)
packages/prisma/index.ts (1)
prisma(3-9)packages/email/src/index.ts (1)
sendEmail(6-29)packages/utils/src/functions/currency-formatter.ts (1)
currencyFormatter(7-22)packages/email/src/templates/partner-payout-withdrawal-completed.tsx (1)
PartnerPayoutWithdrawalCompleted(18-112)apps/web/lib/api/errors.ts (1)
handleAndReturnErrorResponse(162-165)
⏰ 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 (8)
apps/web/app/(ee)/api/stripe/connect/webhook/payout-paid.ts (3)
1-7: LGTM! Queue setup looks correct.The queue initialization and imports are properly configured. Using
APP_DOMAIN_WITH_NGROKenables local development testing with ngrok, which is a good practice.
19-33: Good use of deduplication with event.id.The enqueue logic properly uses
event.idas thededuplicationId, which ensures that duplicate Stripe webhook events won't result in duplicate processing. The payload structure is clean and contains all necessary fields.Note: The absence of error handling here means that if
enqueueJSONfails, the webhook handler will throw an error, causing Stripe to retry the webhook. This is typically the desired behavior for webhook reliability.
35-35: Clear return message for webhook acknowledgment.The return message appropriately indicates the job was enqueued and includes the
messageIdfor tracking.apps/web/app/(ee)/api/cron/payouts/payout-paid/route.ts (5)
1-9: Imports look comprehensive.All necessary dependencies are imported for handling the cron job processing.
10-19: Payload schema correctly matches webhook payload.The Zod schema accurately reflects the structure sent by the webhook handler, including the nullable
traceIdfield.
22-44: Proper authentication and validation flow.The handler correctly verifies the QStash signature before processing, ensuring only legitimate requests from QStash are accepted. The partner lookup is efficient, selecting only the needed field, and gracefully handles missing partners.
78-89: Error handling looks solid.The error handling properly logs failures and returns appropriate error responses using the shared error utility. The success message includes useful debugging information.
46-55: TheupdateManyquery correctly uses bothstripePayoutIdandstripePayoutTraceIdas a composite matching key. This design is intentional:traceIdcan be null (per the Stripe webhook), and Prisma correctly handles IS NULL matching in such cases. The combination of both fields ensures accurate payout matching even though each field individually is nullable. No changes needed.
This reverts commit b107c4d.
Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.