fix(console): add idempotency checks to payment webhook handlers#28403
Open
PanAchy wants to merge 5 commits into
Open
fix(console): add idempotency checks to payment webhook handlers#28403PanAchy wants to merge 5 commits into
PanAchy wants to merge 5 commits into
Conversation
Two webhook handlers unconditionally added credits on every delivery: - checkout.session.completed (manual top-up) - invoice.payment_succeeded with billing_reason=manual (auto-reload) Stripe guarantees at-least-once delivery and retries on any 5xx or timeout. Each retry would insert a duplicate PaymentTable row and increment the workspace balance again, effectively granting free credits. Fix: - Add a unique index on PaymentTable.paymentID in the schema (requires migration generation by a maintainer with infra access) - In both handlers, check if a payment with this paymentID already exists and return early if so, before any balance or DB writes
Contributor
|
The following comment was made by an LLM, it may be inaccurate: Based on my search, I found one potentially related PR: PR #28400: fix(console): guard against duplicate refund and use actual refund amount Why it might be related: This PR also addresses webhook handler safety in the billing system, specifically guarding against duplicate refunds. It shares the same theme of preventing duplicate credit operations and idempotency in webhook handlers, though it targets the All other results returned PR #28403 (the current PR), which is expected and shouldn't be flagged as a duplicate. |
This was referenced May 19, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Issue for this PR
Closes #28402
Type of change
What does this PR do?
Two webhook handlers unconditionally added credits to the workspace balance on every Stripe delivery, with no check for whether the same event had already been processed:
checkout.session.completed(manual top-up)invoice.payment_succeededwithbilling_reason=manual(auto-reload)Stripe guarantees at-least-once delivery and retries on any 5xx or timeout. Each retry would insert a duplicate
PaymentTablerow and incrementBillingTable.balanceagain, effectively granting free credits for every transient server error at webhook time.Fix — application-level idempotency (both handlers):
Before any DB writes, look up whether a
PaymentTablerow with the sameinvoiceIDexists. Return early if found.invoiceIDis used instead ofpaymentIDbecausepaymentIDis null when a coupon covers the full invoice amount — a unique index on a nullable column in MySQL allows multiple NULL rows, so it would not have prevented duplicates for coupon-only invoices.Fix — schema-level safety net (
billing.sql.ts):Added a unique index on
PaymentTable.invoice_id. This enforces idempotency at the DB level even if the application check were bypassed (e.g. race condition between two simultaneous deliveries).A minor fix is also included in the auto-reload handler:
invoice.payments?.data[0]?.paymentnow uses optional chaining to avoid a crash when the payments array is empty (e.g. coupon-covered invoice with no payment intent).How did you verify your code works?
bun typecheckin bothpackages/console/appandpackages/console/core— no errors.invoiceIDis non-null on all invoice and checkout session events. The unique index oninvoice_idis safe to add and correctly prevents duplicates including coupon-only invoices.Screenshots / recordings
If this is a UI change, please include a screenshot or recording.
Checklist
If you do not follow this template your PR will be automatically rejected.