Skip to content

feat(billing): add stripe error translator - #1849

Merged
whoAbhishekSah merged 3 commits into
mainfrom
fix/stripe-error-translator
Aug 5, 2026
Merged

feat(billing): add stripe error translator#1849
whoAbhishekSah merged 3 commits into
mainfrom
fix/stripe-error-translator

Conversation

@whoAbhishekSah

@whoAbhishekSah whoAbhishekSah commented Aug 5, 2026

Copy link
Copy Markdown
Member

What

Adds a billing/errors package (imported as billingerrors, same convention as core/userpat/errors) with three typed errors and one function, TranslateStripeError, that classifies a stripe error into them:

  • resource_missingErrProviderResourceMissing (the record is gone on Stripe)
  • card errors and decline codes → ErrPaymentFailed
  • rate limits, stripe server errors, HTTP 429/5xx → ErrProviderUnavailable

Anything else is returned unchanged.

The translated error is a ProviderError. Its message keeps Stripe's human-readable text (stripe.Error.Msg) instead of the raw JSON blob that stripe.Error.Error() prints. The original error stays in the chain, so errors.As can still reach the stripe.Error when needed.

Why

Almost every billing failure today reaches the caller as a bare "internal server error", even when the problem is the state of their billing account (see #1836). This translator is the first layer of the fix: it gives the services and handlers a typed error they can act on.

Part of #1836. Stack: this PR → services adopt the translator → handlers map the errors to proper codes.

🤖 Generated with Claude Code

@vercel

vercel Bot commented Aug 5, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
frontier Ready Ready Preview Aug 5, 2026 11:41am

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added clearer billing error classification for missing resources, payment failures, and provider outages.
    • Preserves payment-provider messages and request IDs for improved troubleshooting.
    • Recognizes rate-limit, API, server, and network failures.
    • Supports matching translated errors through wrapped error chains.
  • Bug Fixes

    • Prevents unrelated, unknown, canceled, or empty errors from being incorrectly classified.
    • Keeps distinct billing error types separate for more accurate handling.

Walkthrough

The billing errors package adds typed provider error classifications, Stripe and network error translation, provider metadata preservation, multi-error unwrapping, and comprehensive translation tests.

Changes

Stripe error translation

Layer / File(s) Summary
Provider error contract
billing/errors/errors.go
Adds sentinel classifications and ProviderError with formatted messages, request IDs, and multi-error unwrapping.
Stripe classification and validation
billing/errors/errors.go, billing/errors/errors_test.go
Translates recognized Stripe and network errors, preserves messages and causes, passes through unrelated errors, and tests classification, wrapping, request IDs, and error-kind separation.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related issues

  • Issue 1836: The pull request implements the Stripe error translator and typed provider errors described by this issue.
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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 info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 82a368c6-6d6a-499a-a7ee-c5424a35fde6

📥 Commits

Reviewing files that changed from the base of the PR and between 555c089 and 1020777.

📒 Files selected for processing (2)
  • billing/stripe_errors.go
  • billing/stripe_errors_test.go

Comment thread billing/errors/errors_test.go Outdated
@coveralls

Copy link
Copy Markdown

Coverage Report for CI Build 30974209276

Coverage increased (+0.03%) to 47.559%

Details

  • Coverage increased (+0.03%) from the base build.
  • Patch coverage: 24 of 24 lines across 1 file are fully covered (100%).
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 39383
Covered Lines: 18730
Line Coverage: 47.56%
Coverage Strength: 15.4 hits per line

💛 - Coveralls

@whoAbhishekSah

whoAbhishekSah commented Aug 5, 2026

Copy link
Copy Markdown
Member Author

Tested the full stack (#1849 translator -> #1850 service translation -> #1851 handler mapper) live against a local Frontier with real Stripe test mode. Fresh org owned by a normal user; org-level calls made with that user's session, admin calls with a platform admin. Note: DelegatedCheckout is an AdminService RPC — the caller in those rows is a platform admin or internal automation, not the end user.

New error codes, all verified live (every one of these was internal before the stack):

Trigger Result What the caller can do now
Stripe customer deleted, then UpdateBillingAccount / CreateCheckout (setup + subscription) / DelegatedCheckout (admin) / GetUpcomingInvoice / GetBillingAccount with payment methods failed_precondition Stop retrying (it will never succeed). Show "billing account is unlinked" instead of a generic error. The org admin can delete and recreate the billing account, or contact support. Before: retries and a dead-end error toast.
DelegatedCheckout (admin) with no payment method on a paid plan failed_precondition The admin or automation driving the checkout learns the org has no card on file and can ask the org to add a payment method (checkout setup session) before granting the plan.
DelegatedCheckout (admin) with a coupon id that does not exist on Stripe failed_precondition with the Stripe message ("No such coupon") The admin sees the bad coupon id in the error and fixes the request — no log-diving needed.
CancelSubscription / cancel upcoming phase when the Stripe subscription is gone failed_precondition "subscription no longer exists on the billing provider" Treat the subscription as dead: stop retrying, re-subscribe or contact support.
New billing account while the previous one has a negative credit balance failed_precondition "existing account with pending dues found" Show the user the real blocker: clear the dues first, then create the account.
DeleteBillingAccount when the Stripe customer is already deleted still succeeds (tolerance kept) Cleanup and org-delete flows don't get stuck on a half-deleted account.

The common thread: internal tells a client "server bug, retry later" — wrong on both counts for these cases, and the interceptor masks the message so the caller learns nothing. failed_precondition says "your request was fine, the account state is not; retrying won't help, do X first", which is what all of these are.

ErrPaymentFailed and ErrProviderUnavailable cannot be raised synchronously against real Stripe (subscriptions are created with allow_incomplete, and Stripe 5xx/rate limits can't be forced), so those two are covered by the unit tests only. Same for ErrPhaseIsUpdating, which only occurs in a race at a phase boundary.

Happy paths regressed nothing: account create/get/list/update, balance, checkout sessions (real Stripe URLs), delegated subscription (went active, card charged), plan change with scheduled phase, cancel upcoming phase, cancel subscription, upcoming invoice, HasTrialed. Unchanged codes confirmed: not_found, unauthenticated, permission_denied for a non-member, active-account conflict.

Testing also surfaced four cases that still returned internal or a misleading message. Fixed on #1851 (commit 8252b15):

Case Now What the caller can do now
Missing coupon / payment method reported as "billing account is no longer linked" provider's own message passes through See the actual cause instead of a wrong diagnosis.
CreateCheckout on an org with no billing account not_found "customer doesn't exist" Create the billing account first, then check out.
DelegatedCheckout (admin) for a plan the org is already on already_exists The admin or automation treats it as done / shows "already subscribed"; no support ticket for a non-error.
GetProduct / CheckFeatureEntitlement with unknown id not_found Fix the product/feature name in the request.

All re-verified live after the fix.

@whoAbhishekSah

Copy link
Copy Markdown
Member Author

One follow-up on the test report above (commit 8709ad9 on #1851): provider messages passed through to the caller now mask provider-generated object ids. A deleted customer reads as "No such customer: 'cus_*****'" instead of showing the real Stripe customer id. Caller-supplied values don't match the id shape, so "No such coupon: 'SUMMER20'" is unchanged. Covered by unit tests on the mapper.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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 info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: a52c06a8-3382-41ea-97ea-bb029f124b94

📥 Commits

Reviewing files that changed from the base of the PR and between 555c089 and 65732ea.

📒 Files selected for processing (2)
  • billing/errors/errors.go
  • billing/errors/errors_test.go

Comment thread billing/errors/errors.go

@rohilsurana rohilsurana left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Reviewed the whole stack (#1849 -> #1850 -> #1851). This one looks good. Small package with one clear job, and the table tests cover the classification well, including wrapped errors and keeping the kinds apart.

Notes:

  • Package name errors with the billingerrors import alias matches the core/userpat/errors convention. Works for me.
  • Classification precedence looks right: resource missing first, then card/decline, then availability. I checked stripe-go v79: it has no rate limit error type, so matching on the code plus HTTP 429/5xx is the right way with this SDK.

A few small comments inline.

Comment thread billing/errors/errors.go Outdated
Comment thread billing/errors/errors.go
Comment thread billing/errors/errors.go Outdated

@rohilsurana rohilsurana left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM

whoAbhishekSah and others added 3 commits August 5, 2026 17:09
Add a billing/errors package that classifies stripe errors into typed
provider errors: resource_missing means the record is gone on the
provider, card errors mean the payment failed, and rate limits or
stripe outages mean the provider is unavailable. The translated error
keeps stripe's human-readable message and the original error chain.

Part of #1836.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review fixes: connection failures and timeouts reaching stripe now
translate to provider-unavailable (a canceled request stays as is),
the stripe request id is kept on ProviderError for support lookups,
and Unwrap no longer returns a slice with a nil entry when the error
was built without a cause.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@whoAbhishekSah
whoAbhishekSah force-pushed the fix/stripe-error-translator branch from 9722125 to eaeb365 Compare August 5, 2026 11:40
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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 info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c4f368ca-f379-462a-ba5f-b33e7c7d2205

📥 Commits

Reviewing files that changed from the base of the PR and between 6fc4b65 and eaeb365.

📒 Files selected for processing (2)
  • billing/errors/errors.go
  • billing/errors/errors_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • billing/errors/errors.go

Comment thread billing/errors/errors_test.go
@whoAbhishekSah
whoAbhishekSah merged commit 4b1b56a into main Aug 5, 2026
8 checks passed
@whoAbhishekSah
whoAbhishekSah deleted the fix/stripe-error-translator branch August 5, 2026 11:50
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.

3 participants