From 5ade408dfe131971778fc0d019e19e3a63255a6b Mon Sep 17 00:00:00 2001 From: Abhishek Sah Date: Wed, 5 Aug 2026 09:33:12 +0530 Subject: [PATCH] fix(billing): translate stripe errors in billing services Wrap every stripe call in the customer, subscription, checkout, product, and invoice services with the stripe error translator, so a typed provider error reaches the caller instead of a raw stripe error. The old type-assertion checks now use errors.Is and errors.As, which also work on wrapped errors. Product and invoice calls that returned bare errors now carry context. The payment method list iterator error, previously dropped, is now checked. Part of #1836. Co-Authored-By: Claude Fable 5 --- billing/checkout/service.go | 15 ++++++------ billing/customer/service.go | 43 +++++++++++++-------------------- billing/invoice/service.go | 17 ++++++++----- billing/product/service.go | 11 +++++---- billing/subscription/service.go | 21 ++++++++-------- 5 files changed, 53 insertions(+), 54 deletions(-) diff --git a/billing/checkout/service.go b/billing/checkout/service.go index ac05b7a6ec..3ab5ca5b1d 100644 --- a/billing/checkout/service.go +++ b/billing/checkout/service.go @@ -13,6 +13,7 @@ import ( "github.com/stripe/stripe-go/v79" "github.com/raystack/frontier/billing" + billingerrors "github.com/raystack/frontier/billing/errors" "github.com/raystack/frontier/internal/metrics" "github.com/raystack/frontier/pkg/metadata" @@ -350,7 +351,7 @@ func (s *Service) Create(ctx context.Context, ch Checkout) (Checkout, error) { PaymentMethodCollection: stripe.String(string(stripe.PaymentLinkPaymentMethodCollectionIfRequired)), }) if err != nil { - return Checkout{}, fmt.Errorf("failed to create subscription at billing provider: %w", err) + return Checkout{}, fmt.Errorf("failed to create subscription at billing provider: %w", billingerrors.TranslateStripeError(err)) } return s.repository.Create(ctx, Checkout{ @@ -481,7 +482,7 @@ func (s *Service) Create(ctx context.Context, ch Checkout) (Checkout, error) { }, }) if err != nil { - return Checkout{}, fmt.Errorf("failed to buy product at billing provider: %w", err) + return Checkout{}, fmt.Errorf("failed to buy product at billing provider: %w", billingerrors.TranslateStripeError(err)) } return s.repository.Create(ctx, Checkout{ @@ -559,7 +560,7 @@ func (s *Service) SyncWithProvider(ctx context.Context, customerID string) error }, }) if err != nil { - errs = append(errs, fmt.Errorf("failed to get checkout session from billing provider: %w", err)) + errs = append(errs, fmt.Errorf("failed to get checkout session from billing provider: %w", billingerrors.TranslateStripeError(err))) continue } if ch.PaymentStatus != string(checkoutSession.PaymentStatus) { @@ -735,7 +736,7 @@ func (s *Service) ensureSubscription(ctx context.Context, ch Checkout) (string, }, }) if err != nil { - return "", fmt.Errorf("failed to get subscription from billing provider: %w", err) + return "", fmt.Errorf("failed to get subscription from billing provider: %w", billingerrors.TranslateStripeError(err)) } // create subscription @@ -802,7 +803,7 @@ func (s *Service) CreateSessionForPaymentMethod(ctx context.Context, ch Checkout }, }) if err != nil { - return Checkout{}, fmt.Errorf("failed to create checkout at billing provider: %w", err) + return Checkout{}, fmt.Errorf("failed to create checkout at billing provider: %w", billingerrors.TranslateStripeError(err)) } return s.repository.Create(ctx, Checkout{ @@ -844,7 +845,7 @@ func (s *Service) CreateSessionForCustomerPortal(ctx context.Context, ch Checkou session, err := s.stripeClient.BillingPortalSessions.New(sessionParams) if err != nil { - return Checkout{}, fmt.Errorf("failed to create session for customer portal: %w", err) + return Checkout{}, fmt.Errorf("failed to create session for customer portal: %w", billingerrors.TranslateStripeError(err)) } return Checkout{ @@ -989,7 +990,7 @@ func (s *Service) Apply(ctx context.Context, ch Checkout) (*subscription.Subscri Coupon: couponID, }) if err != nil { - return nil, nil, fmt.Errorf("failed to create subscription at billing provider: %w", err) + return nil, nil, fmt.Errorf("failed to create subscription at billing provider: %w", billingerrors.TranslateStripeError(err)) } // register subscription in frontier diff --git a/billing/customer/service.go b/billing/customer/service.go index 143c26a7a3..133af7db81 100644 --- a/billing/customer/service.go +++ b/billing/customer/service.go @@ -2,6 +2,7 @@ package customer import ( "context" + "errors" "fmt" "log/slog" "math/rand" @@ -14,6 +15,7 @@ import ( "github.com/stripe/stripe-go/v79" "github.com/raystack/frontier/billing" + billingerrors "github.com/raystack/frontier/billing/errors" "github.com/raystack/frontier/internal/metrics" "slices" @@ -134,14 +136,11 @@ func (s *Service) RegisterToProvider(ctx context.Context, customer Customer) (*s TestClock: customer.StripeTestClockID, }) if err != nil { - if stripeErr, ok := err.(*stripe.Error); ok { - switch stripeErr.Code { - case stripe.ErrorCodeParameterMissing: - // stripe error - return nil, fmt.Errorf("missing parameter while registering to biller: %s", stripeErr.Error()) - } + var stripeErr *stripe.Error + if errors.As(err, &stripeErr) && stripeErr.Code == stripe.ErrorCodeParameterMissing { + return nil, fmt.Errorf("missing parameter while registering to biller: %s: %w", stripeErr.Msg, err) } - return nil, fmt.Errorf("failed to register in billing provider: %w", err) + return nil, fmt.Errorf("failed to register in billing provider: %w", billingerrors.TranslateStripeError(err)) } return stripeCustomer, nil @@ -194,14 +193,11 @@ func (s *Service) Update(ctx context.Context, customer Customer) (Customer, erro }, }) if err != nil { - if stripeErr, ok := err.(*stripe.Error); ok { - switch stripeErr.Code { - case stripe.ErrorCodeParameterMissing: - // stripe error - return Customer{}, fmt.Errorf("missing parameter while registering to biller: %s", stripeErr.Error()) - } + var stripeErr *stripe.Error + if errors.As(err, &stripeErr) && stripeErr.Code == stripe.ErrorCodeParameterMissing { + return Customer{}, fmt.Errorf("missing parameter while registering to biller: %s: %w", stripeErr.Msg, err) } - return Customer{}, fmt.Errorf("failed to register in billing provider: %w", err) + return Customer{}, fmt.Errorf("failed to register in billing provider: %w", billingerrors.TranslateStripeError(err)) } customer.ProviderID = stripeCustomer.ID return s.repository.UpdateByID(ctx, customer) @@ -286,17 +282,9 @@ func (s *Service) Delete(ctx context.Context, id string) error { Context: ctx, }, }); err != nil { - var throw = true - // Try to safely cast a generic error to a stripe.Error so that we can get at - // some additional Stripe-specific information about what went wrong. - if stripeErr, ok := err.(*stripe.Error); ok { - // The Code field will contain a basic identifier for the failure. - if stripeErr.Code == stripe.ErrorCodeResourceMissing { - // it's ok if the customer is already deleted - throw = false - } - } - if throw { + err = billingerrors.TranslateStripeError(err) + // it's ok if the customer is already deleted + if !errors.Is(err, billingerrors.ErrProviderResourceMissing) { return fmt.Errorf("failed to delete customer from billing provider: %w", err) } } @@ -352,6 +340,9 @@ func (s *Service) ListPaymentMethods(ctx context.Context, id string) ([]PaymentM paymentMethods = append(paymentMethods, pm) } + if err := stripePaymentMethodItr.Err(); err != nil { + return nil, fmt.Errorf("failed to list payment methods from billing provider: %w", billingerrors.TranslateStripeError(err)) + } return paymentMethods, nil } @@ -432,7 +423,7 @@ func (s *Service) SyncWithProvider(ctx context.Context, customr Customer) error }, }) if err != nil { - return fmt.Errorf("failed to get customer from billing provider: %w", err) + return fmt.Errorf("failed to get customer from billing provider: %w", billingerrors.TranslateStripeError(err)) } var shouldUpdate bool diff --git a/billing/invoice/service.go b/billing/invoice/service.go index 341fe92979..14c72b08a9 100644 --- a/billing/invoice/service.go +++ b/billing/invoice/service.go @@ -20,6 +20,7 @@ import ( "github.com/stripe/stripe-go/v79" "github.com/raystack/frontier/billing" + billingerrors "github.com/raystack/frontier/billing/errors" "github.com/raystack/frontier/internal/metrics" "github.com/raystack/frontier/billing/customer" @@ -287,7 +288,7 @@ func (s *Service) SyncWithProvider(ctx context.Context, customr customer.Custome return errors.Join(errs...) } if err := stripeInvoices.Err(); err != nil { - return fmt.Errorf("failed to list invoices: %w", err) + return fmt.Errorf("failed to list invoices: %w", billingerrors.TranslateStripeError(err)) } return nil } @@ -361,7 +362,7 @@ func (s *Service) GetUpcoming(ctx context.Context, customerID string) (Invoice, s.log.DebugContext(ctx, "no upcoming invoice", "error", stripeErr) return Invoice{}, nil } - return Invoice{}, fmt.Errorf("failed to get upcoming invoice: %w", err) + return Invoice{}, fmt.Errorf("failed to get upcoming invoice: %w", billingerrors.TranslateStripeError(err)) } return stripeInvoiceToInvoice(customerID, stripeInvoice), nil @@ -693,7 +694,7 @@ func (s *Service) CreateInProvider(ctx context.Context, custmr customer.Customer }, }) if err != nil { - return nil, fmt.Errorf("failed to create invoice: %w", err) + return nil, fmt.Errorf("failed to create invoice: %w", billingerrors.TranslateStripeError(err)) } // create line item for the invoice @@ -726,12 +727,12 @@ func (s *Service) CreateInProvider(ctx context.Context, custmr customer.Customer Period: itemPeriod, }) if err != nil { - return nil, fmt.Errorf("failed to create invoice item: %w", err) + return nil, fmt.Errorf("failed to create invoice item: %w", billingerrors.TranslateStripeError(err)) } } // fetch updated stripe invoice - return s.stripeClient.Invoices.Get(stripeInvoice.ID, &stripe.InvoiceParams{ + updatedInvoice, err := s.stripeClient.Invoices.Get(stripeInvoice.ID, &stripe.InvoiceParams{ Params: stripe.Params{ Context: ctx, }, @@ -739,6 +740,10 @@ func (s *Service) CreateInProvider(ctx context.Context, custmr customer.Customer new("lines"), }, }) + if err != nil { + return nil, fmt.Errorf("failed to get invoice from billing provider: %w", billingerrors.TranslateStripeError(err)) + } + return updatedInvoice, nil } // Reconcile checks all paid invoices and reconciles them with the system. @@ -835,7 +840,7 @@ func (s *Service) reconcileCreditInvoice(ctx context.Context, inv Invoice) error func (s *Service) TriggerSyncByProviderID(ctx context.Context, id string) error { stripeInvoice, err := s.stripeClient.Invoices.Get(id, &stripe.InvoiceParams{}) if err != nil { - return err + return fmt.Errorf("failed to get invoice from billing provider: %w", billingerrors.TranslateStripeError(err)) } customrs, err := s.customerService.List(ctx, customer.Filter{ diff --git a/billing/product/service.go b/billing/product/service.go index 439fe92d4e..cd1d4ba8c6 100644 --- a/billing/product/service.go +++ b/billing/product/service.go @@ -12,6 +12,7 @@ import ( "slices" "github.com/google/uuid" + billingerrors "github.com/raystack/frontier/billing/errors" "github.com/raystack/frontier/pkg/utils" "github.com/stripe/stripe-go/v79/client" ) @@ -85,7 +86,7 @@ func (s *Service) Create(ctx context.Context, product Product) (Product, error) }, }) if err != nil { - return Product{}, err + return Product{}, fmt.Errorf("failed to create product at billing provider: %w", billingerrors.TranslateStripeError(err)) } productOb, err := s.productRepository.Create(ctx, product) @@ -213,7 +214,7 @@ func (s *Service) Update(ctx context.Context, product Product) (Product, error) }, }) if err != nil { - return Product{}, err + return Product{}, fmt.Errorf("failed to update product at billing provider: %w", billingerrors.TranslateStripeError(err)) } // check feature updates in product @@ -405,7 +406,7 @@ func (s *Service) setPriceActive(ctx context.Context, price Price, active bool) Params: stripe.Params{Context: ctx}, Active: new(active), }); err != nil { - return err + return fmt.Errorf("failed to update price at billing provider: %w", billingerrors.TranslateStripeError(err)) } } if active { @@ -481,7 +482,7 @@ func (s *Service) CreatePrice(ctx context.Context, price Price) (Price, error) { } stripePrice, err := s.stripeClient.Prices.New(providerParams) if err != nil { - return Price{}, err + return Price{}, fmt.Errorf("failed to create price at billing provider: %w", billingerrors.TranslateStripeError(err)) } price.ProviderID = stripePrice.ID @@ -533,7 +534,7 @@ func (s *Service) UpdatePrice(ctx context.Context, price Price) (Price, error) { }, }) if err != nil { - return Price{}, err + return Price{}, fmt.Errorf("failed to update price at billing provider: %w", billingerrors.TranslateStripeError(err)) } return s.priceRepository.UpdateByID(ctx, existingPrice) diff --git a/billing/subscription/service.go b/billing/subscription/service.go index 6c9a872d8f..0b83a5824c 100644 --- a/billing/subscription/service.go +++ b/billing/subscription/service.go @@ -20,6 +20,7 @@ import ( "github.com/raystack/frontier/billing/credit" "github.com/raystack/frontier/billing" + billingerrors "github.com/raystack/frontier/billing/errors" "github.com/raystack/frontier/billing/product" "github.com/raystack/frontier/pkg/utils" @@ -330,7 +331,7 @@ func (s *Service) Cancel(ctx context.Context, id string, immediate bool) (Subscr Prorate: new(true), }) if err != nil { - return Subscription{}, fmt.Errorf("failed to cancel subscription at billing provider: %w", err) + return Subscription{}, fmt.Errorf("failed to cancel subscription at billing provider: %w", billingerrors.TranslateStripeError(err)) } sub.State = string(stripeSubscription.Status) if stripeSubscription.CanceledAt > 0 { @@ -359,7 +360,7 @@ func (s *Service) Cancel(ctx context.Context, id string, immediate bool) (Subscr EndBehavior: stripe.String(string(stripe.SubscriptionScheduleEndBehaviorCancel)), }) if err != nil { - return sub, fmt.Errorf("failed to cancel subscription schedule at billing provider: %w", err) + return sub, fmt.Errorf("failed to cancel subscription schedule at billing provider: %w", billingerrors.TranslateStripeError(err)) } sub.Phase.PlanID = "" sub.Phase.Reason = SubscriptionCancel.String() @@ -381,8 +382,8 @@ func (s *Service) createOrGetSchedule(ctx context.Context, sub Subscription) (*s }, }) if err != nil { - // check if it's a subscription not found err - if stripeErr, ok := err.(*stripe.Error); ok && stripeErr.Code == stripe.ErrorCodeResourceMissing { + err = billingerrors.TranslateStripeError(err) + if errors.Is(err, billingerrors.ErrProviderResourceMissing) { return nil, nil, ErrSubscriptionOnProviderNotFound } return nil, nil, fmt.Errorf("failed to get subscription from billing provider: %w", err) @@ -398,7 +399,7 @@ func (s *Service) createOrGetSchedule(ctx context.Context, sub Subscription) (*s }, }) if err != nil { - return nil, nil, fmt.Errorf("failed to get subscription schedule from billing provider: %w", err) + return nil, nil, fmt.Errorf("failed to get subscription schedule from billing provider: %w", billingerrors.TranslateStripeError(err)) } stripeSubscription.Schedule = schedule } @@ -421,7 +422,7 @@ func (s *Service) createOrGetSchedule(ctx context.Context, sub Subscription) (*s }, }) if err != nil { - return nil, nil, fmt.Errorf("failed to create subscription schedule at billing provider: %w", err) + return nil, nil, fmt.Errorf("failed to create subscription schedule at billing provider: %w", billingerrors.TranslateStripeError(err)) } } return stripeSubscription, stripeSubscription.Schedule, nil @@ -483,7 +484,7 @@ func (s *Service) UpdateProductQuantity(ctx context.Context, orgID string, curre PendingInvoiceItemInterval: getPendingInvoiceItemInterval(currentPlan), }) if err != nil { - return fmt.Errorf("failed to update subscription quantity at billing provider: %w", err) + return fmt.Errorf("failed to update subscription quantity at billing provider: %w", billingerrors.TranslateStripeError(err)) } } } @@ -561,7 +562,7 @@ func (s *Service) UpdateProductQuantity(ctx context.Context, orgID string, curre Phases: updatedPhases, }) if err != nil { - return fmt.Errorf("failed to update subscription schedule at billing provider: %w", err) + return fmt.Errorf("failed to update subscription schedule at billing provider: %w", billingerrors.TranslateStripeError(err)) } } @@ -773,7 +774,7 @@ func (s *Service) ChangePlan(ctx context.Context, id string, changeRequest Chang }, }) if err != nil { - return change, fmt.Errorf("failed to update subscription schedule at billing provider: %w", err) + return change, fmt.Errorf("failed to update subscription schedule at billing provider: %w", billingerrors.TranslateStripeError(err)) } // update subscription with new phase @@ -968,7 +969,7 @@ func (s *Service) CancelUpcomingPhase(ctx context.Context, sub Subscription) err }, }) if err != nil { - return fmt.Errorf("failed to update subscription schedule at billing provider: %w", err) + return fmt.Errorf("failed to update subscription schedule at billing provider: %w", billingerrors.TranslateStripeError(err)) } sub.Phase.Reason = ""