Skip to content

Commit 8252b15

Browse files
fix(api): keep provider message and map more billing errors
- pass the provider's message through for resource-missing errors so a missing coupon or payment method isn't reported as an unlinked account - map customer not-found to not_found instead of internal - name the already-subscribed checkout error and map it to already_exists - map product and feature not-found to not_found Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent e0788f5 commit 8252b15

4 files changed

Lines changed: 55 additions & 8 deletions

File tree

billing/checkout/checkout.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,12 @@ func (s State) String() string {
2020
}
2121

2222
var (
23-
ErrNotFound = errors.New("checkout not found")
24-
ErrInvalidUUID = errors.New("invalid syntax of uuid")
25-
ErrInvalidID = errors.New("invalid checkout id")
26-
ErrInvalidDetail = errors.New("invalid checkout detail")
27-
ErrKycCompleted = errors.New("organization kyc completed")
23+
ErrNotFound = errors.New("checkout not found")
24+
ErrInvalidUUID = errors.New("invalid syntax of uuid")
25+
ErrInvalidID = errors.New("invalid checkout id")
26+
ErrInvalidDetail = errors.New("invalid checkout detail")
27+
ErrKycCompleted = errors.New("organization kyc completed")
28+
ErrAlreadySubscribed = errors.New("already subscribed to the plan")
2829
)
2930

3031
type Checkout struct {

billing/checkout/service.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ func (s *Service) Create(ctx context.Context, ch Checkout) (Checkout, error) {
248248
if subID, err := s.checkIfAlreadySubscribed(ctx, ch); err != nil {
249249
return Checkout{}, err
250250
} else if subID != "" {
251-
return Checkout{}, fmt.Errorf("already subscribed to the plan")
251+
return Checkout{}, ErrAlreadySubscribed
252252
}
253253

254254
// create subscription items
@@ -894,7 +894,7 @@ func (s *Service) Apply(ctx context.Context, ch Checkout) (*subscription.Subscri
894894
if subID, err := s.checkIfAlreadySubscribed(ctx, ch); err != nil {
895895
return nil, nil, err
896896
} else if subID != "" {
897-
return nil, nil, fmt.Errorf("already subscribed to the plan")
897+
return nil, nil, ErrAlreadySubscribed
898898
}
899899

900900
if err := s.cancelTrialingSubscription(ctx, ch.CustomerID, ch.PlanID); err != nil {

internal/api/v1beta1connect/billing_errors.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ import (
55

66
"connectrpc.com/connect"
77

8+
"github.com/raystack/frontier/billing/checkout"
89
"github.com/raystack/frontier/billing/customer"
910
billingerrors "github.com/raystack/frontier/billing/errors"
11+
"github.com/raystack/frontier/billing/product"
1012
"github.com/raystack/frontier/billing/subscription"
1113
)
1214

@@ -16,6 +18,10 @@ import (
1618
func mapBillingError(err error) *connect.Error {
1719
switch {
1820
case errors.Is(err, billingerrors.ErrProviderResourceMissing):
21+
var providerErr *billingerrors.ProviderError
22+
if errors.As(err, &providerErr) {
23+
return connect.NewError(connect.CodeFailedPrecondition, providerErr)
24+
}
1925
return connect.NewError(connect.CodeFailedPrecondition, ErrBillingProviderResourceMissing)
2026
case errors.Is(err, billingerrors.ErrPaymentFailed):
2127
var providerErr *billingerrors.ProviderError
@@ -31,6 +37,14 @@ func mapBillingError(err error) *connect.Error {
3137
return connect.NewError(connect.CodeFailedPrecondition, subscription.ErrPhaseIsUpdating)
3238
case errors.Is(err, customer.ErrExistingAccountWithPendingDues):
3339
return connect.NewError(connect.CodeFailedPrecondition, customer.ErrExistingAccountWithPendingDues)
40+
case errors.Is(err, customer.ErrNotFound):
41+
return connect.NewError(connect.CodeNotFound, ErrCustomerNotFound)
42+
case errors.Is(err, checkout.ErrAlreadySubscribed):
43+
return connect.NewError(connect.CodeAlreadyExists, checkout.ErrAlreadySubscribed)
44+
case errors.Is(err, product.ErrProductNotFound):
45+
return connect.NewError(connect.CodeNotFound, product.ErrProductNotFound)
46+
case errors.Is(err, product.ErrFeatureNotFound):
47+
return connect.NewError(connect.CodeNotFound, product.ErrFeatureNotFound)
3448
default:
3549
return connect.NewError(connect.CodeInternal, err)
3650
}

internal/api/v1beta1connect/billing_errors_test.go

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ import (
99
"github.com/stretchr/testify/assert"
1010
stripe "github.com/stripe/stripe-go/v79"
1111

12+
"github.com/raystack/frontier/billing/checkout"
1213
"github.com/raystack/frontier/billing/customer"
1314
billingerrors "github.com/raystack/frontier/billing/errors"
15+
"github.com/raystack/frontier/billing/product"
1416
"github.com/raystack/frontier/billing/subscription"
1517
)
1618

@@ -34,9 +36,15 @@ func TestMapBillingError(t *testing.T) {
3436
wantMsg string
3537
}{
3638
{
37-
name: "provider resource missing",
39+
name: "provider resource missing keeps provider message",
3840
err: fmt.Errorf("GetUpcomingInvoice: org_id=abc: %w", deadCustomer),
3941
wantCode: connect.CodeFailedPrecondition,
42+
wantMsg: "record no longer exists on the billing provider: No such customer: 'cus_123'",
43+
},
44+
{
45+
name: "provider resource missing without provider error",
46+
err: fmt.Errorf("GetUpcomingInvoice: %w", billingerrors.ErrProviderResourceMissing),
47+
wantCode: connect.CodeFailedPrecondition,
4048
wantMsg: ErrBillingProviderResourceMissing.Error(),
4149
},
4250
{
@@ -69,6 +77,30 @@ func TestMapBillingError(t *testing.T) {
6977
wantCode: connect.CodeFailedPrecondition,
7078
wantMsg: customer.ErrExistingAccountWithPendingDues.Error(),
7179
},
80+
{
81+
name: "customer not found",
82+
err: fmt.Errorf("CreateCheckout.GetBillingAccountFromOrgID: org_id=abc: %w", customer.ErrNotFound),
83+
wantCode: connect.CodeNotFound,
84+
wantMsg: ErrCustomerNotFound.Error(),
85+
},
86+
{
87+
name: "already subscribed to the plan",
88+
err: fmt.Errorf("CreateCheckout.Create: %w", checkout.ErrAlreadySubscribed),
89+
wantCode: connect.CodeAlreadyExists,
90+
wantMsg: checkout.ErrAlreadySubscribed.Error(),
91+
},
92+
{
93+
name: "product not found",
94+
err: fmt.Errorf("GetProduct.GetByID: product_id=abc: %w", product.ErrProductNotFound),
95+
wantCode: connect.CodeNotFound,
96+
wantMsg: product.ErrProductNotFound.Error(),
97+
},
98+
{
99+
name: "feature not found",
100+
err: fmt.Errorf("CheckFeatureEntitlement: feature=abc: %w", product.ErrFeatureNotFound),
101+
wantCode: connect.CodeNotFound,
102+
wantMsg: product.ErrFeatureNotFound.Error(),
103+
},
72104
{
73105
name: "unknown error stays internal",
74106
err: fmt.Errorf("GetBillingAccount: %w", errors.New("db down")),

0 commit comments

Comments
 (0)