Skip to content

fix(stripe): report declined payments to bouncer - #8460

Merged
iscekic merged 4 commits into
mainfrom
fix/bouncer-report-payment-intent-failed
Oct 21, 2026
Merged

iscekic merged 4 commits into
mainfrom
fix/bouncer-report-payment-intent-failed

Conversation

@iscekic

@iscekic iscekic commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

Bouncer's card_testing rule counts charge.failed credit events. Stripe reports a declined card attempt to the webhook as payment_intent.payment_failed, and the handler read neither that event nor the charge behind it, so the rule never saw a failure.

Change

  • Handle payment_intent.payment_failed and enqueue a charge.failed credit event.
  • Resolve the payer from the payment intent metadata, then its Stripe customer, as the other webhook outcomes do.
  • Read the card fingerprint from the attempt's charge, or from its payment method when no charge exists.
  • Skip authentication failures on both events, from the intent error code and the charge failure code or outcome reason. A retried 3DS challenge is not a declined card, and counting it would reach bouncer's card-testing threshold for a paying customer.
  • Key both decline events on the charge id, so one attempt counts once when Stripe sends both payment_intent.payment_failed and charge.failed.

Verification

  • Focused Jest suite apps/web/src/lib/stripe/index.test.ts: a declined payment intent reports the payer and card, both events for one attempt produce one row, an attempt with no charge keeps its own key, and an unresolved payer enqueues nothing.

Bouncer's card_testing rule counts charge.failed credit events. Stripe
reports a declined card attempt as payment_intent.payment_failed, which
the webhook ignored, so the rule never saw a failure.

Report a declined attempt as one charge.failed event keyed on its charge
id, so a second event for the same attempt counts once, and read the card
fingerprint from that charge.
Comment thread apps/web/src/lib/stripe/index.ts Resolved
Comment thread apps/web/src/lib/stripe/index.ts Resolved
@kilo-code-bot

kilo-code-bot Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Executive Summary

The incremental commit only extends the charge-authentication table test with the outcome.reason: 'authentication_failed' case; the case uses a stable id suffix and the suite's beforeEach cleanup keeps each case isolated, and all earlier findings on the Stripe handler are verified resolved at 2ad479e.

Assumptions: [INFERENCE] The webhook endpoint's Stripe-side event subscription is not defined in this repository, so I could not verify that payment_intent.payment_failed is enabled on the deployed endpoint; without that subscription the new handler never runs.

Files Reviewed (2 files)
  • apps/web/src/lib/stripe/index.test.ts - 0 new issues (incremental review; the previously raised coverage gap on the authentication_failed value is now fixed)
  • apps/web/src/lib/stripe/index.ts - 0 new issues (unchanged since the previous review; targeted re-check confirms the charge.failed authentication guard at line 1938 and the fingerprintSource fix at lines 1962-1975)
Previous Review Summaries (3 snapshots, latest commit b6e0687)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit b6e0687)

Status: 1 Issue Found | Recommendation: Address before merge

Executive Summary

The incremental change correctly closes the charge.failed gap for authentication-failed attempts; the only remaining gap is that the newly added authentication_failed charge outcome reason is never exercised by the tests.

Assumptions: [INFERENCE] I could not verify Stripe's current Charge.outcome.reason and failure_code value lists offline, so whether authentication_failed (rather than another string) is what Stripe actually sends for a failed 3DS challenge is taken from the PR description. The earlier WARNING on the charge.failed path is fixed at apps/web/src/lib/stripe/index.ts:1938. The earlier WARNING about the charge-keyed dedupe losing a fingerprint was answered as an accepted trade-off and its comment is outdated in GitHub state; it is not carried forward.

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 1
Issue Details (click to expand)

SUGGESTION

File Line Issue
apps/web/src/lib/stripe/index.test.ts 5024 Both charge-authentication cases use authentication_required, which the code table already accepted, so the new authentication_failed key is untested.
Files Reviewed (2 files)
  • apps/web/src/lib/stripe/index.ts - 0 new issues (previous charge.failed authentication gap verified fixed)
  • apps/web/src/lib/stripe/index.test.ts - 1 issue

Fix these issues in Kilo Cloud

Previous review (commit 71dd91d)

Status: 1 Issue Found | Recommendation: Address before merge

Executive Summary

The new authentication-failure filter applies only to payment_intent.payment_failed, so the same 3DS attempt delivered as charge.failed still reaches bouncer's card_testing count.

Assumptions: [INFERENCE] I could not execute the Stripe integration to observe whether Stripe also emits charge.failed for an authentication-failed attempt; the PR's own dedupe design assumes both events are sent for one attempt, and the charge.failed branch has no authentication-failure guard.

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
apps/web/src/lib/stripe/index.ts 1943 The authentication-failure guard covers only payment_intent.payment_failed; the charge.failed branch for the same attempt still reports it, bypassing the guard.
Files Reviewed (2 files)
  • apps/web/src/lib/stripe/index.ts - 1 issue
  • apps/web/src/lib/stripe/index.test.ts - 0 issues

Fix these issues in Kilo Cloud

Previous review (commit 6858725)

Status: 2 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 1
Issue Details (click to expand)

WARNING

File Line Issue
apps/web/src/lib/stripe/index.ts 350 Charge-keyed dedupe keeps the first write, so a failed best-effort fingerprint lookup on a payment_intent.payment_failed permanently drops the fingerprint that the later charge.failed event would have supplied.

SUGGESTION

File Line Issue
apps/web/src/lib/stripe/index.ts 1888 charge.failed passes the event's own charge as the fingerprint fallback, adding a redundant Stripe API round trip for a value the event already carries (and for every non-card payment method).

Fix these issues in Kilo Cloud

Files Reviewed (2 files)
  • apps/web/src/lib/stripe/index.ts - 2 issues
  • apps/web/src/lib/stripe/index.test.ts - 0 issues

Reviewed by deepseek-flash-latest · Input: 43.4K · Output: 4K · Cached: 420.2K

Review guidance: REVIEW.md from base branch main

Skip a payment intent that failed authentication: a retried 3DS
challenge is not a declined card, and counting it would reach bouncer's
card-testing threshold for a paying customer.

Read a missing fingerprint from the intent's charge, then its payment
method. A charge.failed payload already is the charge, so it no longer
re-reads itself.
Comment thread apps/web/src/lib/stripe/index.ts Resolved
A charge that failed an authentication challenge is the same attempt a
payment intent reports. Checking the charge's failure code and outcome
reason as well keeps a retried 3DS challenge out of bouncer's
card-testing count on both paths.
Comment thread apps/web/src/lib/stripe/index.test.ts Resolved
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