From 113cd93c8cd01d82ffc5003dedb7529519856158 Mon Sep 17 00:00:00 2001 From: Abhishek Sah Date: Wed, 5 Aug 2026 09:36:30 +0530 Subject: [PATCH 1/5] fix(api): map billing errors to actionable codes instead of internal Billing handlers used a bare CodeInternal for every unrecognized error, so account-state problems looked like Frontier bugs to the caller. All billing handlers now fall back to a shared mapBillingError: - provider record missing -> failed_precondition, "billing account is no longer linked to the payment provider" - payment failures -> failed_precondition with the provider's message - provider rate limits and outages -> unavailable - plan change in progress, pending dues, subscription gone on the provider -> failed_precondition - everything else -> internal, as before Fixes #1836. Co-Authored-By: Claude Fable 5 --- internal/api/v1beta1connect/billing_check.go | 10 +- .../api/v1beta1connect/billing_checkout.go | 22 +-- .../api/v1beta1connect/billing_customer.go | 44 +++--- internal/api/v1beta1connect/billing_errors.go | 37 +++++ .../api/v1beta1connect/billing_errors_test.go | 86 +++++++++++ .../api/v1beta1connect/billing_invoice.go | 20 +-- internal/api/v1beta1connect/billing_plan.go | 14 +- .../api/v1beta1connect/billing_product.go | 40 +++--- .../v1beta1connect/billing_subscription.go | 14 +- internal/api/v1beta1connect/billing_usage.go | 18 +-- .../api/v1beta1connect/billing_webhook.go | 2 +- internal/api/v1beta1connect/errors.go | 133 +++++++++--------- 12 files changed, 283 insertions(+), 157 deletions(-) create mode 100644 internal/api/v1beta1connect/billing_errors.go create mode 100644 internal/api/v1beta1connect/billing_errors_test.go diff --git a/internal/api/v1beta1connect/billing_check.go b/internal/api/v1beta1connect/billing_check.go index 20e63ce7c6..5fc5c5c5f0 100644 --- a/internal/api/v1beta1connect/billing_check.go +++ b/internal/api/v1beta1connect/billing_check.go @@ -20,12 +20,12 @@ func (h *ConnectHandler) CheckFeatureEntitlement(ctx context.Context, request *c if errors.Is(err, customer.ErrInvalidUUID) || errors.Is(err, customer.ErrInvalidID) { return nil, connect.NewError(connect.CodeInvalidArgument, err) } - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("CheckFeatureEntitlement.GetByOrgID: org_id=%s: %w", request.Msg.GetOrgId(), err)) + return nil, mapBillingError(fmt.Errorf("CheckFeatureEntitlement.GetByOrgID: org_id=%s: %w", request.Msg.GetOrgId(), err)) } checkStatus, err := h.entitlementService.Check(ctx, cust.ID, request.Msg.GetFeature()) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("CheckFeatureEntitlement: billing_id=%s org_id=%s feature=%s: %w", cust.ID, request.Msg.GetOrgId(), request.Msg.GetFeature(), err)) + return nil, mapBillingError(fmt.Errorf("CheckFeatureEntitlement: billing_id=%s org_id=%s feature=%s: %w", cust.ID, request.Msg.GetOrgId(), request.Msg.GetFeature(), err)) } return connect.NewResponse(&frontierv1beta1.CheckFeatureEntitlementResponse{ @@ -38,7 +38,7 @@ func (h *ConnectHandler) CheckCreditEntitlement(ctx context.Context, request *co OrgID: request.Msg.GetOrgId(), }) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("CheckCreditEntitlement.List: org_id=%s: %w", request.Msg.GetOrgId(), err)) + return nil, mapBillingError(fmt.Errorf("CheckCreditEntitlement.List: org_id=%s: %w", request.Msg.GetOrgId(), err)) } if len(customerList) == 0 { @@ -48,12 +48,12 @@ func (h *ConnectHandler) CheckCreditEntitlement(ctx context.Context, request *co customer := customerList[0] customerDetails, err := h.customerService.GetDetails(ctx, customer.ID) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("CheckCreditEntitlement.GetDetails: customer_id=%s org_id=%s: %w", customer.ID, request.Msg.GetOrgId(), err)) + return nil, mapBillingError(fmt.Errorf("CheckCreditEntitlement.GetDetails: customer_id=%s org_id=%s: %w", customer.ID, request.Msg.GetOrgId(), err)) } creditBalance, err := h.creditService.GetBalance(ctx, customer.ID) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("CheckCreditEntitlement.GetBalance: customer_id=%s org_id=%s: %w", customer.ID, request.Msg.GetOrgId(), err)) + return nil, mapBillingError(fmt.Errorf("CheckCreditEntitlement.GetBalance: customer_id=%s org_id=%s: %w", customer.ID, request.Msg.GetOrgId(), err)) } if creditBalance-request.Msg.GetAmount() >= customerDetails.CreditMin { diff --git a/internal/api/v1beta1connect/billing_checkout.go b/internal/api/v1beta1connect/billing_checkout.go index 954a0d33d7..e6d0191c7a 100644 --- a/internal/api/v1beta1connect/billing_checkout.go +++ b/internal/api/v1beta1connect/billing_checkout.go @@ -20,7 +20,7 @@ func (h *ConnectHandler) CreateCheckout(ctx context.Context, request *connect.Re // Always infer billing_id from org_id (ignore billing_id from request for security) billingID, err := h.GetBillingAccountFromOrgID(ctx, request.Msg.GetOrgId()) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("CreateCheckout.GetBillingAccountFromOrgID: org_id=%s: %w", request.Msg.GetOrgId(), err)) + return nil, mapBillingError(fmt.Errorf("CreateCheckout.GetBillingAccountFromOrgID: org_id=%s: %w", request.Msg.GetOrgId(), err)) } // check if setup requested @@ -31,7 +31,7 @@ func (h *ConnectHandler) CreateCheckout(ctx context.Context, request *connect.Re CancelUrl: request.Msg.GetCancelUrl(), }) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("CreateCheckout.CreateSessionForPaymentMethod: billing_id=%s: %w", billingID, err)) + return nil, mapBillingError(fmt.Errorf("CreateCheckout.CreateSessionForPaymentMethod: billing_id=%s: %w", billingID, err)) } return connect.NewResponse(&frontierv1beta1.CreateCheckoutResponse{ @@ -50,7 +50,7 @@ func (h *ConnectHandler) CreateCheckout(ctx context.Context, request *connect.Re if errors.Is(err, checkout.ErrKycCompleted) { return nil, connect.NewError(connect.CodeFailedPrecondition, ErrPortalChangesKycCompleted) } - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("CreateCheckout.CreateSessionForCustomerPortal: billing_id=%s: %w", billingID, err)) + return nil, mapBillingError(fmt.Errorf("CreateCheckout.CreateSessionForCustomerPortal: billing_id=%s: %w", billingID, err)) } // Audit the customer portal session creation so we can trace who (a super @@ -120,7 +120,7 @@ func (h *ConnectHandler) CreateCheckout(ctx context.Context, request *connect.Re if errors.Is(err, product.ErrPerSeatLimitReached) { return nil, connect.NewError(connect.CodeInvalidArgument, ErrPerSeatLimitReached) } - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("CreateCheckout.Create: billing_id=%s plan_id=%s product_id=%s quantity=%d skip_trial=%v cancel_after_trial=%v: %w", billingID, planID, featureID, quantity, skipTrial, cancelAfterTrial, err)) + return nil, mapBillingError(fmt.Errorf("CreateCheckout.Create: billing_id=%s plan_id=%s product_id=%s quantity=%d skip_trial=%v cancel_after_trial=%v: %w", billingID, planID, featureID, quantity, skipTrial, cancelAfterTrial, err)) } return connect.NewResponse(&frontierv1beta1.CreateCheckoutResponse{ @@ -132,7 +132,7 @@ func (h *ConnectHandler) DelegatedCheckout(ctx context.Context, request *connect // Always infer billing_id from org_id (ignore billing_id from request for security) billingID, err := h.GetBillingAccountFromOrgID(ctx, request.Msg.GetOrgId()) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("DelegatedCheckout.GetBillingAccountFromOrgID: org_id=%s: %w", request.Msg.GetOrgId(), err)) + return nil, mapBillingError(fmt.Errorf("DelegatedCheckout.GetBillingAccountFromOrgID: org_id=%s: %w", request.Msg.GetOrgId(), err)) } var planID string @@ -161,19 +161,19 @@ func (h *ConnectHandler) DelegatedCheckout(ctx context.Context, request *connect ProviderCouponID: providerCouponID, }) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("DelegatedCheckout.Apply: billing_id=%s plan_id=%s product_id=%s product_quantity=%d skip_trial=%v cancel_after_trial=%v provider_coupon_id=%s: %w", billingID, planID, productID, productQuantity, skipTrial, cancelAfterTrail, providerCouponID, err)) + return nil, mapBillingError(fmt.Errorf("DelegatedCheckout.Apply: billing_id=%s plan_id=%s product_id=%s product_quantity=%d skip_trial=%v cancel_after_trial=%v provider_coupon_id=%s: %w", billingID, planID, productID, productQuantity, skipTrial, cancelAfterTrail, providerCouponID, err)) } var subsPb *frontierv1beta1.Subscription if subs != nil { if subsPb, err = transformSubscriptionToPB(*subs); err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("DelegatedCheckout: subscription_id=%s: %w", subs.ID, err)) + return nil, mapBillingError(fmt.Errorf("DelegatedCheckout: subscription_id=%s: %w", subs.ID, err)) } } var productPb *frontierv1beta1.Product if prod != nil { if productPb, err = transformProductToPB(*prod); err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("DelegatedCheckout: product_id=%s: %w", prod.ID, err)) + return nil, mapBillingError(fmt.Errorf("DelegatedCheckout: product_id=%s: %w", prod.ID, err)) } } @@ -191,7 +191,7 @@ func (h *ConnectHandler) ListCheckouts(ctx context.Context, request *connect.Req // Always infer billing_id from org_id (ignore billing_id from request for security) billingID, err := h.GetBillingAccountFromOrgID(ctx, request.Msg.GetOrgId()) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("ListCheckouts.GetBillingAccountFromOrgID: org_id=%s: %w", request.Msg.GetOrgId(), err)) + return nil, mapBillingError(fmt.Errorf("ListCheckouts.GetBillingAccountFromOrgID: org_id=%s: %w", request.Msg.GetOrgId(), err)) } var checkouts []*frontierv1beta1.CheckoutSession @@ -199,7 +199,7 @@ func (h *ConnectHandler) ListCheckouts(ctx context.Context, request *connect.Req CustomerID: billingID, }) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("ListCheckouts.List: billing_id=%s org_id=%s: %w", billingID, request.Msg.GetOrgId(), err)) + return nil, mapBillingError(fmt.Errorf("ListCheckouts.List: billing_id=%s org_id=%s: %w", billingID, request.Msg.GetOrgId(), err)) } for _, v := range checkoutList { checkouts = append(checkouts, transformCheckoutToPB(v)) @@ -217,7 +217,7 @@ func (h *ConnectHandler) GetCheckout(ctx context.Context, request *connect.Reque ch, err := h.checkoutService.GetByID(ctx, request.Msg.GetId()) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("GetCheckout.GetByID: checkout_id=%s: %w", request.Msg.GetId(), err)) + return nil, mapBillingError(fmt.Errorf("GetCheckout.GetByID: checkout_id=%s: %w", request.Msg.GetId(), err)) } return connect.NewResponse(&frontierv1beta1.GetCheckoutResponse{ diff --git a/internal/api/v1beta1connect/billing_customer.go b/internal/api/v1beta1connect/billing_customer.go index e43c93e734..9ee9a0a469 100644 --- a/internal/api/v1beta1connect/billing_customer.go +++ b/internal/api/v1beta1connect/billing_customer.go @@ -55,12 +55,12 @@ func (h *ConnectHandler) CreateBillingAccount(ctx context.Context, request *conn if errors.Is(err, customer.ErrActiveConflict) { return nil, connect.NewError(connect.CodeFailedPrecondition, err) } - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("CreateBillingAccount.Create: org_id=%s customer_name=%s customer_email=%s currency=%s offline=%v: %w", request.Msg.GetOrgId(), request.Msg.GetBody().GetName(), request.Msg.GetBody().GetEmail(), request.Msg.GetBody().GetCurrency(), request.Msg.GetOffline(), err)) + return nil, mapBillingError(fmt.Errorf("CreateBillingAccount.Create: org_id=%s customer_name=%s customer_email=%s currency=%s offline=%v: %w", request.Msg.GetOrgId(), request.Msg.GetBody().GetName(), request.Msg.GetBody().GetEmail(), request.Msg.GetBody().GetCurrency(), request.Msg.GetOffline(), err)) } customerPB, err := transformCustomerToPB(newCustomer) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("CreateBillingAccount: customer_id=%s: %w", newCustomer.ID, err)) + return nil, mapBillingError(fmt.Errorf("CreateBillingAccount: customer_id=%s: %w", newCustomer.ID, err)) } return connect.NewResponse(&frontierv1beta1.CreateBillingAccountResponse{ BillingAccount: customerPB, @@ -105,12 +105,12 @@ func (h *ConnectHandler) UpdateBillingAccount(ctx context.Context, request *conn TaxData: customerTaxes, }) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("UpdateBillingAccount.Update: customer_id=%s customer_name=%s customer_email=%s currency=%s: %w", request.Msg.GetId(), request.Msg.GetBody().GetName(), request.Msg.GetBody().GetEmail(), request.Msg.GetBody().GetCurrency(), err)) + return nil, mapBillingError(fmt.Errorf("UpdateBillingAccount.Update: customer_id=%s customer_name=%s customer_email=%s currency=%s: %w", request.Msg.GetId(), request.Msg.GetBody().GetName(), request.Msg.GetBody().GetEmail(), request.Msg.GetBody().GetCurrency(), err)) } customerPB, err := transformCustomerToPB(updatedCustomer) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("UpdateBillingAccount: customer_id=%s: %w", updatedCustomer.ID, err)) + return nil, mapBillingError(fmt.Errorf("UpdateBillingAccount: customer_id=%s: %w", updatedCustomer.ID, err)) } return connect.NewResponse(&frontierv1beta1.UpdateBillingAccountResponse{ @@ -124,7 +124,7 @@ func (h *ConnectHandler) RegisterBillingAccount(ctx context.Context, request *co if errors.Is(err, customer.ErrNotFound) { return nil, connect.NewError(connect.CodeNotFound, ErrCustomerNotFound) } - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("RegisterBillingAccount.RegisterToProviderIfRequired: customer_id=%s: %w", request.Msg.GetId(), err)) + return nil, mapBillingError(fmt.Errorf("RegisterBillingAccount.RegisterToProviderIfRequired: customer_id=%s: %w", request.Msg.GetId(), err)) } return connect.NewResponse(&frontierv1beta1.RegisterBillingAccountResponse{}), nil } @@ -138,12 +138,12 @@ func (h *ConnectHandler) ListBillingAccounts(ctx context.Context, request *conne OrgID: request.Msg.GetOrgId(), }) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("ListBillingAccounts.List: org_id=%s: %w", request.Msg.GetOrgId(), err)) + return nil, mapBillingError(fmt.Errorf("ListBillingAccounts.List: org_id=%s: %w", request.Msg.GetOrgId(), err)) } for _, v := range customerList { customerPB, err := transformCustomerToPB(v) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("ListBillingAccounts: customer_id=%s: %w", v.ID, err)) + return nil, mapBillingError(fmt.Errorf("ListBillingAccounts: customer_id=%s: %w", v.ID, err)) } customers = append(customers, customerPB) } @@ -161,7 +161,7 @@ func (h *ConnectHandler) ListBillingAccounts(ctx context.Context, request *conne func (h *ConnectHandler) DeleteBillingAccount(ctx context.Context, request *connect.Request[frontierv1beta1.DeleteBillingAccountRequest]) (*connect.Response[frontierv1beta1.DeleteBillingAccountResponse], error) { err := h.customerService.Delete(ctx, request.Msg.GetId()) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("DeleteBillingAccount.Delete: customer_id=%s: %w", request.Msg.GetId(), err)) + return nil, mapBillingError(fmt.Errorf("DeleteBillingAccount.Delete: customer_id=%s: %w", request.Msg.GetId(), err)) } return connect.NewResponse(&frontierv1beta1.DeleteBillingAccountResponse{}), nil } @@ -169,7 +169,7 @@ func (h *ConnectHandler) DeleteBillingAccount(ctx context.Context, request *conn func (h *ConnectHandler) EnableBillingAccount(ctx context.Context, request *connect.Request[frontierv1beta1.EnableBillingAccountRequest]) (*connect.Response[frontierv1beta1.EnableBillingAccountResponse], error) { err := h.customerService.Enable(ctx, request.Msg.GetId()) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("EnableBillingAccount.Enable: customer_id=%s: %w", request.Msg.GetId(), err)) + return nil, mapBillingError(fmt.Errorf("EnableBillingAccount.Enable: customer_id=%s: %w", request.Msg.GetId(), err)) } return connect.NewResponse(&frontierv1beta1.EnableBillingAccountResponse{}), nil } @@ -177,7 +177,7 @@ func (h *ConnectHandler) EnableBillingAccount(ctx context.Context, request *conn func (h *ConnectHandler) DisableBillingAccount(ctx context.Context, request *connect.Request[frontierv1beta1.DisableBillingAccountRequest]) (*connect.Response[frontierv1beta1.DisableBillingAccountResponse], error) { err := h.customerService.Disable(ctx, request.Msg.GetId()) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("DisableBillingAccount.Disable: customer_id=%s: %w", request.Msg.GetId(), err)) + return nil, mapBillingError(fmt.Errorf("DisableBillingAccount.Disable: customer_id=%s: %w", request.Msg.GetId(), err)) } return connect.NewResponse(&frontierv1beta1.DisableBillingAccountResponse{}), nil } @@ -185,7 +185,7 @@ func (h *ConnectHandler) DisableBillingAccount(ctx context.Context, request *con func (h *ConnectHandler) GetBillingBalance(ctx context.Context, request *connect.Request[frontierv1beta1.GetBillingBalanceRequest]) (*connect.Response[frontierv1beta1.GetBillingBalanceResponse], error) { balanceAmount, err := h.creditService.GetBalance(ctx, request.Msg.GetId()) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("GetBillingBalance.GetBalance: customer_id=%s: %w", request.Msg.GetId(), err)) + return nil, mapBillingError(fmt.Errorf("GetBillingBalance.GetBalance: customer_id=%s: %w", request.Msg.GetId(), err)) } return connect.NewResponse(&frontierv1beta1.GetBillingBalanceResponse{ Balance: &frontierv1beta1.BillingAccount_Balance{ @@ -198,7 +198,7 @@ func (h *ConnectHandler) GetBillingBalance(ctx context.Context, request *connect func (h *ConnectHandler) HasTrialed(ctx context.Context, request *connect.Request[frontierv1beta1.HasTrialedRequest]) (*connect.Response[frontierv1beta1.HasTrialedResponse], error) { hasTrialed, err := h.subscriptionService.HasUserSubscribedBefore(ctx, request.Msg.GetId(), request.Msg.GetPlanId()) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("HasTrialed.HasUserSubscribedBefore: customer_id=%s plan_id=%s: %w", request.Msg.GetId(), request.Msg.GetPlanId(), err)) + return nil, mapBillingError(fmt.Errorf("HasTrialed.HasUserSubscribedBefore: customer_id=%s plan_id=%s: %w", request.Msg.GetId(), request.Msg.GetPlanId(), err)) } return connect.NewResponse(&frontierv1beta1.HasTrialedResponse{ Trialed: hasTrialed, @@ -211,12 +211,12 @@ func (h *ConnectHandler) ListAllBillingAccounts(ctx context.Context, request *co OrgID: request.Msg.GetOrgId(), }) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("ListAllBillingAccounts.List: org_id=%s: %w", request.Msg.GetOrgId(), err)) + return nil, mapBillingError(fmt.Errorf("ListAllBillingAccounts.List: org_id=%s: %w", request.Msg.GetOrgId(), err)) } for _, v := range customerList { customerPB, err := transformCustomerToPB(v) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("ListAllBillingAccounts: customer_id=%s: %w", v.ID, err)) + return nil, mapBillingError(fmt.Errorf("ListAllBillingAccounts: customer_id=%s: %w", v.ID, err)) } customers = append(customers, customerPB) } @@ -265,7 +265,7 @@ func transformCustomerToPB(customer customer.Customer) (*frontierv1beta1.Billing func (h *ConnectHandler) UpdateBillingAccountLimits(ctx context.Context, request *connect.Request[frontierv1beta1.UpdateBillingAccountLimitsRequest]) (*connect.Response[frontierv1beta1.UpdateBillingAccountLimitsResponse], error) { _, err := h.customerService.UpdateCreditMinByID(ctx, request.Msg.GetId(), request.Msg.GetCreditMin()) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("UpdateBillingAccountLimits.UpdateCreditMinByID: customer_id=%s credit_min=%d: %w", request.Msg.GetId(), request.Msg.GetCreditMin(), err)) + return nil, mapBillingError(fmt.Errorf("UpdateBillingAccountLimits.UpdateCreditMinByID: customer_id=%s credit_min=%d: %w", request.Msg.GetId(), request.Msg.GetCreditMin(), err)) } return connect.NewResponse(&frontierv1beta1.UpdateBillingAccountLimitsResponse{}), nil @@ -274,7 +274,7 @@ func (h *ConnectHandler) UpdateBillingAccountLimits(ctx context.Context, request func (h *ConnectHandler) GetBillingAccountDetails(ctx context.Context, request *connect.Request[frontierv1beta1.GetBillingAccountDetailsRequest]) (*connect.Response[frontierv1beta1.GetBillingAccountDetailsResponse], error) { details, err := h.customerService.GetDetails(ctx, request.Msg.GetId()) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("GetBillingAccountDetails.GetDetails: customer_id=%s: %w", request.Msg.GetId(), err)) + return nil, mapBillingError(fmt.Errorf("GetBillingAccountDetails.GetDetails: customer_id=%s: %w", request.Msg.GetId(), err)) } return connect.NewResponse(&frontierv1beta1.GetBillingAccountDetailsResponse{ @@ -289,19 +289,19 @@ func (h *ConnectHandler) GetBillingAccount(ctx context.Context, request *connect if errors.Is(err, customer.ErrNotFound) { return nil, connect.NewError(connect.CodeNotFound, ErrNotFound) } - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("GetBillingAccount.GetByID: customer_id=%s: %w", request.Msg.GetId(), err)) + return nil, mapBillingError(fmt.Errorf("GetBillingAccount.GetByID: customer_id=%s: %w", request.Msg.GetId(), err)) } var paymentMethodsPbs []*frontierv1beta1.PaymentMethod if request.Msg.GetWithPaymentMethods() { pms, err := h.customerService.ListPaymentMethods(ctx, request.Msg.GetId()) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("GetBillingAccount.ListPaymentMethods: customer_id=%s: %w", request.Msg.GetId(), err)) + return nil, mapBillingError(fmt.Errorf("GetBillingAccount.ListPaymentMethods: customer_id=%s: %w", request.Msg.GetId(), err)) } for _, v := range pms { pmPB, err := transformPaymentMethodToPB(v) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("GetBillingAccount: payment_method_id=%s: %w", v.ID, err)) + return nil, mapBillingError(fmt.Errorf("GetBillingAccount: payment_method_id=%s: %w", v.ID, err)) } paymentMethodsPbs = append(paymentMethodsPbs, pmPB) } @@ -311,7 +311,7 @@ func (h *ConnectHandler) GetBillingAccount(ctx context.Context, request *connect if request.Msg.GetWithBillingDetails() { billingDetails, err := h.customerService.GetDetails(ctx, request.Msg.GetId()) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("GetBillingAccount.GetDetails: customer_id=%s: %w", request.Msg.GetId(), err)) + return nil, mapBillingError(fmt.Errorf("GetBillingAccount.GetDetails: customer_id=%s: %w", request.Msg.GetId(), err)) } billingDetailsPb = &frontierv1beta1.BillingAccountDetails{ CreditMin: billingDetails.CreditMin, @@ -321,7 +321,7 @@ func (h *ConnectHandler) GetBillingAccount(ctx context.Context, request *connect customerPB, err := transformCustomerToPB(customerOb) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("GetBillingAccount: customer_id=%s: %w", customerOb.ID, err)) + return nil, mapBillingError(fmt.Errorf("GetBillingAccount: customer_id=%s: %w", customerOb.ID, err)) } response := &frontierv1beta1.GetBillingAccountResponse{ @@ -367,7 +367,7 @@ func (h *ConnectHandler) UpdateBillingAccountDetails(ctx context.Context, reques DueInDays: request.Msg.GetDueInDays(), }) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("UpdateBillingAccountDetails.UpdateDetails: customer_id=%s credit_min=%d due_in_days=%d: %w", request.Msg.GetId(), request.Msg.GetCreditMin(), request.Msg.GetDueInDays(), err)) + return nil, mapBillingError(fmt.Errorf("UpdateBillingAccountDetails.UpdateDetails: customer_id=%s credit_min=%d due_in_days=%d: %w", request.Msg.GetId(), request.Msg.GetCreditMin(), request.Msg.GetDueInDays(), err)) } // Add audit log - infer org_id from billing account diff --git a/internal/api/v1beta1connect/billing_errors.go b/internal/api/v1beta1connect/billing_errors.go new file mode 100644 index 0000000000..4681c1c61b --- /dev/null +++ b/internal/api/v1beta1connect/billing_errors.go @@ -0,0 +1,37 @@ +package v1beta1connect + +import ( + "errors" + + "connectrpc.com/connect" + + "github.com/raystack/frontier/billing/customer" + billingerrors "github.com/raystack/frontier/billing/errors" + "github.com/raystack/frontier/billing/subscription" +) + +// mapBillingError is the fallback for billing handlers in place of a bare +// CodeInternal. Provider and account-state problems reach the caller as +// codes they can act on; everything else stays internal. +func mapBillingError(err error) *connect.Error { + switch { + case errors.Is(err, billingerrors.ErrProviderResourceMissing): + return connect.NewError(connect.CodeFailedPrecondition, ErrBillingProviderResourceMissing) + case errors.Is(err, billingerrors.ErrPaymentFailed): + var providerErr *billingerrors.ProviderError + if errors.As(err, &providerErr) { + return connect.NewError(connect.CodeFailedPrecondition, providerErr) + } + return connect.NewError(connect.CodeFailedPrecondition, billingerrors.ErrPaymentFailed) + case errors.Is(err, billingerrors.ErrProviderUnavailable): + return connect.NewError(connect.CodeUnavailable, ErrBillingProviderUnavailable) + case errors.Is(err, subscription.ErrSubscriptionOnProviderNotFound): + return connect.NewError(connect.CodeFailedPrecondition, ErrSubscriptionProviderMissing) + case errors.Is(err, subscription.ErrPhaseIsUpdating): + return connect.NewError(connect.CodeFailedPrecondition, subscription.ErrPhaseIsUpdating) + case errors.Is(err, customer.ErrExistingAccountWithPendingDues): + return connect.NewError(connect.CodeFailedPrecondition, customer.ErrExistingAccountWithPendingDues) + default: + return connect.NewError(connect.CodeInternal, err) + } +} diff --git a/internal/api/v1beta1connect/billing_errors_test.go b/internal/api/v1beta1connect/billing_errors_test.go new file mode 100644 index 0000000000..2f992e573c --- /dev/null +++ b/internal/api/v1beta1connect/billing_errors_test.go @@ -0,0 +1,86 @@ +package v1beta1connect + +import ( + "errors" + "fmt" + "testing" + + "connectrpc.com/connect" + "github.com/stretchr/testify/assert" + stripe "github.com/stripe/stripe-go/v79" + + "github.com/raystack/frontier/billing/customer" + billingerrors "github.com/raystack/frontier/billing/errors" + "github.com/raystack/frontier/billing/subscription" +) + +func TestMapBillingError(t *testing.T) { + deadCustomer := billingerrors.TranslateStripeError(&stripe.Error{ + Code: stripe.ErrorCodeResourceMissing, + Msg: "No such customer: 'cus_123'", + }) + cardDeclined := billingerrors.TranslateStripeError(&stripe.Error{ + Type: stripe.ErrorTypeCard, + Msg: "Your card was declined.", + }) + rateLimited := billingerrors.TranslateStripeError(&stripe.Error{ + Code: stripe.ErrorCodeRateLimit, + }) + + tests := []struct { + name string + err error + wantCode connect.Code + wantMsg string + }{ + { + name: "provider resource missing", + err: fmt.Errorf("GetUpcomingInvoice: org_id=abc: %w", deadCustomer), + wantCode: connect.CodeFailedPrecondition, + wantMsg: ErrBillingProviderResourceMissing.Error(), + }, + { + name: "payment failed keeps provider message", + err: fmt.Errorf("CreateCheckout: %w", cardDeclined), + wantCode: connect.CodeFailedPrecondition, + wantMsg: "payment failed: Your card was declined.", + }, + { + name: "provider unavailable", + err: fmt.Errorf("ListInvoices: %w", rateLimited), + wantCode: connect.CodeUnavailable, + wantMsg: ErrBillingProviderUnavailable.Error(), + }, + { + name: "subscription gone on provider", + err: fmt.Errorf("ChangeSubscription: %w", subscription.ErrSubscriptionOnProviderNotFound), + wantCode: connect.CodeFailedPrecondition, + wantMsg: ErrSubscriptionProviderMissing.Error(), + }, + { + name: "plan change in progress", + err: fmt.Errorf("ChangeSubscription: %w", subscription.ErrPhaseIsUpdating), + wantCode: connect.CodeFailedPrecondition, + wantMsg: subscription.ErrPhaseIsUpdating.Error(), + }, + { + name: "pending dues", + err: fmt.Errorf("CreateBillingAccount: %w", customer.ErrExistingAccountWithPendingDues), + wantCode: connect.CodeFailedPrecondition, + wantMsg: customer.ErrExistingAccountWithPendingDues.Error(), + }, + { + name: "unknown error stays internal", + err: fmt.Errorf("GetBillingAccount: %w", errors.New("db down")), + wantCode: connect.CodeInternal, + wantMsg: "GetBillingAccount: db down", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := mapBillingError(tt.err) + assert.Equal(t, tt.wantCode, got.Code()) + assert.Equal(t, tt.wantMsg, got.Message()) + }) + } +} diff --git a/internal/api/v1beta1connect/billing_invoice.go b/internal/api/v1beta1connect/billing_invoice.go index 16268d7df4..124319e7a5 100644 --- a/internal/api/v1beta1connect/billing_invoice.go +++ b/internal/api/v1beta1connect/billing_invoice.go @@ -22,13 +22,13 @@ func (h *ConnectHandler) ListAllInvoices(ctx context.Context, request *connect.R Pagination: paginate, }) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("ListAllInvoices.ListAll: page_num=%d page_size=%d: %w", request.Msg.GetPageNum(), request.Msg.GetPageSize(), err)) + return nil, mapBillingError(fmt.Errorf("ListAllInvoices.ListAll: page_num=%d page_size=%d: %w", request.Msg.GetPageNum(), request.Msg.GetPageSize(), err)) } var invoicePBs []*frontierv1beta1.Invoice for _, v := range invoices { invoicePB, err := transformInvoiceToPB(v) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("ListAllInvoices: invoice_id=%s: %w", v.ID, err)) + return nil, mapBillingError(fmt.Errorf("ListAllInvoices: invoice_id=%s: %w", v.ID, err)) } invoicePBs = append(invoicePBs, invoicePB) } @@ -53,7 +53,7 @@ func (h *ConnectHandler) ListInvoices(ctx context.Context, request *connect.Requ if errors.Is(err, customer.ErrInvalidUUID) || errors.Is(err, customer.ErrInvalidID) { return nil, connect.NewError(connect.CodeInvalidArgument, ErrBadRequest) } - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("ListInvoices.GetByOrgID: org_id=%s: %w", request.Msg.GetOrgId(), err)) + return nil, mapBillingError(fmt.Errorf("ListInvoices.GetByOrgID: org_id=%s: %w", request.Msg.GetOrgId(), err)) } billingID := cust.ID @@ -62,13 +62,13 @@ func (h *ConnectHandler) ListInvoices(ctx context.Context, request *connect.Requ NonZeroOnly: request.Msg.GetNonzeroAmountOnly(), }) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("ListInvoices.List: org_id=%s billing_id=%s nonzero_amount_only=%v: %w", request.Msg.GetOrgId(), billingID, request.Msg.GetNonzeroAmountOnly(), err)) + return nil, mapBillingError(fmt.Errorf("ListInvoices.List: org_id=%s billing_id=%s nonzero_amount_only=%v: %w", request.Msg.GetOrgId(), billingID, request.Msg.GetNonzeroAmountOnly(), err)) } var invoicePBs []*frontierv1beta1.Invoice for _, v := range invoices { invoicePB, err := transformInvoiceToPB(v) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("ListInvoices: invoice_id=%s: %w", v.ID, err)) + return nil, mapBillingError(fmt.Errorf("ListInvoices: invoice_id=%s: %w", v.ID, err)) } invoicePBs = append(invoicePBs, invoicePB) } @@ -97,17 +97,17 @@ func (h *ConnectHandler) GetUpcomingInvoice(ctx context.Context, request *connec if errors.Is(err, customer.ErrInvalidUUID) || errors.Is(err, customer.ErrInvalidID) { return nil, connect.NewError(connect.CodeInvalidArgument, ErrBadRequest) } - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("GetUpcomingInvoice.GetByOrgID: org_id=%s: %w", request.Msg.GetOrgId(), err)) + return nil, mapBillingError(fmt.Errorf("GetUpcomingInvoice.GetByOrgID: org_id=%s: %w", request.Msg.GetOrgId(), err)) } billingID := cust.ID invoice, err := h.invoiceService.GetUpcoming(ctx, billingID) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("GetUpcomingInvoice.GetUpcoming: org_id=%s billing_id=%s: %w", request.Msg.GetOrgId(), billingID, err)) + return nil, mapBillingError(fmt.Errorf("GetUpcomingInvoice.GetUpcoming: org_id=%s billing_id=%s: %w", request.Msg.GetOrgId(), billingID, err)) } invoicePB, err := transformInvoiceToPB(invoice) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("GetUpcomingInvoice: invoice_id=%s: %w", invoice.ID, err)) + return nil, mapBillingError(fmt.Errorf("GetUpcomingInvoice: invoice_id=%s: %w", invoice.ID, err)) } return connect.NewResponse(&frontierv1beta1.GetUpcomingInvoiceResponse{ @@ -152,7 +152,7 @@ func transformInvoiceToPB(i invoice.Invoice) (*frontierv1beta1.Invoice, error) { func (h *ConnectHandler) GenerateInvoices(ctx context.Context, request *connect.Request[frontierv1beta1.GenerateInvoicesRequest]) (*connect.Response[frontierv1beta1.GenerateInvoicesResponse], error) { err := h.invoiceService.TriggerCreditOverdraftInvoices(ctx) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("GenerateInvoices.TriggerCreditOverdraftInvoices: %w", err)) + return nil, mapBillingError(fmt.Errorf("GenerateInvoices.TriggerCreditOverdraftInvoices: %w", err)) } return connect.NewResponse(&frontierv1beta1.GenerateInvoicesResponse{}), nil } @@ -175,7 +175,7 @@ func (h *ConnectHandler) SearchInvoices(ctx context.Context, request *connect.Re if errors.Is(err, invoice.ErrBadInput) { return nil, connect.NewError(connect.CodeInvalidArgument, err) } - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("SearchInvoices.SearchInvoices: query_offset=%d query_limit=%d: %w", rqlQuery.Offset, rqlQuery.Limit, err)) + return nil, mapBillingError(fmt.Errorf("SearchInvoices.SearchInvoices: query_offset=%d query_limit=%d: %w", rqlQuery.Offset, rqlQuery.Limit, err)) } for _, v := range invoicesData { diff --git a/internal/api/v1beta1connect/billing_plan.go b/internal/api/v1beta1connect/billing_plan.go index d2a5cf0e99..4c1e39ec8c 100644 --- a/internal/api/v1beta1connect/billing_plan.go +++ b/internal/api/v1beta1connect/billing_plan.go @@ -79,17 +79,17 @@ func (h *ConnectHandler) CreatePlan(ctx context.Context, request *connect.Reques Products: products, }) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("CreatePlan.UpsertPlans: plan_name=%s plan_title=%s interval=%s product_count=%d: %w", planToCreate.Name, planToCreate.Title, planToCreate.Interval, len(products), err)) + return nil, mapBillingError(fmt.Errorf("CreatePlan.UpsertPlans: plan_name=%s plan_title=%s interval=%s product_count=%d: %w", planToCreate.Name, planToCreate.Title, planToCreate.Interval, len(products), err)) } newPlan, err := h.planService.GetByID(ctx, planToCreate.Name) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("CreatePlan.GetByID: plan_name=%s: %w", planToCreate.Name, err)) + return nil, mapBillingError(fmt.Errorf("CreatePlan.GetByID: plan_name=%s: %w", planToCreate.Name, err)) } planPB, err := transformPlanToPB(newPlan) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("CreatePlan: plan_id=%s: %w", newPlan.ID, err)) + return nil, mapBillingError(fmt.Errorf("CreatePlan: plan_id=%s: %w", newPlan.ID, err)) } return connect.NewResponse(&frontierv1beta1.CreatePlanResponse{Plan: planPB}), nil @@ -99,12 +99,12 @@ func (h *ConnectHandler) ListPlans(ctx context.Context, request *connect.Request var plans []*frontierv1beta1.Plan planList, err := h.planService.List(ctx, plan.Filter{}) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("ListPlans.List: %w", err)) + return nil, mapBillingError(fmt.Errorf("ListPlans.List: %w", err)) } for _, v := range planList { planPB, err := transformPlanToPB(v) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("ListPlans: plan_id=%s: %w", v.ID, err)) + return nil, mapBillingError(fmt.Errorf("ListPlans: plan_id=%s: %w", v.ID, err)) } plans = append(plans, planPB) } @@ -115,12 +115,12 @@ func (h *ConnectHandler) ListPlans(ctx context.Context, request *connect.Request func (h *ConnectHandler) GetPlan(ctx context.Context, request *connect.Request[frontierv1beta1.GetPlanRequest]) (*connect.Response[frontierv1beta1.GetPlanResponse], error) { planOb, err := h.planService.GetByID(ctx, request.Msg.GetId()) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("GetPlan.GetByID: plan_id=%s: %w", request.Msg.GetId(), err)) + return nil, mapBillingError(fmt.Errorf("GetPlan.GetByID: plan_id=%s: %w", request.Msg.GetId(), err)) } planPB, err := transformPlanToPB(planOb) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("GetPlan: plan_id=%s: %w", planOb.ID, err)) + return nil, mapBillingError(fmt.Errorf("GetPlan: plan_id=%s: %w", planOb.ID, err)) } return connect.NewResponse(&frontierv1beta1.GetPlanResponse{Plan: planPB}), nil diff --git a/internal/api/v1beta1connect/billing_product.go b/internal/api/v1beta1connect/billing_product.go index cee1be9f95..cafc609bd4 100644 --- a/internal/api/v1beta1connect/billing_product.go +++ b/internal/api/v1beta1connect/billing_product.go @@ -15,12 +15,12 @@ func (h *ConnectHandler) ListProducts(ctx context.Context, request *connect.Requ var products []*frontierv1beta1.Product productsList, err := h.productService.List(ctx, product.Filter{}) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("ListProducts.List: %w", err)) + return nil, mapBillingError(fmt.Errorf("ListProducts.List: %w", err)) } for _, v := range productsList { productPB, err := transformProductToPB(v) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("ListProducts: entity_id=%s: %w", v.ID, err)) + return nil, mapBillingError(fmt.Errorf("ListProducts: entity_id=%s: %w", v.ID, err)) } products = append(products, productPB) } @@ -33,12 +33,12 @@ func (h *ConnectHandler) ListProducts(ctx context.Context, request *connect.Requ func (h *ConnectHandler) GetProduct(ctx context.Context, request *connect.Request[frontierv1beta1.GetProductRequest]) (*connect.Response[frontierv1beta1.GetProductResponse], error) { product, err := h.productService.GetByID(ctx, request.Msg.GetId()) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("GetProduct.GetByID: product_id=%s: %w", request.Msg.GetId(), err)) + return nil, mapBillingError(fmt.Errorf("GetProduct.GetByID: product_id=%s: %w", request.Msg.GetId(), err)) } productPB, err := transformProductToPB(product) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("GetProduct: entity_id=%s: %w", product.ID, err)) + return nil, mapBillingError(fmt.Errorf("GetProduct: entity_id=%s: %w", product.ID, err)) } return connect.NewResponse(&frontierv1beta1.GetProductResponse{ @@ -93,14 +93,14 @@ func (h *ConnectHandler) CreateProduct(ctx context.Context, request *connect.Req Metadata: metaDataMap, }) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("CreateProduct.Create: product_name=%s product_title=%s behavior=%s price_count=%d feature_count=%d: %w", + return nil, mapBillingError(fmt.Errorf("CreateProduct.Create: product_name=%s product_title=%s behavior=%s price_count=%d feature_count=%d: %w", request.Msg.GetBody().GetName(), request.Msg.GetBody().GetTitle(), request.Msg.GetBody().GetBehavior(), len(productPrices), len(productFeatures), err)) } productPB, err := transformProductToPB(newProduct) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("CreateProduct: entity_id=%s: %w", newProduct.ID, err)) + return nil, mapBillingError(fmt.Errorf("CreateProduct: entity_id=%s: %w", newProduct.ID, err)) } return connect.NewResponse(&frontierv1beta1.CreateProductResponse{ @@ -161,19 +161,19 @@ func (h *ConnectHandler) UpdateProduct(ctx context.Context, request *connect.Req Metadata: metaDataMap, }) if err != nil { + wrapped := fmt.Errorf("UpdateProduct.Update: product_id=%s product_name=%s product_title=%s behavior=%s price_count=%d feature_count=%d: %w", + request.Msg.GetId(), request.Msg.GetBody().GetName(), request.Msg.GetBody().GetTitle(), + request.Msg.GetBody().GetBehavior(), len(productPrices), len(productFeatures), err) // an invalid price (bad name, duplicate, or a change to an immutable // field) is the caller's fault, so report it as an invalid argument. - code := connect.CodeInternal if errors.Is(err, product.ErrInvalidDetail) { - code = connect.CodeInvalidArgument + return nil, connect.NewError(connect.CodeInvalidArgument, wrapped) } - return nil, connect.NewError(code, fmt.Errorf("UpdateProduct.Update: product_id=%s product_name=%s product_title=%s behavior=%s price_count=%d feature_count=%d: %w", - request.Msg.GetId(), request.Msg.GetBody().GetName(), request.Msg.GetBody().GetTitle(), - request.Msg.GetBody().GetBehavior(), len(productPrices), len(productFeatures), err)) + return nil, mapBillingError(wrapped) } productPb, err := transformProductToPB(updatedProduct) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("UpdateProduct: entity_id=%s: %w", updatedProduct.ID, err)) + return nil, mapBillingError(fmt.Errorf("UpdateProduct: entity_id=%s: %w", updatedProduct.ID, err)) } return connect.NewResponse(&frontierv1beta1.UpdateProductResponse{ @@ -184,14 +184,14 @@ func (h *ConnectHandler) UpdateProduct(ctx context.Context, request *connect.Req func (h *ConnectHandler) ListFeatures(ctx context.Context, request *connect.Request[frontierv1beta1.ListFeaturesRequest]) (*connect.Response[frontierv1beta1.ListFeaturesResponse], error) { features, err := h.productService.ListFeatures(ctx, product.Filter{}) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("ListFeatures.ListFeatures: %w", err)) + return nil, mapBillingError(fmt.Errorf("ListFeatures.ListFeatures: %w", err)) } var featuresPB []*frontierv1beta1.Feature for _, v := range features { f, err := transformFeatureToPB(v) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("ListFeatures: entity_id=%s: %w", v.ID, err)) + return nil, mapBillingError(fmt.Errorf("ListFeatures: entity_id=%s: %w", v.ID, err)) } featuresPB = append(featuresPB, f) } @@ -213,13 +213,13 @@ func (h *ConnectHandler) CreateFeature(ctx context.Context, request *connect.Req if errors.Is(err, product.ErrInvalidFeatureDetail) { return nil, connect.NewError(connect.CodeInvalidArgument, ErrBadRequest) } - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("CreateFeature.UpsertFeature: feature_name=%s feature_title=%s product_ids=%v: %w", + return nil, mapBillingError(fmt.Errorf("CreateFeature.UpsertFeature: feature_name=%s feature_title=%s product_ids=%v: %w", request.Msg.GetBody().GetName(), request.Msg.GetBody().GetTitle(), request.Msg.GetBody().GetProductIds(), err)) } featurePB, err := transformFeatureToPB(newFeature) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("CreateFeature: entity_id=%s: %w", newFeature.ID, err)) + return nil, mapBillingError(fmt.Errorf("CreateFeature: entity_id=%s: %w", newFeature.ID, err)) } return connect.NewResponse(&frontierv1beta1.CreateFeatureResponse{ @@ -240,13 +240,13 @@ func (h *ConnectHandler) UpdateFeature(ctx context.Context, request *connect.Req if errors.Is(err, product.ErrInvalidFeatureDetail) { return nil, connect.NewError(connect.CodeInvalidArgument, ErrBadRequest) } - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("UpdateFeature.UpsertFeature: feature_id=%s feature_name=%s feature_title=%s product_ids=%v: %w", + return nil, mapBillingError(fmt.Errorf("UpdateFeature.UpsertFeature: feature_id=%s feature_name=%s feature_title=%s product_ids=%v: %w", request.Msg.GetId(), request.Msg.GetBody().GetName(), request.Msg.GetBody().GetTitle(), request.Msg.GetBody().GetProductIds(), err)) } featurePB, err := transformFeatureToPB(updatedFeature) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("UpdateFeature: entity_id=%s: %w", updatedFeature.ID, err)) + return nil, mapBillingError(fmt.Errorf("UpdateFeature: entity_id=%s: %w", updatedFeature.ID, err)) } return connect.NewResponse(&frontierv1beta1.UpdateFeatureResponse{ @@ -257,12 +257,12 @@ func (h *ConnectHandler) UpdateFeature(ctx context.Context, request *connect.Req func (h *ConnectHandler) GetFeature(ctx context.Context, request *connect.Request[frontierv1beta1.GetFeatureRequest]) (*connect.Response[frontierv1beta1.GetFeatureResponse], error) { feature, err := h.productService.GetFeatureByID(ctx, request.Msg.GetId()) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("GetFeature.GetFeatureByID: feature_id=%s: %w", request.Msg.GetId(), err)) + return nil, mapBillingError(fmt.Errorf("GetFeature.GetFeatureByID: feature_id=%s: %w", request.Msg.GetId(), err)) } featurePB, err := transformFeatureToPB(feature) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("GetFeature: entity_id=%s: %w", feature.ID, err)) + return nil, mapBillingError(fmt.Errorf("GetFeature: entity_id=%s: %w", feature.ID, err)) } return connect.NewResponse(&frontierv1beta1.GetFeatureResponse{ diff --git a/internal/api/v1beta1connect/billing_subscription.go b/internal/api/v1beta1connect/billing_subscription.go index 7f9425ccfa..ddbb068bd6 100644 --- a/internal/api/v1beta1connect/billing_subscription.go +++ b/internal/api/v1beta1connect/billing_subscription.go @@ -28,7 +28,7 @@ func (h *ConnectHandler) ListSubscriptions(ctx context.Context, request *connect Subscriptions: []*frontierv1beta1.Subscription{}, }), nil } - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("ListSubscriptions.GetByOrgID: org_id=%s: %w", request.Msg.GetOrgId(), err)) + return nil, mapBillingError(fmt.Errorf("ListSubscriptions.GetByOrgID: org_id=%s: %w", request.Msg.GetOrgId(), err)) } billingID := cust.ID @@ -48,13 +48,13 @@ func (h *ConnectHandler) ListSubscriptions(ctx context.Context, request *connect PlanID: planID, }) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("ListSubscriptions.List: billing_id=%s org_id=%s state=%s plan_id=%s: %w", + return nil, mapBillingError(fmt.Errorf("ListSubscriptions.List: billing_id=%s org_id=%s state=%s plan_id=%s: %w", billingID, request.Msg.GetOrgId(), request.Msg.GetState(), planID, err)) } for _, v := range subscriptionList { subscriptionPB, err := transformSubscriptionToPB(v) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("ListSubscriptions: entity_id=%s: %w", v.ID, err)) + return nil, mapBillingError(fmt.Errorf("ListSubscriptions: entity_id=%s: %w", v.ID, err)) } subscriptions = append(subscriptions, subscriptionPB) } @@ -72,12 +72,12 @@ func (h *ConnectHandler) ListSubscriptions(ctx context.Context, request *connect func (h *ConnectHandler) GetSubscription(ctx context.Context, request *connect.Request[frontierv1beta1.GetSubscriptionRequest]) (*connect.Response[frontierv1beta1.GetSubscriptionResponse], error) { subscription, err := h.subscriptionService.GetByID(ctx, request.Msg.GetId()) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("GetSubscription.GetByID: subscription_id=%s: %w", request.Msg.GetId(), err)) + return nil, mapBillingError(fmt.Errorf("GetSubscription.GetByID: subscription_id=%s: %w", request.Msg.GetId(), err)) } subscriptionPB, err := transformSubscriptionToPB(subscription) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("GetSubscription: entity_id=%s: %w", subscription.ID, err)) + return nil, mapBillingError(fmt.Errorf("GetSubscription: entity_id=%s: %w", subscription.ID, err)) } response := &frontierv1beta1.GetSubscriptionResponse{ Subscription: subscriptionPB, @@ -92,7 +92,7 @@ func (h *ConnectHandler) GetSubscription(ctx context.Context, request *connect.R func (h *ConnectHandler) CancelSubscription(ctx context.Context, request *connect.Request[frontierv1beta1.CancelSubscriptionRequest]) (*connect.Response[frontierv1beta1.CancelSubscriptionResponse], error) { _, err := h.subscriptionService.Cancel(ctx, request.Msg.GetId(), request.Msg.GetImmediate()) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("CancelSubscription.Cancel: subscription_id=%s immediate=%v: %w", request.Msg.GetId(), request.Msg.GetImmediate(), err)) + return nil, mapBillingError(fmt.Errorf("CancelSubscription.Cancel: subscription_id=%s immediate=%v: %w", request.Msg.GetId(), request.Msg.GetImmediate(), err)) } return connect.NewResponse(&frontierv1beta1.CancelSubscriptionResponse{}), nil } @@ -126,7 +126,7 @@ func (h *ConnectHandler) ChangeSubscription(ctx context.Context, request *connec if errors.Is(err, subscription.ErrAlreadyOnSamePlan) { return nil, connect.NewError(connect.CodeInvalidArgument, ErrAlreadyOnSamePlan) } - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("ChangeSubscription.ChangePlan: subscription_id=%s plan_id=%s immediate=%v cancel_upcoming=%v: %w", + return nil, mapBillingError(fmt.Errorf("ChangeSubscription.ChangePlan: subscription_id=%s plan_id=%s immediate=%v cancel_upcoming=%v: %w", request.Msg.GetId(), changeReq.PlanID, changeReq.Immediate, changeReq.CancelUpcoming, err)) } diff --git a/internal/api/v1beta1connect/billing_usage.go b/internal/api/v1beta1connect/billing_usage.go index a471276be4..370e926c11 100644 --- a/internal/api/v1beta1connect/billing_usage.go +++ b/internal/api/v1beta1connect/billing_usage.go @@ -27,7 +27,7 @@ func (h *ConnectHandler) CreateBillingUsage(ctx context.Context, request *connec if errors.Is(err, customer.ErrInvalidUUID) || errors.Is(err, customer.ErrInvalidID) { return nil, connect.NewError(connect.CodeInvalidArgument, err) } - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("CreateBillingUsage.GetByOrgID: org_id=%s: %w", request.Msg.GetOrgId(), err)) + return nil, mapBillingError(fmt.Errorf("CreateBillingUsage.GetByOrgID: org_id=%s: %w", request.Msg.GetOrgId(), err)) } createRequests := make([]usage.Usage, 0, len(request.Msg.GetUsages())) @@ -56,7 +56,7 @@ func (h *ConnectHandler) CreateBillingUsage(ctx context.Context, request *connec if errors.Is(err, credit.ErrAlreadyApplied) { return nil, connect.NewError(connect.CodeAlreadyExists, ErrAlreadyApplied) } - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("CreateBillingUsage.Report: billing_id=%s org_id=%s usage_count=%d: %w", + return nil, mapBillingError(fmt.Errorf("CreateBillingUsage.Report: billing_id=%s org_id=%s usage_count=%d: %w", cust.ID, request.Msg.GetOrgId(), len(createRequests), err)) } @@ -77,7 +77,7 @@ func (h *ConnectHandler) ListBillingTransactions(ctx context.Context, request *c if errors.Is(err, customer.ErrInvalidUUID) || errors.Is(err, customer.ErrInvalidID) { return nil, connect.NewError(connect.CodeInvalidArgument, ErrBadRequest) } - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("ListBillingTransactions.GetByOrgID: org_id=%s: %w", request.Msg.GetOrgId(), err)) + return nil, mapBillingError(fmt.Errorf("ListBillingTransactions.GetByOrgID: org_id=%s: %w", request.Msg.GetOrgId(), err)) } billingID := cust.ID @@ -101,13 +101,13 @@ func (h *ConnectHandler) ListBillingTransactions(ctx context.Context, request *c EndRange: endRange, }) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("ListBillingTransactions.List: org_id=%s billing_id=%s start_range=%v end_range=%v: %w", + return nil, mapBillingError(fmt.Errorf("ListBillingTransactions.List: org_id=%s billing_id=%s start_range=%v end_range=%v: %w", request.Msg.GetOrgId(), billingID, startRange, endRange, err)) } for _, v := range transactionsList { transactionPB, err := transformTransactionToPB(v) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("ListBillingTransactions: entity_id=%s: %w", v.ID, err)) + return nil, mapBillingError(fmt.Errorf("ListBillingTransactions: entity_id=%s: %w", v.ID, err)) } transactions = append(transactions, transactionPB) } @@ -139,13 +139,13 @@ func (h *ConnectHandler) TotalDebitedTransactions(ctx context.Context, request * if errors.Is(err, customer.ErrInvalidUUID) || errors.Is(err, customer.ErrInvalidID) { return nil, connect.NewError(connect.CodeInvalidArgument, ErrBadRequest) } - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("TotalDebitedTransactions.GetByOrgID: org_id=%s: %w", request.Msg.GetOrgId(), err)) + return nil, mapBillingError(fmt.Errorf("TotalDebitedTransactions.GetByOrgID: org_id=%s: %w", request.Msg.GetOrgId(), err)) } billingID := cust.ID debitAmount, err := h.creditService.GetTotalDebitedAmount(ctx, billingID) if err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("TotalDebitedTransactions.GetTotalDebitedAmount: org_id=%s billing_id=%s: %w", + return nil, mapBillingError(fmt.Errorf("TotalDebitedTransactions.GetTotalDebitedAmount: org_id=%s billing_id=%s: %w", request.Msg.GetOrgId(), billingID, err)) } @@ -186,7 +186,7 @@ func (h *ConnectHandler) RevertBillingUsage(ctx context.Context, request *connec if errors.Is(err, customer.ErrInvalidUUID) || errors.Is(err, customer.ErrInvalidID) { return nil, connect.NewError(connect.CodeInvalidArgument, err) } - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("RevertBillingUsage.GetByOrgID: org_id=%s: %w", request.Msg.GetOrgId(), err)) + return nil, mapBillingError(fmt.Errorf("RevertBillingUsage.GetByOrgID: org_id=%s: %w", request.Msg.GetOrgId(), err)) } if err := h.usageService.Revert(ctx, cust.ID, @@ -202,7 +202,7 @@ func (h *ConnectHandler) RevertBillingUsage(ctx context.Context, request *connec } else if errors.Is(err, credit.ErrAlreadyApplied) { return nil, connect.NewError(connect.CodeInvalidArgument, err) } - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("RevertBillingUsage.Revert: billing_id=%s org_id=%s usage_id=%s amount=%d: %w", + return nil, mapBillingError(fmt.Errorf("RevertBillingUsage.Revert: billing_id=%s org_id=%s usage_id=%s amount=%d: %w", cust.ID, request.Msg.GetOrgId(), request.Msg.GetUsageId(), request.Msg.GetAmount(), err)) } return connect.NewResponse(&frontierv1beta1.RevertBillingUsageResponse{}), nil diff --git a/internal/api/v1beta1connect/billing_webhook.go b/internal/api/v1beta1connect/billing_webhook.go index bc10ce3277..4fca3d459b 100644 --- a/internal/api/v1beta1connect/billing_webhook.go +++ b/internal/api/v1beta1connect/billing_webhook.go @@ -26,7 +26,7 @@ func (h *ConnectHandler) BillingWebhookCallback(ctx context.Context, request *co Name: request.Msg.GetProvider(), Body: request.Msg.GetBody(), }); err != nil { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("BillingWebhookCallback.BillingWebhook: provider=%s: %w", request.Msg.GetProvider(), err)) + return nil, mapBillingError(fmt.Errorf("BillingWebhookCallback.BillingWebhook: provider=%s: %w", request.Msg.GetProvider(), err)) } return connect.NewResponse(&frontierv1beta1.BillingWebhookCallbackResponse{}), nil } diff --git a/internal/api/v1beta1connect/errors.go b/internal/api/v1beta1connect/errors.go index 4dd5890e6c..302048ecd7 100644 --- a/internal/api/v1beta1connect/errors.go +++ b/internal/api/v1beta1connect/errors.go @@ -5,69 +5,72 @@ import ( ) var ( - ErrBadRequest = errors.New("invalid syntax in body") - ErrInvalidMetadata = errors.New("metadata schema validation failed") - ErrOperationUnsupported = errors.New("operation not supported") - ErrInternalServerError = errors.ErrInternalServerError - ErrUnauthenticated = errors.New("not authenticated") - ErrUnauthorized = errors.New("not authorized") - ErrNotFound = errors.New("not found") - ErrInvalidEmail = errors.New("Invalid email") - ErrUserNotExist = errors.New("user doesn't exist") - ErrInvalidNamesapceOrID = errors.New("namespace and ID cannot be empty") - ErrConflictRequest = errors.New("already exist") - ErrBadBodyMetaSchemaError = errors.New(ErrBadRequest.Error() + " : " + ErrInvalidMetadata.Error()) - ErrInvalidActorType = errors.New("invalid actor type") - ErrActivityRequired = errors.New("activity is required") - ErrStatusRequired = errors.New("status is required") - ErrProspectIdRequired = errors.New("prospect ID is required") - ErrProspectNotFound = errors.New("record not found for the given input") - ErrRQLParse = errors.New("error parsing RQL query") - ErrOrgDisabled = errors.New("org is disabled. Please contact your administrator to enable it") - ErrMinAdminCount = errors.New("org must have at least one admin, consider adding another admin before removing") - ErrLastOwnerRole = errors.New("org must have at least one owner, consider assigning another owner before changing this user's role") - ErrDomainNotFound = errors.New("domain whitelist request doesn't exist") - ErrDomainAlreadyExists = errors.New("domain name already exists for that organization") - ErrInvalidHost = errors.New("invalid domain, no such host found") - ErrTXTRecordNotFound = errors.New("required TXT record not found for domain verification") - ErrDomainMismatch = errors.New("user and org's whitelisted domains doesn't match") - ErrInvitationNotFound = errors.New("invitation not found") - ErrInvitationExpired = errors.New("invitation expired") - ErrAlreadyMember = errors.New("principal is already a member of the resource") - ErrNotMember = errors.New("principal is not a member of the resource") - ErrInvalidOrgRole = errors.New("role is not valid for organization scope") - ErrInvalidProjectRole = errors.New("role is not valid for project scope") - ErrInvalidGroupRole = errors.New("role is not valid for group scope") - ErrLastGroupOwnerRole = errors.New("group must have at least one owner, consider assigning another owner before changing this user's role") - ErrNotOrgMember = errors.New("principal is not a member of the organization") - ErrEmptyEmailID = errors.New("email id is empty") - ErrEmailConflict = errors.New("user email can't be updated") - ErrCustomerNotFound = errors.New("customer doesn't exist") - ErrServiceUserNotFound = errors.New("service user not found") - ErrServiceUserCredNotFound = errors.New("service user credentials not found") - ErrConflictingPlanChange = errors.New("cannot change plan and cancel upcoming changes at the same time") - ErrNoChangeRequested = errors.New("no change requested") - ErrPerSeatLimitReached = errors.New("per seat limit reached") - ErrAlreadyOnSamePlan = errors.New("already on same plan") - ErrBillingProviderNotSupported = errors.New("provider not supported") - ErrInsufficientCredits = errors.New("insufficient credits") - ErrAlreadyApplied = errors.New("credits already applied") - ErrInvalidRoleID = errors.New("role id is invalid") - ErrNamespaceSplitNotation = errors.New("subject/object should be provided as 'namespace:uuid'") - ErrPolicyNotFound = errors.New("policy doesn't exist") - ErrProjectNotFound = errors.New("project doesn't exist") - ErrGroupNotFound = errors.New("group doesn't exist") - ErrOrgNotFound = errors.New("org doesn't exist") - ErrGroupMinOwnerCount = errors.New("group must have at least one owner, consider adding another owner before removing") - ErrPortalChangesKycCompleted = errors.New("customer portal changes not allowed: organization kyc completed") - ErrResourceNotFound = errors.New("resource doesn't exist") - ErrInvalidPreferenceFilter = errors.New("invalid preference filter set") - ErrTraitNotFound = errors.New("preference trait not found, preferences can only be created with valid trait") - ErrInvalidPreferenceValue = errors.New("invalid value for preference") - ErrInvalidPreferenceScope = errors.New("invalid scope: trait does not support scoping or scope_type/scope_id must be provided together") - ErrMetaschemaNotFound = errors.New("metaschema doesn't exist") - ErrSessionNotFound = errors.New("session doesn't exist") - ErrInvalidSessionID = errors.New("invalid session_id format: must be a valid UUID") - ErrInvalidUserID = errors.New("invalid user_id format: must be a valid UUID") - ErrRoleNotFound = errors.New("role doesn't exist") + ErrBadRequest = errors.New("invalid syntax in body") + ErrInvalidMetadata = errors.New("metadata schema validation failed") + ErrOperationUnsupported = errors.New("operation not supported") + ErrInternalServerError = errors.ErrInternalServerError + ErrUnauthenticated = errors.New("not authenticated") + ErrUnauthorized = errors.New("not authorized") + ErrNotFound = errors.New("not found") + ErrInvalidEmail = errors.New("Invalid email") + ErrUserNotExist = errors.New("user doesn't exist") + ErrInvalidNamesapceOrID = errors.New("namespace and ID cannot be empty") + ErrConflictRequest = errors.New("already exist") + ErrBadBodyMetaSchemaError = errors.New(ErrBadRequest.Error() + " : " + ErrInvalidMetadata.Error()) + ErrInvalidActorType = errors.New("invalid actor type") + ErrActivityRequired = errors.New("activity is required") + ErrStatusRequired = errors.New("status is required") + ErrProspectIdRequired = errors.New("prospect ID is required") + ErrProspectNotFound = errors.New("record not found for the given input") + ErrRQLParse = errors.New("error parsing RQL query") + ErrOrgDisabled = errors.New("org is disabled. Please contact your administrator to enable it") + ErrMinAdminCount = errors.New("org must have at least one admin, consider adding another admin before removing") + ErrLastOwnerRole = errors.New("org must have at least one owner, consider assigning another owner before changing this user's role") + ErrDomainNotFound = errors.New("domain whitelist request doesn't exist") + ErrDomainAlreadyExists = errors.New("domain name already exists for that organization") + ErrInvalidHost = errors.New("invalid domain, no such host found") + ErrTXTRecordNotFound = errors.New("required TXT record not found for domain verification") + ErrDomainMismatch = errors.New("user and org's whitelisted domains doesn't match") + ErrInvitationNotFound = errors.New("invitation not found") + ErrInvitationExpired = errors.New("invitation expired") + ErrAlreadyMember = errors.New("principal is already a member of the resource") + ErrNotMember = errors.New("principal is not a member of the resource") + ErrInvalidOrgRole = errors.New("role is not valid for organization scope") + ErrInvalidProjectRole = errors.New("role is not valid for project scope") + ErrInvalidGroupRole = errors.New("role is not valid for group scope") + ErrLastGroupOwnerRole = errors.New("group must have at least one owner, consider assigning another owner before changing this user's role") + ErrNotOrgMember = errors.New("principal is not a member of the organization") + ErrEmptyEmailID = errors.New("email id is empty") + ErrEmailConflict = errors.New("user email can't be updated") + ErrCustomerNotFound = errors.New("customer doesn't exist") + ErrServiceUserNotFound = errors.New("service user not found") + ErrServiceUserCredNotFound = errors.New("service user credentials not found") + ErrConflictingPlanChange = errors.New("cannot change plan and cancel upcoming changes at the same time") + ErrNoChangeRequested = errors.New("no change requested") + ErrPerSeatLimitReached = errors.New("per seat limit reached") + ErrAlreadyOnSamePlan = errors.New("already on same plan") + ErrBillingProviderNotSupported = errors.New("provider not supported") + ErrBillingProviderResourceMissing = errors.New("billing account is no longer linked to the payment provider") + ErrBillingProviderUnavailable = errors.New("billing provider is unavailable, retry later") + ErrSubscriptionProviderMissing = errors.New("subscription no longer exists on the billing provider") + ErrInsufficientCredits = errors.New("insufficient credits") + ErrAlreadyApplied = errors.New("credits already applied") + ErrInvalidRoleID = errors.New("role id is invalid") + ErrNamespaceSplitNotation = errors.New("subject/object should be provided as 'namespace:uuid'") + ErrPolicyNotFound = errors.New("policy doesn't exist") + ErrProjectNotFound = errors.New("project doesn't exist") + ErrGroupNotFound = errors.New("group doesn't exist") + ErrOrgNotFound = errors.New("org doesn't exist") + ErrGroupMinOwnerCount = errors.New("group must have at least one owner, consider adding another owner before removing") + ErrPortalChangesKycCompleted = errors.New("customer portal changes not allowed: organization kyc completed") + ErrResourceNotFound = errors.New("resource doesn't exist") + ErrInvalidPreferenceFilter = errors.New("invalid preference filter set") + ErrTraitNotFound = errors.New("preference trait not found, preferences can only be created with valid trait") + ErrInvalidPreferenceValue = errors.New("invalid value for preference") + ErrInvalidPreferenceScope = errors.New("invalid scope: trait does not support scoping or scope_type/scope_id must be provided together") + ErrMetaschemaNotFound = errors.New("metaschema doesn't exist") + ErrSessionNotFound = errors.New("session doesn't exist") + ErrInvalidSessionID = errors.New("invalid session_id format: must be a valid UUID") + ErrInvalidUserID = errors.New("invalid user_id format: must be a valid UUID") + ErrRoleNotFound = errors.New("role doesn't exist") ) From e6e67cbecc1e46767b7af4a4dea48736edbd0d9c Mon Sep 17 00:00:00 2001 From: Abhishek Sah Date: Wed, 5 Aug 2026 11:32:47 +0530 Subject: [PATCH 2/5] 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 --- billing/checkout/checkout.go | 11 +++--- billing/checkout/service.go | 4 +-- internal/api/v1beta1connect/billing_errors.go | 14 ++++++++ .../api/v1beta1connect/billing_errors_test.go | 34 ++++++++++++++++++- 4 files changed, 55 insertions(+), 8 deletions(-) diff --git a/billing/checkout/checkout.go b/billing/checkout/checkout.go index b2cfdab30d..bc71d040c9 100644 --- a/billing/checkout/checkout.go +++ b/billing/checkout/checkout.go @@ -20,11 +20,12 @@ func (s State) String() string { } var ( - ErrNotFound = errors.New("checkout not found") - ErrInvalidUUID = errors.New("invalid syntax of uuid") - ErrInvalidID = errors.New("invalid checkout id") - ErrInvalidDetail = errors.New("invalid checkout detail") - ErrKycCompleted = errors.New("organization kyc completed") + ErrNotFound = errors.New("checkout not found") + ErrInvalidUUID = errors.New("invalid syntax of uuid") + ErrInvalidID = errors.New("invalid checkout id") + ErrInvalidDetail = errors.New("invalid checkout detail") + ErrKycCompleted = errors.New("organization kyc completed") + ErrAlreadySubscribed = errors.New("already subscribed to the plan") ) type Checkout struct { diff --git a/billing/checkout/service.go b/billing/checkout/service.go index 3ab5ca5b1d..01a61e2d9b 100644 --- a/billing/checkout/service.go +++ b/billing/checkout/service.go @@ -248,7 +248,7 @@ func (s *Service) Create(ctx context.Context, ch Checkout) (Checkout, error) { if subID, err := s.checkIfAlreadySubscribed(ctx, ch); err != nil { return Checkout{}, err } else if subID != "" { - return Checkout{}, fmt.Errorf("already subscribed to the plan") + return Checkout{}, ErrAlreadySubscribed } // create subscription items @@ -894,7 +894,7 @@ func (s *Service) Apply(ctx context.Context, ch Checkout) (*subscription.Subscri if subID, err := s.checkIfAlreadySubscribed(ctx, ch); err != nil { return nil, nil, err } else if subID != "" { - return nil, nil, fmt.Errorf("already subscribed to the plan") + return nil, nil, ErrAlreadySubscribed } if err := s.cancelTrialingSubscription(ctx, ch.CustomerID, ch.PlanID); err != nil { diff --git a/internal/api/v1beta1connect/billing_errors.go b/internal/api/v1beta1connect/billing_errors.go index 4681c1c61b..629fc92eb7 100644 --- a/internal/api/v1beta1connect/billing_errors.go +++ b/internal/api/v1beta1connect/billing_errors.go @@ -5,8 +5,10 @@ import ( "connectrpc.com/connect" + "github.com/raystack/frontier/billing/checkout" "github.com/raystack/frontier/billing/customer" billingerrors "github.com/raystack/frontier/billing/errors" + "github.com/raystack/frontier/billing/product" "github.com/raystack/frontier/billing/subscription" ) @@ -16,6 +18,10 @@ import ( func mapBillingError(err error) *connect.Error { switch { case errors.Is(err, billingerrors.ErrProviderResourceMissing): + var providerErr *billingerrors.ProviderError + if errors.As(err, &providerErr) { + return connect.NewError(connect.CodeFailedPrecondition, providerErr) + } return connect.NewError(connect.CodeFailedPrecondition, ErrBillingProviderResourceMissing) case errors.Is(err, billingerrors.ErrPaymentFailed): var providerErr *billingerrors.ProviderError @@ -31,6 +37,14 @@ func mapBillingError(err error) *connect.Error { return connect.NewError(connect.CodeFailedPrecondition, subscription.ErrPhaseIsUpdating) case errors.Is(err, customer.ErrExistingAccountWithPendingDues): return connect.NewError(connect.CodeFailedPrecondition, customer.ErrExistingAccountWithPendingDues) + case errors.Is(err, customer.ErrNotFound): + return connect.NewError(connect.CodeNotFound, ErrCustomerNotFound) + case errors.Is(err, checkout.ErrAlreadySubscribed): + return connect.NewError(connect.CodeAlreadyExists, checkout.ErrAlreadySubscribed) + case errors.Is(err, product.ErrProductNotFound): + return connect.NewError(connect.CodeNotFound, product.ErrProductNotFound) + case errors.Is(err, product.ErrFeatureNotFound): + return connect.NewError(connect.CodeNotFound, product.ErrFeatureNotFound) default: return connect.NewError(connect.CodeInternal, err) } diff --git a/internal/api/v1beta1connect/billing_errors_test.go b/internal/api/v1beta1connect/billing_errors_test.go index 2f992e573c..e9c1f32820 100644 --- a/internal/api/v1beta1connect/billing_errors_test.go +++ b/internal/api/v1beta1connect/billing_errors_test.go @@ -9,8 +9,10 @@ import ( "github.com/stretchr/testify/assert" stripe "github.com/stripe/stripe-go/v79" + "github.com/raystack/frontier/billing/checkout" "github.com/raystack/frontier/billing/customer" billingerrors "github.com/raystack/frontier/billing/errors" + "github.com/raystack/frontier/billing/product" "github.com/raystack/frontier/billing/subscription" ) @@ -34,9 +36,15 @@ func TestMapBillingError(t *testing.T) { wantMsg string }{ { - name: "provider resource missing", + name: "provider resource missing keeps provider message", err: fmt.Errorf("GetUpcomingInvoice: org_id=abc: %w", deadCustomer), wantCode: connect.CodeFailedPrecondition, + wantMsg: "record no longer exists on the billing provider: No such customer: 'cus_123'", + }, + { + name: "provider resource missing without provider error", + err: fmt.Errorf("GetUpcomingInvoice: %w", billingerrors.ErrProviderResourceMissing), + wantCode: connect.CodeFailedPrecondition, wantMsg: ErrBillingProviderResourceMissing.Error(), }, { @@ -69,6 +77,30 @@ func TestMapBillingError(t *testing.T) { wantCode: connect.CodeFailedPrecondition, wantMsg: customer.ErrExistingAccountWithPendingDues.Error(), }, + { + name: "customer not found", + err: fmt.Errorf("CreateCheckout.GetBillingAccountFromOrgID: org_id=abc: %w", customer.ErrNotFound), + wantCode: connect.CodeNotFound, + wantMsg: ErrCustomerNotFound.Error(), + }, + { + name: "already subscribed to the plan", + err: fmt.Errorf("CreateCheckout.Create: %w", checkout.ErrAlreadySubscribed), + wantCode: connect.CodeAlreadyExists, + wantMsg: checkout.ErrAlreadySubscribed.Error(), + }, + { + name: "product not found", + err: fmt.Errorf("GetProduct.GetByID: product_id=abc: %w", product.ErrProductNotFound), + wantCode: connect.CodeNotFound, + wantMsg: product.ErrProductNotFound.Error(), + }, + { + name: "feature not found", + err: fmt.Errorf("CheckFeatureEntitlement: feature=abc: %w", product.ErrFeatureNotFound), + wantCode: connect.CodeNotFound, + wantMsg: product.ErrFeatureNotFound.Error(), + }, { name: "unknown error stays internal", err: fmt.Errorf("GetBillingAccount: %w", errors.New("db down")), From ee04d19c3645b18f1b66bd49e68fc0c2260e82b3 Mon Sep 17 00:00:00 2001 From: Abhishek Sah Date: Wed, 5 Aug 2026 11:40:43 +0530 Subject: [PATCH 3/5] fix(api): mask provider ids in billing error messages Provider messages passed to the caller keep their text but hide provider-generated object ids: a deleted customer reads as "No such customer: 'cus_*****'". Caller-supplied values like coupon codes don't match the id shape and stay intact. Co-Authored-By: Claude Fable 5 --- internal/api/v1beta1connect/billing_errors.go | 15 +++++++++++++-- .../api/v1beta1connect/billing_errors_test.go | 16 +++++++++++++--- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/internal/api/v1beta1connect/billing_errors.go b/internal/api/v1beta1connect/billing_errors.go index 629fc92eb7..24f57f35bd 100644 --- a/internal/api/v1beta1connect/billing_errors.go +++ b/internal/api/v1beta1connect/billing_errors.go @@ -2,6 +2,7 @@ package v1beta1connect import ( "errors" + "regexp" "connectrpc.com/connect" @@ -12,6 +13,16 @@ import ( "github.com/raystack/frontier/billing/subscription" ) +// provider-generated object ids (random alphanumeric after the prefix). +// Caller-supplied values like coupon codes don't match this shape. +var providerIDPattern = regexp.MustCompile(`\b(cus|sub_sched|sub|in|ii|il|pi|pm|seti|si|price|prod|cs|coup|promo)_[A-Za-z0-9]{8,}\b`) + +// redactedProviderError hides provider object ids in a message shown to the +// caller, keeping the rest of the provider's text. +func redactedProviderError(providerErr *billingerrors.ProviderError) error { + return errors.New(providerIDPattern.ReplaceAllString(providerErr.Error(), "${1}_*****")) +} + // mapBillingError is the fallback for billing handlers in place of a bare // CodeInternal. Provider and account-state problems reach the caller as // codes they can act on; everything else stays internal. @@ -20,13 +31,13 @@ func mapBillingError(err error) *connect.Error { case errors.Is(err, billingerrors.ErrProviderResourceMissing): var providerErr *billingerrors.ProviderError if errors.As(err, &providerErr) { - return connect.NewError(connect.CodeFailedPrecondition, providerErr) + return connect.NewError(connect.CodeFailedPrecondition, redactedProviderError(providerErr)) } return connect.NewError(connect.CodeFailedPrecondition, ErrBillingProviderResourceMissing) case errors.Is(err, billingerrors.ErrPaymentFailed): var providerErr *billingerrors.ProviderError if errors.As(err, &providerErr) { - return connect.NewError(connect.CodeFailedPrecondition, providerErr) + return connect.NewError(connect.CodeFailedPrecondition, redactedProviderError(providerErr)) } return connect.NewError(connect.CodeFailedPrecondition, billingerrors.ErrPaymentFailed) case errors.Is(err, billingerrors.ErrProviderUnavailable): diff --git a/internal/api/v1beta1connect/billing_errors_test.go b/internal/api/v1beta1connect/billing_errors_test.go index e9c1f32820..797869799e 100644 --- a/internal/api/v1beta1connect/billing_errors_test.go +++ b/internal/api/v1beta1connect/billing_errors_test.go @@ -19,7 +19,11 @@ import ( func TestMapBillingError(t *testing.T) { deadCustomer := billingerrors.TranslateStripeError(&stripe.Error{ Code: stripe.ErrorCodeResourceMissing, - Msg: "No such customer: 'cus_123'", + Msg: "No such customer: 'cus_QhBNKtbzOZzumU'", + }) + deadCoupon := billingerrors.TranslateStripeError(&stripe.Error{ + Code: stripe.ErrorCodeResourceMissing, + Msg: "No such coupon: 'SUMMER20'", }) cardDeclined := billingerrors.TranslateStripeError(&stripe.Error{ Type: stripe.ErrorTypeCard, @@ -36,10 +40,16 @@ func TestMapBillingError(t *testing.T) { wantMsg string }{ { - name: "provider resource missing keeps provider message", + name: "provider resource missing masks the provider id", err: fmt.Errorf("GetUpcomingInvoice: org_id=abc: %w", deadCustomer), wantCode: connect.CodeFailedPrecondition, - wantMsg: "record no longer exists on the billing provider: No such customer: 'cus_123'", + wantMsg: "record no longer exists on the billing provider: No such customer: 'cus_*****'", + }, + { + name: "provider resource missing keeps a caller-supplied value", + err: fmt.Errorf("DelegatedCheckout: %w", deadCoupon), + wantCode: connect.CodeFailedPrecondition, + wantMsg: "record no longer exists on the billing provider: No such coupon: 'SUMMER20'", }, { name: "provider resource missing without provider error", From e46a5d4cf8bdd3e64f3e1248b15d06efc048810c Mon Sep 17 00:00:00 2001 From: Abhishek Sah Date: Wed, 5 Aug 2026 11:43:31 +0530 Subject: [PATCH 4/5] fix(api): also mask mode-infixed and charge ids in billing errors cs_test_/cs_live_ checkout session ids escaped the mask because of the mode infix, and charge (ch_) ids were not in the prefix list. Co-Authored-By: Claude Fable 5 --- internal/api/v1beta1connect/billing_errors.go | 2 +- internal/api/v1beta1connect/billing_errors_test.go | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/internal/api/v1beta1connect/billing_errors.go b/internal/api/v1beta1connect/billing_errors.go index 24f57f35bd..29259d9ebe 100644 --- a/internal/api/v1beta1connect/billing_errors.go +++ b/internal/api/v1beta1connect/billing_errors.go @@ -15,7 +15,7 @@ import ( // provider-generated object ids (random alphanumeric after the prefix). // Caller-supplied values like coupon codes don't match this shape. -var providerIDPattern = regexp.MustCompile(`\b(cus|sub_sched|sub|in|ii|il|pi|pm|seti|si|price|prod|cs|coup|promo)_[A-Za-z0-9]{8,}\b`) +var providerIDPattern = regexp.MustCompile(`\b(cus|sub_sched|sub|in|ii|il|pi|pm|seti|si|price|prod|cs|ch|coup|promo)_(?:(?:test|live)_)?[A-Za-z0-9]{8,}\b`) // redactedProviderError hides provider object ids in a message shown to the // caller, keeping the rest of the provider's text. diff --git a/internal/api/v1beta1connect/billing_errors_test.go b/internal/api/v1beta1connect/billing_errors_test.go index 797869799e..cb1faf1fec 100644 --- a/internal/api/v1beta1connect/billing_errors_test.go +++ b/internal/api/v1beta1connect/billing_errors_test.go @@ -51,6 +51,15 @@ func TestMapBillingError(t *testing.T) { wantCode: connect.CodeFailedPrecondition, wantMsg: "record no longer exists on the billing provider: No such coupon: 'SUMMER20'", }, + { + name: "provider resource missing masks a test-mode checkout session id", + err: fmt.Errorf("GetCheckout: %w", billingerrors.TranslateStripeError(&stripe.Error{ + Code: stripe.ErrorCodeResourceMissing, + Msg: "No such checkout.session: 'cs_test_c1GSMJhe9lzCEkJAVj3R5ife'", + })), + wantCode: connect.CodeFailedPrecondition, + wantMsg: "record no longer exists on the billing provider: No such checkout.session: 'cs_*****'", + }, { name: "provider resource missing without provider error", err: fmt.Errorf("GetUpcomingInvoice: %w", billingerrors.ErrProviderResourceMissing), From e947ba0acb1bf818e694b67447addf3f559f1821 Mon Sep 17 00:00:00 2001 From: Abhishek Sah Date: Wed, 5 Aug 2026 17:04:18 +0530 Subject: [PATCH 5/5] fix(api): log mapped billing errors, mask ids by shape MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review fixes on the error mapper: - mapped errors are logged with full detail (handler context, provider request id) before the clean error goes to the caller — the caller error is all the logger interceptor sees, so without this the org and method context would vanish from server logs for exactly these cases - the id mask now matches the general shape of provider ids instead of an allowlist of prefixes, so an object type we haven't listed cannot leak its id - the resource-missing fallback uses the same message as the translated path instead of a second text that guessed the missing object Co-Authored-By: Claude Fable 5 --- internal/api/v1beta1connect/billing_check.go | 10 +- .../api/v1beta1connect/billing_checkout.go | 22 +-- .../api/v1beta1connect/billing_customer.go | 44 +++--- internal/api/v1beta1connect/billing_errors.go | 32 ++++- .../api/v1beta1connect/billing_errors_test.go | 14 +- .../api/v1beta1connect/billing_invoice.go | 20 +-- internal/api/v1beta1connect/billing_plan.go | 14 +- .../api/v1beta1connect/billing_product.go | 32 ++--- .../v1beta1connect/billing_subscription.go | 14 +- internal/api/v1beta1connect/billing_usage.go | 18 +-- .../api/v1beta1connect/billing_webhook.go | 2 +- internal/api/v1beta1connect/errors.go | 135 +++++++++--------- 12 files changed, 193 insertions(+), 164 deletions(-) diff --git a/internal/api/v1beta1connect/billing_check.go b/internal/api/v1beta1connect/billing_check.go index 5fc5c5c5f0..298b5b8dc3 100644 --- a/internal/api/v1beta1connect/billing_check.go +++ b/internal/api/v1beta1connect/billing_check.go @@ -20,12 +20,12 @@ func (h *ConnectHandler) CheckFeatureEntitlement(ctx context.Context, request *c if errors.Is(err, customer.ErrInvalidUUID) || errors.Is(err, customer.ErrInvalidID) { return nil, connect.NewError(connect.CodeInvalidArgument, err) } - return nil, mapBillingError(fmt.Errorf("CheckFeatureEntitlement.GetByOrgID: org_id=%s: %w", request.Msg.GetOrgId(), err)) + return nil, mapBillingError(ctx, fmt.Errorf("CheckFeatureEntitlement.GetByOrgID: org_id=%s: %w", request.Msg.GetOrgId(), err)) } checkStatus, err := h.entitlementService.Check(ctx, cust.ID, request.Msg.GetFeature()) if err != nil { - return nil, mapBillingError(fmt.Errorf("CheckFeatureEntitlement: billing_id=%s org_id=%s feature=%s: %w", cust.ID, request.Msg.GetOrgId(), request.Msg.GetFeature(), err)) + return nil, mapBillingError(ctx, fmt.Errorf("CheckFeatureEntitlement: billing_id=%s org_id=%s feature=%s: %w", cust.ID, request.Msg.GetOrgId(), request.Msg.GetFeature(), err)) } return connect.NewResponse(&frontierv1beta1.CheckFeatureEntitlementResponse{ @@ -38,7 +38,7 @@ func (h *ConnectHandler) CheckCreditEntitlement(ctx context.Context, request *co OrgID: request.Msg.GetOrgId(), }) if err != nil { - return nil, mapBillingError(fmt.Errorf("CheckCreditEntitlement.List: org_id=%s: %w", request.Msg.GetOrgId(), err)) + return nil, mapBillingError(ctx, fmt.Errorf("CheckCreditEntitlement.List: org_id=%s: %w", request.Msg.GetOrgId(), err)) } if len(customerList) == 0 { @@ -48,12 +48,12 @@ func (h *ConnectHandler) CheckCreditEntitlement(ctx context.Context, request *co customer := customerList[0] customerDetails, err := h.customerService.GetDetails(ctx, customer.ID) if err != nil { - return nil, mapBillingError(fmt.Errorf("CheckCreditEntitlement.GetDetails: customer_id=%s org_id=%s: %w", customer.ID, request.Msg.GetOrgId(), err)) + return nil, mapBillingError(ctx, fmt.Errorf("CheckCreditEntitlement.GetDetails: customer_id=%s org_id=%s: %w", customer.ID, request.Msg.GetOrgId(), err)) } creditBalance, err := h.creditService.GetBalance(ctx, customer.ID) if err != nil { - return nil, mapBillingError(fmt.Errorf("CheckCreditEntitlement.GetBalance: customer_id=%s org_id=%s: %w", customer.ID, request.Msg.GetOrgId(), err)) + return nil, mapBillingError(ctx, fmt.Errorf("CheckCreditEntitlement.GetBalance: customer_id=%s org_id=%s: %w", customer.ID, request.Msg.GetOrgId(), err)) } if creditBalance-request.Msg.GetAmount() >= customerDetails.CreditMin { diff --git a/internal/api/v1beta1connect/billing_checkout.go b/internal/api/v1beta1connect/billing_checkout.go index e6d0191c7a..e20f81981b 100644 --- a/internal/api/v1beta1connect/billing_checkout.go +++ b/internal/api/v1beta1connect/billing_checkout.go @@ -20,7 +20,7 @@ func (h *ConnectHandler) CreateCheckout(ctx context.Context, request *connect.Re // Always infer billing_id from org_id (ignore billing_id from request for security) billingID, err := h.GetBillingAccountFromOrgID(ctx, request.Msg.GetOrgId()) if err != nil { - return nil, mapBillingError(fmt.Errorf("CreateCheckout.GetBillingAccountFromOrgID: org_id=%s: %w", request.Msg.GetOrgId(), err)) + return nil, mapBillingError(ctx, fmt.Errorf("CreateCheckout.GetBillingAccountFromOrgID: org_id=%s: %w", request.Msg.GetOrgId(), err)) } // check if setup requested @@ -31,7 +31,7 @@ func (h *ConnectHandler) CreateCheckout(ctx context.Context, request *connect.Re CancelUrl: request.Msg.GetCancelUrl(), }) if err != nil { - return nil, mapBillingError(fmt.Errorf("CreateCheckout.CreateSessionForPaymentMethod: billing_id=%s: %w", billingID, err)) + return nil, mapBillingError(ctx, fmt.Errorf("CreateCheckout.CreateSessionForPaymentMethod: billing_id=%s: %w", billingID, err)) } return connect.NewResponse(&frontierv1beta1.CreateCheckoutResponse{ @@ -50,7 +50,7 @@ func (h *ConnectHandler) CreateCheckout(ctx context.Context, request *connect.Re if errors.Is(err, checkout.ErrKycCompleted) { return nil, connect.NewError(connect.CodeFailedPrecondition, ErrPortalChangesKycCompleted) } - return nil, mapBillingError(fmt.Errorf("CreateCheckout.CreateSessionForCustomerPortal: billing_id=%s: %w", billingID, err)) + return nil, mapBillingError(ctx, fmt.Errorf("CreateCheckout.CreateSessionForCustomerPortal: billing_id=%s: %w", billingID, err)) } // Audit the customer portal session creation so we can trace who (a super @@ -120,7 +120,7 @@ func (h *ConnectHandler) CreateCheckout(ctx context.Context, request *connect.Re if errors.Is(err, product.ErrPerSeatLimitReached) { return nil, connect.NewError(connect.CodeInvalidArgument, ErrPerSeatLimitReached) } - return nil, mapBillingError(fmt.Errorf("CreateCheckout.Create: billing_id=%s plan_id=%s product_id=%s quantity=%d skip_trial=%v cancel_after_trial=%v: %w", billingID, planID, featureID, quantity, skipTrial, cancelAfterTrial, err)) + return nil, mapBillingError(ctx, fmt.Errorf("CreateCheckout.Create: billing_id=%s plan_id=%s product_id=%s quantity=%d skip_trial=%v cancel_after_trial=%v: %w", billingID, planID, featureID, quantity, skipTrial, cancelAfterTrial, err)) } return connect.NewResponse(&frontierv1beta1.CreateCheckoutResponse{ @@ -132,7 +132,7 @@ func (h *ConnectHandler) DelegatedCheckout(ctx context.Context, request *connect // Always infer billing_id from org_id (ignore billing_id from request for security) billingID, err := h.GetBillingAccountFromOrgID(ctx, request.Msg.GetOrgId()) if err != nil { - return nil, mapBillingError(fmt.Errorf("DelegatedCheckout.GetBillingAccountFromOrgID: org_id=%s: %w", request.Msg.GetOrgId(), err)) + return nil, mapBillingError(ctx, fmt.Errorf("DelegatedCheckout.GetBillingAccountFromOrgID: org_id=%s: %w", request.Msg.GetOrgId(), err)) } var planID string @@ -161,19 +161,19 @@ func (h *ConnectHandler) DelegatedCheckout(ctx context.Context, request *connect ProviderCouponID: providerCouponID, }) if err != nil { - return nil, mapBillingError(fmt.Errorf("DelegatedCheckout.Apply: billing_id=%s plan_id=%s product_id=%s product_quantity=%d skip_trial=%v cancel_after_trial=%v provider_coupon_id=%s: %w", billingID, planID, productID, productQuantity, skipTrial, cancelAfterTrail, providerCouponID, err)) + return nil, mapBillingError(ctx, fmt.Errorf("DelegatedCheckout.Apply: billing_id=%s plan_id=%s product_id=%s product_quantity=%d skip_trial=%v cancel_after_trial=%v provider_coupon_id=%s: %w", billingID, planID, productID, productQuantity, skipTrial, cancelAfterTrail, providerCouponID, err)) } var subsPb *frontierv1beta1.Subscription if subs != nil { if subsPb, err = transformSubscriptionToPB(*subs); err != nil { - return nil, mapBillingError(fmt.Errorf("DelegatedCheckout: subscription_id=%s: %w", subs.ID, err)) + return nil, mapBillingError(ctx, fmt.Errorf("DelegatedCheckout: subscription_id=%s: %w", subs.ID, err)) } } var productPb *frontierv1beta1.Product if prod != nil { if productPb, err = transformProductToPB(*prod); err != nil { - return nil, mapBillingError(fmt.Errorf("DelegatedCheckout: product_id=%s: %w", prod.ID, err)) + return nil, mapBillingError(ctx, fmt.Errorf("DelegatedCheckout: product_id=%s: %w", prod.ID, err)) } } @@ -191,7 +191,7 @@ func (h *ConnectHandler) ListCheckouts(ctx context.Context, request *connect.Req // Always infer billing_id from org_id (ignore billing_id from request for security) billingID, err := h.GetBillingAccountFromOrgID(ctx, request.Msg.GetOrgId()) if err != nil { - return nil, mapBillingError(fmt.Errorf("ListCheckouts.GetBillingAccountFromOrgID: org_id=%s: %w", request.Msg.GetOrgId(), err)) + return nil, mapBillingError(ctx, fmt.Errorf("ListCheckouts.GetBillingAccountFromOrgID: org_id=%s: %w", request.Msg.GetOrgId(), err)) } var checkouts []*frontierv1beta1.CheckoutSession @@ -199,7 +199,7 @@ func (h *ConnectHandler) ListCheckouts(ctx context.Context, request *connect.Req CustomerID: billingID, }) if err != nil { - return nil, mapBillingError(fmt.Errorf("ListCheckouts.List: billing_id=%s org_id=%s: %w", billingID, request.Msg.GetOrgId(), err)) + return nil, mapBillingError(ctx, fmt.Errorf("ListCheckouts.List: billing_id=%s org_id=%s: %w", billingID, request.Msg.GetOrgId(), err)) } for _, v := range checkoutList { checkouts = append(checkouts, transformCheckoutToPB(v)) @@ -217,7 +217,7 @@ func (h *ConnectHandler) GetCheckout(ctx context.Context, request *connect.Reque ch, err := h.checkoutService.GetByID(ctx, request.Msg.GetId()) if err != nil { - return nil, mapBillingError(fmt.Errorf("GetCheckout.GetByID: checkout_id=%s: %w", request.Msg.GetId(), err)) + return nil, mapBillingError(ctx, fmt.Errorf("GetCheckout.GetByID: checkout_id=%s: %w", request.Msg.GetId(), err)) } return connect.NewResponse(&frontierv1beta1.GetCheckoutResponse{ diff --git a/internal/api/v1beta1connect/billing_customer.go b/internal/api/v1beta1connect/billing_customer.go index 9ee9a0a469..40d4beab31 100644 --- a/internal/api/v1beta1connect/billing_customer.go +++ b/internal/api/v1beta1connect/billing_customer.go @@ -55,12 +55,12 @@ func (h *ConnectHandler) CreateBillingAccount(ctx context.Context, request *conn if errors.Is(err, customer.ErrActiveConflict) { return nil, connect.NewError(connect.CodeFailedPrecondition, err) } - return nil, mapBillingError(fmt.Errorf("CreateBillingAccount.Create: org_id=%s customer_name=%s customer_email=%s currency=%s offline=%v: %w", request.Msg.GetOrgId(), request.Msg.GetBody().GetName(), request.Msg.GetBody().GetEmail(), request.Msg.GetBody().GetCurrency(), request.Msg.GetOffline(), err)) + return nil, mapBillingError(ctx, fmt.Errorf("CreateBillingAccount.Create: org_id=%s customer_name=%s customer_email=%s currency=%s offline=%v: %w", request.Msg.GetOrgId(), request.Msg.GetBody().GetName(), request.Msg.GetBody().GetEmail(), request.Msg.GetBody().GetCurrency(), request.Msg.GetOffline(), err)) } customerPB, err := transformCustomerToPB(newCustomer) if err != nil { - return nil, mapBillingError(fmt.Errorf("CreateBillingAccount: customer_id=%s: %w", newCustomer.ID, err)) + return nil, mapBillingError(ctx, fmt.Errorf("CreateBillingAccount: customer_id=%s: %w", newCustomer.ID, err)) } return connect.NewResponse(&frontierv1beta1.CreateBillingAccountResponse{ BillingAccount: customerPB, @@ -105,12 +105,12 @@ func (h *ConnectHandler) UpdateBillingAccount(ctx context.Context, request *conn TaxData: customerTaxes, }) if err != nil { - return nil, mapBillingError(fmt.Errorf("UpdateBillingAccount.Update: customer_id=%s customer_name=%s customer_email=%s currency=%s: %w", request.Msg.GetId(), request.Msg.GetBody().GetName(), request.Msg.GetBody().GetEmail(), request.Msg.GetBody().GetCurrency(), err)) + return nil, mapBillingError(ctx, fmt.Errorf("UpdateBillingAccount.Update: customer_id=%s customer_name=%s customer_email=%s currency=%s: %w", request.Msg.GetId(), request.Msg.GetBody().GetName(), request.Msg.GetBody().GetEmail(), request.Msg.GetBody().GetCurrency(), err)) } customerPB, err := transformCustomerToPB(updatedCustomer) if err != nil { - return nil, mapBillingError(fmt.Errorf("UpdateBillingAccount: customer_id=%s: %w", updatedCustomer.ID, err)) + return nil, mapBillingError(ctx, fmt.Errorf("UpdateBillingAccount: customer_id=%s: %w", updatedCustomer.ID, err)) } return connect.NewResponse(&frontierv1beta1.UpdateBillingAccountResponse{ @@ -124,7 +124,7 @@ func (h *ConnectHandler) RegisterBillingAccount(ctx context.Context, request *co if errors.Is(err, customer.ErrNotFound) { return nil, connect.NewError(connect.CodeNotFound, ErrCustomerNotFound) } - return nil, mapBillingError(fmt.Errorf("RegisterBillingAccount.RegisterToProviderIfRequired: customer_id=%s: %w", request.Msg.GetId(), err)) + return nil, mapBillingError(ctx, fmt.Errorf("RegisterBillingAccount.RegisterToProviderIfRequired: customer_id=%s: %w", request.Msg.GetId(), err)) } return connect.NewResponse(&frontierv1beta1.RegisterBillingAccountResponse{}), nil } @@ -138,12 +138,12 @@ func (h *ConnectHandler) ListBillingAccounts(ctx context.Context, request *conne OrgID: request.Msg.GetOrgId(), }) if err != nil { - return nil, mapBillingError(fmt.Errorf("ListBillingAccounts.List: org_id=%s: %w", request.Msg.GetOrgId(), err)) + return nil, mapBillingError(ctx, fmt.Errorf("ListBillingAccounts.List: org_id=%s: %w", request.Msg.GetOrgId(), err)) } for _, v := range customerList { customerPB, err := transformCustomerToPB(v) if err != nil { - return nil, mapBillingError(fmt.Errorf("ListBillingAccounts: customer_id=%s: %w", v.ID, err)) + return nil, mapBillingError(ctx, fmt.Errorf("ListBillingAccounts: customer_id=%s: %w", v.ID, err)) } customers = append(customers, customerPB) } @@ -161,7 +161,7 @@ func (h *ConnectHandler) ListBillingAccounts(ctx context.Context, request *conne func (h *ConnectHandler) DeleteBillingAccount(ctx context.Context, request *connect.Request[frontierv1beta1.DeleteBillingAccountRequest]) (*connect.Response[frontierv1beta1.DeleteBillingAccountResponse], error) { err := h.customerService.Delete(ctx, request.Msg.GetId()) if err != nil { - return nil, mapBillingError(fmt.Errorf("DeleteBillingAccount.Delete: customer_id=%s: %w", request.Msg.GetId(), err)) + return nil, mapBillingError(ctx, fmt.Errorf("DeleteBillingAccount.Delete: customer_id=%s: %w", request.Msg.GetId(), err)) } return connect.NewResponse(&frontierv1beta1.DeleteBillingAccountResponse{}), nil } @@ -169,7 +169,7 @@ func (h *ConnectHandler) DeleteBillingAccount(ctx context.Context, request *conn func (h *ConnectHandler) EnableBillingAccount(ctx context.Context, request *connect.Request[frontierv1beta1.EnableBillingAccountRequest]) (*connect.Response[frontierv1beta1.EnableBillingAccountResponse], error) { err := h.customerService.Enable(ctx, request.Msg.GetId()) if err != nil { - return nil, mapBillingError(fmt.Errorf("EnableBillingAccount.Enable: customer_id=%s: %w", request.Msg.GetId(), err)) + return nil, mapBillingError(ctx, fmt.Errorf("EnableBillingAccount.Enable: customer_id=%s: %w", request.Msg.GetId(), err)) } return connect.NewResponse(&frontierv1beta1.EnableBillingAccountResponse{}), nil } @@ -177,7 +177,7 @@ func (h *ConnectHandler) EnableBillingAccount(ctx context.Context, request *conn func (h *ConnectHandler) DisableBillingAccount(ctx context.Context, request *connect.Request[frontierv1beta1.DisableBillingAccountRequest]) (*connect.Response[frontierv1beta1.DisableBillingAccountResponse], error) { err := h.customerService.Disable(ctx, request.Msg.GetId()) if err != nil { - return nil, mapBillingError(fmt.Errorf("DisableBillingAccount.Disable: customer_id=%s: %w", request.Msg.GetId(), err)) + return nil, mapBillingError(ctx, fmt.Errorf("DisableBillingAccount.Disable: customer_id=%s: %w", request.Msg.GetId(), err)) } return connect.NewResponse(&frontierv1beta1.DisableBillingAccountResponse{}), nil } @@ -185,7 +185,7 @@ func (h *ConnectHandler) DisableBillingAccount(ctx context.Context, request *con func (h *ConnectHandler) GetBillingBalance(ctx context.Context, request *connect.Request[frontierv1beta1.GetBillingBalanceRequest]) (*connect.Response[frontierv1beta1.GetBillingBalanceResponse], error) { balanceAmount, err := h.creditService.GetBalance(ctx, request.Msg.GetId()) if err != nil { - return nil, mapBillingError(fmt.Errorf("GetBillingBalance.GetBalance: customer_id=%s: %w", request.Msg.GetId(), err)) + return nil, mapBillingError(ctx, fmt.Errorf("GetBillingBalance.GetBalance: customer_id=%s: %w", request.Msg.GetId(), err)) } return connect.NewResponse(&frontierv1beta1.GetBillingBalanceResponse{ Balance: &frontierv1beta1.BillingAccount_Balance{ @@ -198,7 +198,7 @@ func (h *ConnectHandler) GetBillingBalance(ctx context.Context, request *connect func (h *ConnectHandler) HasTrialed(ctx context.Context, request *connect.Request[frontierv1beta1.HasTrialedRequest]) (*connect.Response[frontierv1beta1.HasTrialedResponse], error) { hasTrialed, err := h.subscriptionService.HasUserSubscribedBefore(ctx, request.Msg.GetId(), request.Msg.GetPlanId()) if err != nil { - return nil, mapBillingError(fmt.Errorf("HasTrialed.HasUserSubscribedBefore: customer_id=%s plan_id=%s: %w", request.Msg.GetId(), request.Msg.GetPlanId(), err)) + return nil, mapBillingError(ctx, fmt.Errorf("HasTrialed.HasUserSubscribedBefore: customer_id=%s plan_id=%s: %w", request.Msg.GetId(), request.Msg.GetPlanId(), err)) } return connect.NewResponse(&frontierv1beta1.HasTrialedResponse{ Trialed: hasTrialed, @@ -211,12 +211,12 @@ func (h *ConnectHandler) ListAllBillingAccounts(ctx context.Context, request *co OrgID: request.Msg.GetOrgId(), }) if err != nil { - return nil, mapBillingError(fmt.Errorf("ListAllBillingAccounts.List: org_id=%s: %w", request.Msg.GetOrgId(), err)) + return nil, mapBillingError(ctx, fmt.Errorf("ListAllBillingAccounts.List: org_id=%s: %w", request.Msg.GetOrgId(), err)) } for _, v := range customerList { customerPB, err := transformCustomerToPB(v) if err != nil { - return nil, mapBillingError(fmt.Errorf("ListAllBillingAccounts: customer_id=%s: %w", v.ID, err)) + return nil, mapBillingError(ctx, fmt.Errorf("ListAllBillingAccounts: customer_id=%s: %w", v.ID, err)) } customers = append(customers, customerPB) } @@ -265,7 +265,7 @@ func transformCustomerToPB(customer customer.Customer) (*frontierv1beta1.Billing func (h *ConnectHandler) UpdateBillingAccountLimits(ctx context.Context, request *connect.Request[frontierv1beta1.UpdateBillingAccountLimitsRequest]) (*connect.Response[frontierv1beta1.UpdateBillingAccountLimitsResponse], error) { _, err := h.customerService.UpdateCreditMinByID(ctx, request.Msg.GetId(), request.Msg.GetCreditMin()) if err != nil { - return nil, mapBillingError(fmt.Errorf("UpdateBillingAccountLimits.UpdateCreditMinByID: customer_id=%s credit_min=%d: %w", request.Msg.GetId(), request.Msg.GetCreditMin(), err)) + return nil, mapBillingError(ctx, fmt.Errorf("UpdateBillingAccountLimits.UpdateCreditMinByID: customer_id=%s credit_min=%d: %w", request.Msg.GetId(), request.Msg.GetCreditMin(), err)) } return connect.NewResponse(&frontierv1beta1.UpdateBillingAccountLimitsResponse{}), nil @@ -274,7 +274,7 @@ func (h *ConnectHandler) UpdateBillingAccountLimits(ctx context.Context, request func (h *ConnectHandler) GetBillingAccountDetails(ctx context.Context, request *connect.Request[frontierv1beta1.GetBillingAccountDetailsRequest]) (*connect.Response[frontierv1beta1.GetBillingAccountDetailsResponse], error) { details, err := h.customerService.GetDetails(ctx, request.Msg.GetId()) if err != nil { - return nil, mapBillingError(fmt.Errorf("GetBillingAccountDetails.GetDetails: customer_id=%s: %w", request.Msg.GetId(), err)) + return nil, mapBillingError(ctx, fmt.Errorf("GetBillingAccountDetails.GetDetails: customer_id=%s: %w", request.Msg.GetId(), err)) } return connect.NewResponse(&frontierv1beta1.GetBillingAccountDetailsResponse{ @@ -289,19 +289,19 @@ func (h *ConnectHandler) GetBillingAccount(ctx context.Context, request *connect if errors.Is(err, customer.ErrNotFound) { return nil, connect.NewError(connect.CodeNotFound, ErrNotFound) } - return nil, mapBillingError(fmt.Errorf("GetBillingAccount.GetByID: customer_id=%s: %w", request.Msg.GetId(), err)) + return nil, mapBillingError(ctx, fmt.Errorf("GetBillingAccount.GetByID: customer_id=%s: %w", request.Msg.GetId(), err)) } var paymentMethodsPbs []*frontierv1beta1.PaymentMethod if request.Msg.GetWithPaymentMethods() { pms, err := h.customerService.ListPaymentMethods(ctx, request.Msg.GetId()) if err != nil { - return nil, mapBillingError(fmt.Errorf("GetBillingAccount.ListPaymentMethods: customer_id=%s: %w", request.Msg.GetId(), err)) + return nil, mapBillingError(ctx, fmt.Errorf("GetBillingAccount.ListPaymentMethods: customer_id=%s: %w", request.Msg.GetId(), err)) } for _, v := range pms { pmPB, err := transformPaymentMethodToPB(v) if err != nil { - return nil, mapBillingError(fmt.Errorf("GetBillingAccount: payment_method_id=%s: %w", v.ID, err)) + return nil, mapBillingError(ctx, fmt.Errorf("GetBillingAccount: payment_method_id=%s: %w", v.ID, err)) } paymentMethodsPbs = append(paymentMethodsPbs, pmPB) } @@ -311,7 +311,7 @@ func (h *ConnectHandler) GetBillingAccount(ctx context.Context, request *connect if request.Msg.GetWithBillingDetails() { billingDetails, err := h.customerService.GetDetails(ctx, request.Msg.GetId()) if err != nil { - return nil, mapBillingError(fmt.Errorf("GetBillingAccount.GetDetails: customer_id=%s: %w", request.Msg.GetId(), err)) + return nil, mapBillingError(ctx, fmt.Errorf("GetBillingAccount.GetDetails: customer_id=%s: %w", request.Msg.GetId(), err)) } billingDetailsPb = &frontierv1beta1.BillingAccountDetails{ CreditMin: billingDetails.CreditMin, @@ -321,7 +321,7 @@ func (h *ConnectHandler) GetBillingAccount(ctx context.Context, request *connect customerPB, err := transformCustomerToPB(customerOb) if err != nil { - return nil, mapBillingError(fmt.Errorf("GetBillingAccount: customer_id=%s: %w", customerOb.ID, err)) + return nil, mapBillingError(ctx, fmt.Errorf("GetBillingAccount: customer_id=%s: %w", customerOb.ID, err)) } response := &frontierv1beta1.GetBillingAccountResponse{ @@ -367,7 +367,7 @@ func (h *ConnectHandler) UpdateBillingAccountDetails(ctx context.Context, reques DueInDays: request.Msg.GetDueInDays(), }) if err != nil { - return nil, mapBillingError(fmt.Errorf("UpdateBillingAccountDetails.UpdateDetails: customer_id=%s credit_min=%d due_in_days=%d: %w", request.Msg.GetId(), request.Msg.GetCreditMin(), request.Msg.GetDueInDays(), err)) + return nil, mapBillingError(ctx, fmt.Errorf("UpdateBillingAccountDetails.UpdateDetails: customer_id=%s credit_min=%d due_in_days=%d: %w", request.Msg.GetId(), request.Msg.GetCreditMin(), request.Msg.GetDueInDays(), err)) } // Add audit log - infer org_id from billing account diff --git a/internal/api/v1beta1connect/billing_errors.go b/internal/api/v1beta1connect/billing_errors.go index 29259d9ebe..3706dd4dbd 100644 --- a/internal/api/v1beta1connect/billing_errors.go +++ b/internal/api/v1beta1connect/billing_errors.go @@ -1,7 +1,9 @@ package v1beta1connect import ( + "context" "errors" + "log/slog" "regexp" "connectrpc.com/connect" @@ -13,9 +15,11 @@ import ( "github.com/raystack/frontier/billing/subscription" ) -// provider-generated object ids (random alphanumeric after the prefix). -// Caller-supplied values like coupon codes don't match this shape. -var providerIDPattern = regexp.MustCompile(`\b(cus|sub_sched|sub|in|ii|il|pi|pm|seti|si|price|prod|cs|ch|coup|promo)_(?:(?:test|live)_)?[A-Za-z0-9]{8,}\b`) +// provider-generated object ids: a short lowercase prefix, an optional +// test/live mode segment, and a long random alphanumeric part. The pattern is +// deliberately general so an id of an object type we haven't seen still gets +// masked; caller-supplied values like coupon codes don't match this shape. +var providerIDPattern = regexp.MustCompile(`\b([a-z]{2,10})_(?:(?:test|live)_)?[A-Za-z0-9]{8,}\b`) // redactedProviderError hides provider object ids in a message shown to the // caller, keeping the rest of the provider's text. @@ -25,15 +29,31 @@ func redactedProviderError(providerErr *billingerrors.ProviderError) error { // mapBillingError is the fallback for billing handlers in place of a bare // CodeInternal. Provider and account-state problems reach the caller as -// codes they can act on; everything else stays internal. -func mapBillingError(err error) *connect.Error { +// codes they can act on; everything else stays internal. Mapped errors are +// logged here with their full detail, because the caller-facing error is +// deliberately stripped of it and that error is all the logger interceptor +// sees. +func mapBillingError(ctx context.Context, err error) *connect.Error { + mapped := mapBillingErrorCode(err) + if mapped.Code() != connect.CodeInternal { + args := []any{"error", err, "code", mapped.Code().String()} + var providerErr *billingerrors.ProviderError + if errors.As(err, &providerErr) && providerErr.RequestID != "" { + args = append(args, "provider_request_id", providerErr.RequestID) + } + slog.WarnContext(ctx, "billing request failed", args...) + } + return mapped +} + +func mapBillingErrorCode(err error) *connect.Error { switch { case errors.Is(err, billingerrors.ErrProviderResourceMissing): var providerErr *billingerrors.ProviderError if errors.As(err, &providerErr) { return connect.NewError(connect.CodeFailedPrecondition, redactedProviderError(providerErr)) } - return connect.NewError(connect.CodeFailedPrecondition, ErrBillingProviderResourceMissing) + return connect.NewError(connect.CodeFailedPrecondition, billingerrors.ErrProviderResourceMissing) case errors.Is(err, billingerrors.ErrPaymentFailed): var providerErr *billingerrors.ProviderError if errors.As(err, &providerErr) { diff --git a/internal/api/v1beta1connect/billing_errors_test.go b/internal/api/v1beta1connect/billing_errors_test.go index cb1faf1fec..4b1aa90fc0 100644 --- a/internal/api/v1beta1connect/billing_errors_test.go +++ b/internal/api/v1beta1connect/billing_errors_test.go @@ -1,6 +1,7 @@ package v1beta1connect import ( + "context" "errors" "fmt" "testing" @@ -64,7 +65,16 @@ func TestMapBillingError(t *testing.T) { name: "provider resource missing without provider error", err: fmt.Errorf("GetUpcomingInvoice: %w", billingerrors.ErrProviderResourceMissing), wantCode: connect.CodeFailedPrecondition, - wantMsg: ErrBillingProviderResourceMissing.Error(), + wantMsg: billingerrors.ErrProviderResourceMissing.Error(), + }, + { + name: "provider resource missing masks an unlisted id kind", + err: fmt.Errorf("GetTransaction: %w", billingerrors.TranslateStripeError(&stripe.Error{ + Code: stripe.ErrorCodeResourceMissing, + Msg: "No such balance transaction: 'txn_3PqR7sTuVwXyZ012'", + })), + wantCode: connect.CodeFailedPrecondition, + wantMsg: "record no longer exists on the billing provider: No such balance transaction: 'txn_*****'", }, { name: "payment failed keeps provider message", @@ -129,7 +139,7 @@ func TestMapBillingError(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := mapBillingError(tt.err) + got := mapBillingError(context.Background(), tt.err) assert.Equal(t, tt.wantCode, got.Code()) assert.Equal(t, tt.wantMsg, got.Message()) }) diff --git a/internal/api/v1beta1connect/billing_invoice.go b/internal/api/v1beta1connect/billing_invoice.go index 124319e7a5..033066c3ba 100644 --- a/internal/api/v1beta1connect/billing_invoice.go +++ b/internal/api/v1beta1connect/billing_invoice.go @@ -22,13 +22,13 @@ func (h *ConnectHandler) ListAllInvoices(ctx context.Context, request *connect.R Pagination: paginate, }) if err != nil { - return nil, mapBillingError(fmt.Errorf("ListAllInvoices.ListAll: page_num=%d page_size=%d: %w", request.Msg.GetPageNum(), request.Msg.GetPageSize(), err)) + return nil, mapBillingError(ctx, fmt.Errorf("ListAllInvoices.ListAll: page_num=%d page_size=%d: %w", request.Msg.GetPageNum(), request.Msg.GetPageSize(), err)) } var invoicePBs []*frontierv1beta1.Invoice for _, v := range invoices { invoicePB, err := transformInvoiceToPB(v) if err != nil { - return nil, mapBillingError(fmt.Errorf("ListAllInvoices: invoice_id=%s: %w", v.ID, err)) + return nil, mapBillingError(ctx, fmt.Errorf("ListAllInvoices: invoice_id=%s: %w", v.ID, err)) } invoicePBs = append(invoicePBs, invoicePB) } @@ -53,7 +53,7 @@ func (h *ConnectHandler) ListInvoices(ctx context.Context, request *connect.Requ if errors.Is(err, customer.ErrInvalidUUID) || errors.Is(err, customer.ErrInvalidID) { return nil, connect.NewError(connect.CodeInvalidArgument, ErrBadRequest) } - return nil, mapBillingError(fmt.Errorf("ListInvoices.GetByOrgID: org_id=%s: %w", request.Msg.GetOrgId(), err)) + return nil, mapBillingError(ctx, fmt.Errorf("ListInvoices.GetByOrgID: org_id=%s: %w", request.Msg.GetOrgId(), err)) } billingID := cust.ID @@ -62,13 +62,13 @@ func (h *ConnectHandler) ListInvoices(ctx context.Context, request *connect.Requ NonZeroOnly: request.Msg.GetNonzeroAmountOnly(), }) if err != nil { - return nil, mapBillingError(fmt.Errorf("ListInvoices.List: org_id=%s billing_id=%s nonzero_amount_only=%v: %w", request.Msg.GetOrgId(), billingID, request.Msg.GetNonzeroAmountOnly(), err)) + return nil, mapBillingError(ctx, fmt.Errorf("ListInvoices.List: org_id=%s billing_id=%s nonzero_amount_only=%v: %w", request.Msg.GetOrgId(), billingID, request.Msg.GetNonzeroAmountOnly(), err)) } var invoicePBs []*frontierv1beta1.Invoice for _, v := range invoices { invoicePB, err := transformInvoiceToPB(v) if err != nil { - return nil, mapBillingError(fmt.Errorf("ListInvoices: invoice_id=%s: %w", v.ID, err)) + return nil, mapBillingError(ctx, fmt.Errorf("ListInvoices: invoice_id=%s: %w", v.ID, err)) } invoicePBs = append(invoicePBs, invoicePB) } @@ -97,17 +97,17 @@ func (h *ConnectHandler) GetUpcomingInvoice(ctx context.Context, request *connec if errors.Is(err, customer.ErrInvalidUUID) || errors.Is(err, customer.ErrInvalidID) { return nil, connect.NewError(connect.CodeInvalidArgument, ErrBadRequest) } - return nil, mapBillingError(fmt.Errorf("GetUpcomingInvoice.GetByOrgID: org_id=%s: %w", request.Msg.GetOrgId(), err)) + return nil, mapBillingError(ctx, fmt.Errorf("GetUpcomingInvoice.GetByOrgID: org_id=%s: %w", request.Msg.GetOrgId(), err)) } billingID := cust.ID invoice, err := h.invoiceService.GetUpcoming(ctx, billingID) if err != nil { - return nil, mapBillingError(fmt.Errorf("GetUpcomingInvoice.GetUpcoming: org_id=%s billing_id=%s: %w", request.Msg.GetOrgId(), billingID, err)) + return nil, mapBillingError(ctx, fmt.Errorf("GetUpcomingInvoice.GetUpcoming: org_id=%s billing_id=%s: %w", request.Msg.GetOrgId(), billingID, err)) } invoicePB, err := transformInvoiceToPB(invoice) if err != nil { - return nil, mapBillingError(fmt.Errorf("GetUpcomingInvoice: invoice_id=%s: %w", invoice.ID, err)) + return nil, mapBillingError(ctx, fmt.Errorf("GetUpcomingInvoice: invoice_id=%s: %w", invoice.ID, err)) } return connect.NewResponse(&frontierv1beta1.GetUpcomingInvoiceResponse{ @@ -152,7 +152,7 @@ func transformInvoiceToPB(i invoice.Invoice) (*frontierv1beta1.Invoice, error) { func (h *ConnectHandler) GenerateInvoices(ctx context.Context, request *connect.Request[frontierv1beta1.GenerateInvoicesRequest]) (*connect.Response[frontierv1beta1.GenerateInvoicesResponse], error) { err := h.invoiceService.TriggerCreditOverdraftInvoices(ctx) if err != nil { - return nil, mapBillingError(fmt.Errorf("GenerateInvoices.TriggerCreditOverdraftInvoices: %w", err)) + return nil, mapBillingError(ctx, fmt.Errorf("GenerateInvoices.TriggerCreditOverdraftInvoices: %w", err)) } return connect.NewResponse(&frontierv1beta1.GenerateInvoicesResponse{}), nil } @@ -175,7 +175,7 @@ func (h *ConnectHandler) SearchInvoices(ctx context.Context, request *connect.Re if errors.Is(err, invoice.ErrBadInput) { return nil, connect.NewError(connect.CodeInvalidArgument, err) } - return nil, mapBillingError(fmt.Errorf("SearchInvoices.SearchInvoices: query_offset=%d query_limit=%d: %w", rqlQuery.Offset, rqlQuery.Limit, err)) + return nil, mapBillingError(ctx, fmt.Errorf("SearchInvoices.SearchInvoices: query_offset=%d query_limit=%d: %w", rqlQuery.Offset, rqlQuery.Limit, err)) } for _, v := range invoicesData { diff --git a/internal/api/v1beta1connect/billing_plan.go b/internal/api/v1beta1connect/billing_plan.go index 4c1e39ec8c..5375bcd2ec 100644 --- a/internal/api/v1beta1connect/billing_plan.go +++ b/internal/api/v1beta1connect/billing_plan.go @@ -79,17 +79,17 @@ func (h *ConnectHandler) CreatePlan(ctx context.Context, request *connect.Reques Products: products, }) if err != nil { - return nil, mapBillingError(fmt.Errorf("CreatePlan.UpsertPlans: plan_name=%s plan_title=%s interval=%s product_count=%d: %w", planToCreate.Name, planToCreate.Title, planToCreate.Interval, len(products), err)) + return nil, mapBillingError(ctx, fmt.Errorf("CreatePlan.UpsertPlans: plan_name=%s plan_title=%s interval=%s product_count=%d: %w", planToCreate.Name, planToCreate.Title, planToCreate.Interval, len(products), err)) } newPlan, err := h.planService.GetByID(ctx, planToCreate.Name) if err != nil { - return nil, mapBillingError(fmt.Errorf("CreatePlan.GetByID: plan_name=%s: %w", planToCreate.Name, err)) + return nil, mapBillingError(ctx, fmt.Errorf("CreatePlan.GetByID: plan_name=%s: %w", planToCreate.Name, err)) } planPB, err := transformPlanToPB(newPlan) if err != nil { - return nil, mapBillingError(fmt.Errorf("CreatePlan: plan_id=%s: %w", newPlan.ID, err)) + return nil, mapBillingError(ctx, fmt.Errorf("CreatePlan: plan_id=%s: %w", newPlan.ID, err)) } return connect.NewResponse(&frontierv1beta1.CreatePlanResponse{Plan: planPB}), nil @@ -99,12 +99,12 @@ func (h *ConnectHandler) ListPlans(ctx context.Context, request *connect.Request var plans []*frontierv1beta1.Plan planList, err := h.planService.List(ctx, plan.Filter{}) if err != nil { - return nil, mapBillingError(fmt.Errorf("ListPlans.List: %w", err)) + return nil, mapBillingError(ctx, fmt.Errorf("ListPlans.List: %w", err)) } for _, v := range planList { planPB, err := transformPlanToPB(v) if err != nil { - return nil, mapBillingError(fmt.Errorf("ListPlans: plan_id=%s: %w", v.ID, err)) + return nil, mapBillingError(ctx, fmt.Errorf("ListPlans: plan_id=%s: %w", v.ID, err)) } plans = append(plans, planPB) } @@ -115,12 +115,12 @@ func (h *ConnectHandler) ListPlans(ctx context.Context, request *connect.Request func (h *ConnectHandler) GetPlan(ctx context.Context, request *connect.Request[frontierv1beta1.GetPlanRequest]) (*connect.Response[frontierv1beta1.GetPlanResponse], error) { planOb, err := h.planService.GetByID(ctx, request.Msg.GetId()) if err != nil { - return nil, mapBillingError(fmt.Errorf("GetPlan.GetByID: plan_id=%s: %w", request.Msg.GetId(), err)) + return nil, mapBillingError(ctx, fmt.Errorf("GetPlan.GetByID: plan_id=%s: %w", request.Msg.GetId(), err)) } planPB, err := transformPlanToPB(planOb) if err != nil { - return nil, mapBillingError(fmt.Errorf("GetPlan: plan_id=%s: %w", planOb.ID, err)) + return nil, mapBillingError(ctx, fmt.Errorf("GetPlan: plan_id=%s: %w", planOb.ID, err)) } return connect.NewResponse(&frontierv1beta1.GetPlanResponse{Plan: planPB}), nil diff --git a/internal/api/v1beta1connect/billing_product.go b/internal/api/v1beta1connect/billing_product.go index cafc609bd4..8626aaa8e4 100644 --- a/internal/api/v1beta1connect/billing_product.go +++ b/internal/api/v1beta1connect/billing_product.go @@ -15,12 +15,12 @@ func (h *ConnectHandler) ListProducts(ctx context.Context, request *connect.Requ var products []*frontierv1beta1.Product productsList, err := h.productService.List(ctx, product.Filter{}) if err != nil { - return nil, mapBillingError(fmt.Errorf("ListProducts.List: %w", err)) + return nil, mapBillingError(ctx, fmt.Errorf("ListProducts.List: %w", err)) } for _, v := range productsList { productPB, err := transformProductToPB(v) if err != nil { - return nil, mapBillingError(fmt.Errorf("ListProducts: entity_id=%s: %w", v.ID, err)) + return nil, mapBillingError(ctx, fmt.Errorf("ListProducts: entity_id=%s: %w", v.ID, err)) } products = append(products, productPB) } @@ -33,12 +33,12 @@ func (h *ConnectHandler) ListProducts(ctx context.Context, request *connect.Requ func (h *ConnectHandler) GetProduct(ctx context.Context, request *connect.Request[frontierv1beta1.GetProductRequest]) (*connect.Response[frontierv1beta1.GetProductResponse], error) { product, err := h.productService.GetByID(ctx, request.Msg.GetId()) if err != nil { - return nil, mapBillingError(fmt.Errorf("GetProduct.GetByID: product_id=%s: %w", request.Msg.GetId(), err)) + return nil, mapBillingError(ctx, fmt.Errorf("GetProduct.GetByID: product_id=%s: %w", request.Msg.GetId(), err)) } productPB, err := transformProductToPB(product) if err != nil { - return nil, mapBillingError(fmt.Errorf("GetProduct: entity_id=%s: %w", product.ID, err)) + return nil, mapBillingError(ctx, fmt.Errorf("GetProduct: entity_id=%s: %w", product.ID, err)) } return connect.NewResponse(&frontierv1beta1.GetProductResponse{ @@ -93,14 +93,14 @@ func (h *ConnectHandler) CreateProduct(ctx context.Context, request *connect.Req Metadata: metaDataMap, }) if err != nil { - return nil, mapBillingError(fmt.Errorf("CreateProduct.Create: product_name=%s product_title=%s behavior=%s price_count=%d feature_count=%d: %w", + return nil, mapBillingError(ctx, fmt.Errorf("CreateProduct.Create: product_name=%s product_title=%s behavior=%s price_count=%d feature_count=%d: %w", request.Msg.GetBody().GetName(), request.Msg.GetBody().GetTitle(), request.Msg.GetBody().GetBehavior(), len(productPrices), len(productFeatures), err)) } productPB, err := transformProductToPB(newProduct) if err != nil { - return nil, mapBillingError(fmt.Errorf("CreateProduct: entity_id=%s: %w", newProduct.ID, err)) + return nil, mapBillingError(ctx, fmt.Errorf("CreateProduct: entity_id=%s: %w", newProduct.ID, err)) } return connect.NewResponse(&frontierv1beta1.CreateProductResponse{ @@ -169,11 +169,11 @@ func (h *ConnectHandler) UpdateProduct(ctx context.Context, request *connect.Req if errors.Is(err, product.ErrInvalidDetail) { return nil, connect.NewError(connect.CodeInvalidArgument, wrapped) } - return nil, mapBillingError(wrapped) + return nil, mapBillingError(ctx, wrapped) } productPb, err := transformProductToPB(updatedProduct) if err != nil { - return nil, mapBillingError(fmt.Errorf("UpdateProduct: entity_id=%s: %w", updatedProduct.ID, err)) + return nil, mapBillingError(ctx, fmt.Errorf("UpdateProduct: entity_id=%s: %w", updatedProduct.ID, err)) } return connect.NewResponse(&frontierv1beta1.UpdateProductResponse{ @@ -184,14 +184,14 @@ func (h *ConnectHandler) UpdateProduct(ctx context.Context, request *connect.Req func (h *ConnectHandler) ListFeatures(ctx context.Context, request *connect.Request[frontierv1beta1.ListFeaturesRequest]) (*connect.Response[frontierv1beta1.ListFeaturesResponse], error) { features, err := h.productService.ListFeatures(ctx, product.Filter{}) if err != nil { - return nil, mapBillingError(fmt.Errorf("ListFeatures.ListFeatures: %w", err)) + return nil, mapBillingError(ctx, fmt.Errorf("ListFeatures.ListFeatures: %w", err)) } var featuresPB []*frontierv1beta1.Feature for _, v := range features { f, err := transformFeatureToPB(v) if err != nil { - return nil, mapBillingError(fmt.Errorf("ListFeatures: entity_id=%s: %w", v.ID, err)) + return nil, mapBillingError(ctx, fmt.Errorf("ListFeatures: entity_id=%s: %w", v.ID, err)) } featuresPB = append(featuresPB, f) } @@ -213,13 +213,13 @@ func (h *ConnectHandler) CreateFeature(ctx context.Context, request *connect.Req if errors.Is(err, product.ErrInvalidFeatureDetail) { return nil, connect.NewError(connect.CodeInvalidArgument, ErrBadRequest) } - return nil, mapBillingError(fmt.Errorf("CreateFeature.UpsertFeature: feature_name=%s feature_title=%s product_ids=%v: %w", + return nil, mapBillingError(ctx, fmt.Errorf("CreateFeature.UpsertFeature: feature_name=%s feature_title=%s product_ids=%v: %w", request.Msg.GetBody().GetName(), request.Msg.GetBody().GetTitle(), request.Msg.GetBody().GetProductIds(), err)) } featurePB, err := transformFeatureToPB(newFeature) if err != nil { - return nil, mapBillingError(fmt.Errorf("CreateFeature: entity_id=%s: %w", newFeature.ID, err)) + return nil, mapBillingError(ctx, fmt.Errorf("CreateFeature: entity_id=%s: %w", newFeature.ID, err)) } return connect.NewResponse(&frontierv1beta1.CreateFeatureResponse{ @@ -240,13 +240,13 @@ func (h *ConnectHandler) UpdateFeature(ctx context.Context, request *connect.Req if errors.Is(err, product.ErrInvalidFeatureDetail) { return nil, connect.NewError(connect.CodeInvalidArgument, ErrBadRequest) } - return nil, mapBillingError(fmt.Errorf("UpdateFeature.UpsertFeature: feature_id=%s feature_name=%s feature_title=%s product_ids=%v: %w", + return nil, mapBillingError(ctx, fmt.Errorf("UpdateFeature.UpsertFeature: feature_id=%s feature_name=%s feature_title=%s product_ids=%v: %w", request.Msg.GetId(), request.Msg.GetBody().GetName(), request.Msg.GetBody().GetTitle(), request.Msg.GetBody().GetProductIds(), err)) } featurePB, err := transformFeatureToPB(updatedFeature) if err != nil { - return nil, mapBillingError(fmt.Errorf("UpdateFeature: entity_id=%s: %w", updatedFeature.ID, err)) + return nil, mapBillingError(ctx, fmt.Errorf("UpdateFeature: entity_id=%s: %w", updatedFeature.ID, err)) } return connect.NewResponse(&frontierv1beta1.UpdateFeatureResponse{ @@ -257,12 +257,12 @@ func (h *ConnectHandler) UpdateFeature(ctx context.Context, request *connect.Req func (h *ConnectHandler) GetFeature(ctx context.Context, request *connect.Request[frontierv1beta1.GetFeatureRequest]) (*connect.Response[frontierv1beta1.GetFeatureResponse], error) { feature, err := h.productService.GetFeatureByID(ctx, request.Msg.GetId()) if err != nil { - return nil, mapBillingError(fmt.Errorf("GetFeature.GetFeatureByID: feature_id=%s: %w", request.Msg.GetId(), err)) + return nil, mapBillingError(ctx, fmt.Errorf("GetFeature.GetFeatureByID: feature_id=%s: %w", request.Msg.GetId(), err)) } featurePB, err := transformFeatureToPB(feature) if err != nil { - return nil, mapBillingError(fmt.Errorf("GetFeature: entity_id=%s: %w", feature.ID, err)) + return nil, mapBillingError(ctx, fmt.Errorf("GetFeature: entity_id=%s: %w", feature.ID, err)) } return connect.NewResponse(&frontierv1beta1.GetFeatureResponse{ diff --git a/internal/api/v1beta1connect/billing_subscription.go b/internal/api/v1beta1connect/billing_subscription.go index ddbb068bd6..d2b3c8a136 100644 --- a/internal/api/v1beta1connect/billing_subscription.go +++ b/internal/api/v1beta1connect/billing_subscription.go @@ -28,7 +28,7 @@ func (h *ConnectHandler) ListSubscriptions(ctx context.Context, request *connect Subscriptions: []*frontierv1beta1.Subscription{}, }), nil } - return nil, mapBillingError(fmt.Errorf("ListSubscriptions.GetByOrgID: org_id=%s: %w", request.Msg.GetOrgId(), err)) + return nil, mapBillingError(ctx, fmt.Errorf("ListSubscriptions.GetByOrgID: org_id=%s: %w", request.Msg.GetOrgId(), err)) } billingID := cust.ID @@ -48,13 +48,13 @@ func (h *ConnectHandler) ListSubscriptions(ctx context.Context, request *connect PlanID: planID, }) if err != nil { - return nil, mapBillingError(fmt.Errorf("ListSubscriptions.List: billing_id=%s org_id=%s state=%s plan_id=%s: %w", + return nil, mapBillingError(ctx, fmt.Errorf("ListSubscriptions.List: billing_id=%s org_id=%s state=%s plan_id=%s: %w", billingID, request.Msg.GetOrgId(), request.Msg.GetState(), planID, err)) } for _, v := range subscriptionList { subscriptionPB, err := transformSubscriptionToPB(v) if err != nil { - return nil, mapBillingError(fmt.Errorf("ListSubscriptions: entity_id=%s: %w", v.ID, err)) + return nil, mapBillingError(ctx, fmt.Errorf("ListSubscriptions: entity_id=%s: %w", v.ID, err)) } subscriptions = append(subscriptions, subscriptionPB) } @@ -72,12 +72,12 @@ func (h *ConnectHandler) ListSubscriptions(ctx context.Context, request *connect func (h *ConnectHandler) GetSubscription(ctx context.Context, request *connect.Request[frontierv1beta1.GetSubscriptionRequest]) (*connect.Response[frontierv1beta1.GetSubscriptionResponse], error) { subscription, err := h.subscriptionService.GetByID(ctx, request.Msg.GetId()) if err != nil { - return nil, mapBillingError(fmt.Errorf("GetSubscription.GetByID: subscription_id=%s: %w", request.Msg.GetId(), err)) + return nil, mapBillingError(ctx, fmt.Errorf("GetSubscription.GetByID: subscription_id=%s: %w", request.Msg.GetId(), err)) } subscriptionPB, err := transformSubscriptionToPB(subscription) if err != nil { - return nil, mapBillingError(fmt.Errorf("GetSubscription: entity_id=%s: %w", subscription.ID, err)) + return nil, mapBillingError(ctx, fmt.Errorf("GetSubscription: entity_id=%s: %w", subscription.ID, err)) } response := &frontierv1beta1.GetSubscriptionResponse{ Subscription: subscriptionPB, @@ -92,7 +92,7 @@ func (h *ConnectHandler) GetSubscription(ctx context.Context, request *connect.R func (h *ConnectHandler) CancelSubscription(ctx context.Context, request *connect.Request[frontierv1beta1.CancelSubscriptionRequest]) (*connect.Response[frontierv1beta1.CancelSubscriptionResponse], error) { _, err := h.subscriptionService.Cancel(ctx, request.Msg.GetId(), request.Msg.GetImmediate()) if err != nil { - return nil, mapBillingError(fmt.Errorf("CancelSubscription.Cancel: subscription_id=%s immediate=%v: %w", request.Msg.GetId(), request.Msg.GetImmediate(), err)) + return nil, mapBillingError(ctx, fmt.Errorf("CancelSubscription.Cancel: subscription_id=%s immediate=%v: %w", request.Msg.GetId(), request.Msg.GetImmediate(), err)) } return connect.NewResponse(&frontierv1beta1.CancelSubscriptionResponse{}), nil } @@ -126,7 +126,7 @@ func (h *ConnectHandler) ChangeSubscription(ctx context.Context, request *connec if errors.Is(err, subscription.ErrAlreadyOnSamePlan) { return nil, connect.NewError(connect.CodeInvalidArgument, ErrAlreadyOnSamePlan) } - return nil, mapBillingError(fmt.Errorf("ChangeSubscription.ChangePlan: subscription_id=%s plan_id=%s immediate=%v cancel_upcoming=%v: %w", + return nil, mapBillingError(ctx, fmt.Errorf("ChangeSubscription.ChangePlan: subscription_id=%s plan_id=%s immediate=%v cancel_upcoming=%v: %w", request.Msg.GetId(), changeReq.PlanID, changeReq.Immediate, changeReq.CancelUpcoming, err)) } diff --git a/internal/api/v1beta1connect/billing_usage.go b/internal/api/v1beta1connect/billing_usage.go index 370e926c11..ff7564cbf8 100644 --- a/internal/api/v1beta1connect/billing_usage.go +++ b/internal/api/v1beta1connect/billing_usage.go @@ -27,7 +27,7 @@ func (h *ConnectHandler) CreateBillingUsage(ctx context.Context, request *connec if errors.Is(err, customer.ErrInvalidUUID) || errors.Is(err, customer.ErrInvalidID) { return nil, connect.NewError(connect.CodeInvalidArgument, err) } - return nil, mapBillingError(fmt.Errorf("CreateBillingUsage.GetByOrgID: org_id=%s: %w", request.Msg.GetOrgId(), err)) + return nil, mapBillingError(ctx, fmt.Errorf("CreateBillingUsage.GetByOrgID: org_id=%s: %w", request.Msg.GetOrgId(), err)) } createRequests := make([]usage.Usage, 0, len(request.Msg.GetUsages())) @@ -56,7 +56,7 @@ func (h *ConnectHandler) CreateBillingUsage(ctx context.Context, request *connec if errors.Is(err, credit.ErrAlreadyApplied) { return nil, connect.NewError(connect.CodeAlreadyExists, ErrAlreadyApplied) } - return nil, mapBillingError(fmt.Errorf("CreateBillingUsage.Report: billing_id=%s org_id=%s usage_count=%d: %w", + return nil, mapBillingError(ctx, fmt.Errorf("CreateBillingUsage.Report: billing_id=%s org_id=%s usage_count=%d: %w", cust.ID, request.Msg.GetOrgId(), len(createRequests), err)) } @@ -77,7 +77,7 @@ func (h *ConnectHandler) ListBillingTransactions(ctx context.Context, request *c if errors.Is(err, customer.ErrInvalidUUID) || errors.Is(err, customer.ErrInvalidID) { return nil, connect.NewError(connect.CodeInvalidArgument, ErrBadRequest) } - return nil, mapBillingError(fmt.Errorf("ListBillingTransactions.GetByOrgID: org_id=%s: %w", request.Msg.GetOrgId(), err)) + return nil, mapBillingError(ctx, fmt.Errorf("ListBillingTransactions.GetByOrgID: org_id=%s: %w", request.Msg.GetOrgId(), err)) } billingID := cust.ID @@ -101,13 +101,13 @@ func (h *ConnectHandler) ListBillingTransactions(ctx context.Context, request *c EndRange: endRange, }) if err != nil { - return nil, mapBillingError(fmt.Errorf("ListBillingTransactions.List: org_id=%s billing_id=%s start_range=%v end_range=%v: %w", + return nil, mapBillingError(ctx, fmt.Errorf("ListBillingTransactions.List: org_id=%s billing_id=%s start_range=%v end_range=%v: %w", request.Msg.GetOrgId(), billingID, startRange, endRange, err)) } for _, v := range transactionsList { transactionPB, err := transformTransactionToPB(v) if err != nil { - return nil, mapBillingError(fmt.Errorf("ListBillingTransactions: entity_id=%s: %w", v.ID, err)) + return nil, mapBillingError(ctx, fmt.Errorf("ListBillingTransactions: entity_id=%s: %w", v.ID, err)) } transactions = append(transactions, transactionPB) } @@ -139,13 +139,13 @@ func (h *ConnectHandler) TotalDebitedTransactions(ctx context.Context, request * if errors.Is(err, customer.ErrInvalidUUID) || errors.Is(err, customer.ErrInvalidID) { return nil, connect.NewError(connect.CodeInvalidArgument, ErrBadRequest) } - return nil, mapBillingError(fmt.Errorf("TotalDebitedTransactions.GetByOrgID: org_id=%s: %w", request.Msg.GetOrgId(), err)) + return nil, mapBillingError(ctx, fmt.Errorf("TotalDebitedTransactions.GetByOrgID: org_id=%s: %w", request.Msg.GetOrgId(), err)) } billingID := cust.ID debitAmount, err := h.creditService.GetTotalDebitedAmount(ctx, billingID) if err != nil { - return nil, mapBillingError(fmt.Errorf("TotalDebitedTransactions.GetTotalDebitedAmount: org_id=%s billing_id=%s: %w", + return nil, mapBillingError(ctx, fmt.Errorf("TotalDebitedTransactions.GetTotalDebitedAmount: org_id=%s billing_id=%s: %w", request.Msg.GetOrgId(), billingID, err)) } @@ -186,7 +186,7 @@ func (h *ConnectHandler) RevertBillingUsage(ctx context.Context, request *connec if errors.Is(err, customer.ErrInvalidUUID) || errors.Is(err, customer.ErrInvalidID) { return nil, connect.NewError(connect.CodeInvalidArgument, err) } - return nil, mapBillingError(fmt.Errorf("RevertBillingUsage.GetByOrgID: org_id=%s: %w", request.Msg.GetOrgId(), err)) + return nil, mapBillingError(ctx, fmt.Errorf("RevertBillingUsage.GetByOrgID: org_id=%s: %w", request.Msg.GetOrgId(), err)) } if err := h.usageService.Revert(ctx, cust.ID, @@ -202,7 +202,7 @@ func (h *ConnectHandler) RevertBillingUsage(ctx context.Context, request *connec } else if errors.Is(err, credit.ErrAlreadyApplied) { return nil, connect.NewError(connect.CodeInvalidArgument, err) } - return nil, mapBillingError(fmt.Errorf("RevertBillingUsage.Revert: billing_id=%s org_id=%s usage_id=%s amount=%d: %w", + return nil, mapBillingError(ctx, fmt.Errorf("RevertBillingUsage.Revert: billing_id=%s org_id=%s usage_id=%s amount=%d: %w", cust.ID, request.Msg.GetOrgId(), request.Msg.GetUsageId(), request.Msg.GetAmount(), err)) } return connect.NewResponse(&frontierv1beta1.RevertBillingUsageResponse{}), nil diff --git a/internal/api/v1beta1connect/billing_webhook.go b/internal/api/v1beta1connect/billing_webhook.go index 4fca3d459b..5846023bc9 100644 --- a/internal/api/v1beta1connect/billing_webhook.go +++ b/internal/api/v1beta1connect/billing_webhook.go @@ -26,7 +26,7 @@ func (h *ConnectHandler) BillingWebhookCallback(ctx context.Context, request *co Name: request.Msg.GetProvider(), Body: request.Msg.GetBody(), }); err != nil { - return nil, mapBillingError(fmt.Errorf("BillingWebhookCallback.BillingWebhook: provider=%s: %w", request.Msg.GetProvider(), err)) + return nil, mapBillingError(ctx, fmt.Errorf("BillingWebhookCallback.BillingWebhook: provider=%s: %w", request.Msg.GetProvider(), err)) } return connect.NewResponse(&frontierv1beta1.BillingWebhookCallbackResponse{}), nil } diff --git a/internal/api/v1beta1connect/errors.go b/internal/api/v1beta1connect/errors.go index 302048ecd7..effd135b42 100644 --- a/internal/api/v1beta1connect/errors.go +++ b/internal/api/v1beta1connect/errors.go @@ -5,72 +5,71 @@ import ( ) var ( - ErrBadRequest = errors.New("invalid syntax in body") - ErrInvalidMetadata = errors.New("metadata schema validation failed") - ErrOperationUnsupported = errors.New("operation not supported") - ErrInternalServerError = errors.ErrInternalServerError - ErrUnauthenticated = errors.New("not authenticated") - ErrUnauthorized = errors.New("not authorized") - ErrNotFound = errors.New("not found") - ErrInvalidEmail = errors.New("Invalid email") - ErrUserNotExist = errors.New("user doesn't exist") - ErrInvalidNamesapceOrID = errors.New("namespace and ID cannot be empty") - ErrConflictRequest = errors.New("already exist") - ErrBadBodyMetaSchemaError = errors.New(ErrBadRequest.Error() + " : " + ErrInvalidMetadata.Error()) - ErrInvalidActorType = errors.New("invalid actor type") - ErrActivityRequired = errors.New("activity is required") - ErrStatusRequired = errors.New("status is required") - ErrProspectIdRequired = errors.New("prospect ID is required") - ErrProspectNotFound = errors.New("record not found for the given input") - ErrRQLParse = errors.New("error parsing RQL query") - ErrOrgDisabled = errors.New("org is disabled. Please contact your administrator to enable it") - ErrMinAdminCount = errors.New("org must have at least one admin, consider adding another admin before removing") - ErrLastOwnerRole = errors.New("org must have at least one owner, consider assigning another owner before changing this user's role") - ErrDomainNotFound = errors.New("domain whitelist request doesn't exist") - ErrDomainAlreadyExists = errors.New("domain name already exists for that organization") - ErrInvalidHost = errors.New("invalid domain, no such host found") - ErrTXTRecordNotFound = errors.New("required TXT record not found for domain verification") - ErrDomainMismatch = errors.New("user and org's whitelisted domains doesn't match") - ErrInvitationNotFound = errors.New("invitation not found") - ErrInvitationExpired = errors.New("invitation expired") - ErrAlreadyMember = errors.New("principal is already a member of the resource") - ErrNotMember = errors.New("principal is not a member of the resource") - ErrInvalidOrgRole = errors.New("role is not valid for organization scope") - ErrInvalidProjectRole = errors.New("role is not valid for project scope") - ErrInvalidGroupRole = errors.New("role is not valid for group scope") - ErrLastGroupOwnerRole = errors.New("group must have at least one owner, consider assigning another owner before changing this user's role") - ErrNotOrgMember = errors.New("principal is not a member of the organization") - ErrEmptyEmailID = errors.New("email id is empty") - ErrEmailConflict = errors.New("user email can't be updated") - ErrCustomerNotFound = errors.New("customer doesn't exist") - ErrServiceUserNotFound = errors.New("service user not found") - ErrServiceUserCredNotFound = errors.New("service user credentials not found") - ErrConflictingPlanChange = errors.New("cannot change plan and cancel upcoming changes at the same time") - ErrNoChangeRequested = errors.New("no change requested") - ErrPerSeatLimitReached = errors.New("per seat limit reached") - ErrAlreadyOnSamePlan = errors.New("already on same plan") - ErrBillingProviderNotSupported = errors.New("provider not supported") - ErrBillingProviderResourceMissing = errors.New("billing account is no longer linked to the payment provider") - ErrBillingProviderUnavailable = errors.New("billing provider is unavailable, retry later") - ErrSubscriptionProviderMissing = errors.New("subscription no longer exists on the billing provider") - ErrInsufficientCredits = errors.New("insufficient credits") - ErrAlreadyApplied = errors.New("credits already applied") - ErrInvalidRoleID = errors.New("role id is invalid") - ErrNamespaceSplitNotation = errors.New("subject/object should be provided as 'namespace:uuid'") - ErrPolicyNotFound = errors.New("policy doesn't exist") - ErrProjectNotFound = errors.New("project doesn't exist") - ErrGroupNotFound = errors.New("group doesn't exist") - ErrOrgNotFound = errors.New("org doesn't exist") - ErrGroupMinOwnerCount = errors.New("group must have at least one owner, consider adding another owner before removing") - ErrPortalChangesKycCompleted = errors.New("customer portal changes not allowed: organization kyc completed") - ErrResourceNotFound = errors.New("resource doesn't exist") - ErrInvalidPreferenceFilter = errors.New("invalid preference filter set") - ErrTraitNotFound = errors.New("preference trait not found, preferences can only be created with valid trait") - ErrInvalidPreferenceValue = errors.New("invalid value for preference") - ErrInvalidPreferenceScope = errors.New("invalid scope: trait does not support scoping or scope_type/scope_id must be provided together") - ErrMetaschemaNotFound = errors.New("metaschema doesn't exist") - ErrSessionNotFound = errors.New("session doesn't exist") - ErrInvalidSessionID = errors.New("invalid session_id format: must be a valid UUID") - ErrInvalidUserID = errors.New("invalid user_id format: must be a valid UUID") - ErrRoleNotFound = errors.New("role doesn't exist") + ErrBadRequest = errors.New("invalid syntax in body") + ErrInvalidMetadata = errors.New("metadata schema validation failed") + ErrOperationUnsupported = errors.New("operation not supported") + ErrInternalServerError = errors.ErrInternalServerError + ErrUnauthenticated = errors.New("not authenticated") + ErrUnauthorized = errors.New("not authorized") + ErrNotFound = errors.New("not found") + ErrInvalidEmail = errors.New("Invalid email") + ErrUserNotExist = errors.New("user doesn't exist") + ErrInvalidNamesapceOrID = errors.New("namespace and ID cannot be empty") + ErrConflictRequest = errors.New("already exist") + ErrBadBodyMetaSchemaError = errors.New(ErrBadRequest.Error() + " : " + ErrInvalidMetadata.Error()) + ErrInvalidActorType = errors.New("invalid actor type") + ErrActivityRequired = errors.New("activity is required") + ErrStatusRequired = errors.New("status is required") + ErrProspectIdRequired = errors.New("prospect ID is required") + ErrProspectNotFound = errors.New("record not found for the given input") + ErrRQLParse = errors.New("error parsing RQL query") + ErrOrgDisabled = errors.New("org is disabled. Please contact your administrator to enable it") + ErrMinAdminCount = errors.New("org must have at least one admin, consider adding another admin before removing") + ErrLastOwnerRole = errors.New("org must have at least one owner, consider assigning another owner before changing this user's role") + ErrDomainNotFound = errors.New("domain whitelist request doesn't exist") + ErrDomainAlreadyExists = errors.New("domain name already exists for that organization") + ErrInvalidHost = errors.New("invalid domain, no such host found") + ErrTXTRecordNotFound = errors.New("required TXT record not found for domain verification") + ErrDomainMismatch = errors.New("user and org's whitelisted domains doesn't match") + ErrInvitationNotFound = errors.New("invitation not found") + ErrInvitationExpired = errors.New("invitation expired") + ErrAlreadyMember = errors.New("principal is already a member of the resource") + ErrNotMember = errors.New("principal is not a member of the resource") + ErrInvalidOrgRole = errors.New("role is not valid for organization scope") + ErrInvalidProjectRole = errors.New("role is not valid for project scope") + ErrInvalidGroupRole = errors.New("role is not valid for group scope") + ErrLastGroupOwnerRole = errors.New("group must have at least one owner, consider assigning another owner before changing this user's role") + ErrNotOrgMember = errors.New("principal is not a member of the organization") + ErrEmptyEmailID = errors.New("email id is empty") + ErrEmailConflict = errors.New("user email can't be updated") + ErrCustomerNotFound = errors.New("customer doesn't exist") + ErrServiceUserNotFound = errors.New("service user not found") + ErrServiceUserCredNotFound = errors.New("service user credentials not found") + ErrConflictingPlanChange = errors.New("cannot change plan and cancel upcoming changes at the same time") + ErrNoChangeRequested = errors.New("no change requested") + ErrPerSeatLimitReached = errors.New("per seat limit reached") + ErrAlreadyOnSamePlan = errors.New("already on same plan") + ErrBillingProviderNotSupported = errors.New("provider not supported") + ErrBillingProviderUnavailable = errors.New("billing provider is unavailable, retry later") + ErrSubscriptionProviderMissing = errors.New("subscription no longer exists on the billing provider") + ErrInsufficientCredits = errors.New("insufficient credits") + ErrAlreadyApplied = errors.New("credits already applied") + ErrInvalidRoleID = errors.New("role id is invalid") + ErrNamespaceSplitNotation = errors.New("subject/object should be provided as 'namespace:uuid'") + ErrPolicyNotFound = errors.New("policy doesn't exist") + ErrProjectNotFound = errors.New("project doesn't exist") + ErrGroupNotFound = errors.New("group doesn't exist") + ErrOrgNotFound = errors.New("org doesn't exist") + ErrGroupMinOwnerCount = errors.New("group must have at least one owner, consider adding another owner before removing") + ErrPortalChangesKycCompleted = errors.New("customer portal changes not allowed: organization kyc completed") + ErrResourceNotFound = errors.New("resource doesn't exist") + ErrInvalidPreferenceFilter = errors.New("invalid preference filter set") + ErrTraitNotFound = errors.New("preference trait not found, preferences can only be created with valid trait") + ErrInvalidPreferenceValue = errors.New("invalid value for preference") + ErrInvalidPreferenceScope = errors.New("invalid scope: trait does not support scoping or scope_type/scope_id must be provided together") + ErrMetaschemaNotFound = errors.New("metaschema doesn't exist") + ErrSessionNotFound = errors.New("session doesn't exist") + ErrInvalidSessionID = errors.New("invalid session_id format: must be a valid UUID") + ErrInvalidUserID = errors.New("invalid user_id format: must be a valid UUID") + ErrRoleNotFound = errors.New("role doesn't exist") )