Skip to content

Commit 76987b9

Browse files
committed
fix(api): non-existent tax code
1 parent 8edef47 commit 76987b9

6 files changed

Lines changed: 117 additions & 24 deletions

File tree

openmeter/billing/charges/charge.go

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,33 @@ func (c ChargeIntent) GetUniqueReferenceID() (*string, error) {
385385
return nil, fmt.Errorf("invalid charge type: %s", c.t)
386386
}
387387

388-
// Meta returns the shared meta.Intent embedded in every charge type.
388+
// TaxCodeID returns the intent's configured tax code ID.
389+
// It is empty when no tax code is set.
390+
func (i ChargeIntent) TaxCodeID() (string, error) {
391+
switch i.t {
392+
case meta.ChargeTypeFlatFee:
393+
if i.flatFee == nil {
394+
return "", fmt.Errorf("flat fee is nil")
395+
}
396+
397+
return i.flatFee.TaxConfig.TaxCodeID, nil
398+
case meta.ChargeTypeUsageBased:
399+
if i.usageBased == nil {
400+
return "", fmt.Errorf("usage based is nil")
401+
}
402+
403+
return i.usageBased.TaxConfig.TaxCodeID, nil
404+
case meta.ChargeTypeCreditPurchase:
405+
if i.creditPurchase == nil {
406+
return "", fmt.Errorf("credit purchase is nil")
407+
}
408+
409+
return i.creditPurchase.TaxConfig.TaxCodeID, nil
410+
}
411+
412+
return "", fmt.Errorf("unsupported charge type: %s", i.t)
413+
}
414+
389415
// WithTaxCodeID returns a copy of the intent with TaxCodeID set to id.
390416
// Existing tax behavior and other intent fields are preserved.
391417
func (i ChargeIntent) WithTaxCodeID(id string) (ChargeIntent, error) {

openmeter/billing/charges/creditpurchase/adapter/charge.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/openmeterio/openmeter/openmeter/ent/db"
1313
dbchargecreditpurchase "github.com/openmeterio/openmeter/openmeter/ent/db/chargecreditpurchase"
1414
"github.com/openmeterio/openmeter/pkg/framework/entutils"
15+
"github.com/openmeterio/openmeter/pkg/models"
1516
"github.com/openmeterio/openmeter/pkg/pagination"
1617
"github.com/openmeterio/openmeter/pkg/slicesx"
1718
)
@@ -89,6 +90,12 @@ func (a *adapter) CreateCharge(ctx context.Context, in creditpurchase.CreateChar
8990

9091
dbCreditPurchase, err := create.Save(ctx)
9192
if err != nil {
93+
if db.IsConstraintError(err) {
94+
return creditpurchase.Charge{}, models.NewGenericPreConditionFailedError(
95+
fmt.Errorf("charge references a resource that does not exist"),
96+
)
97+
}
98+
9299
return creditpurchase.Charge{}, err
93100
}
94101

openmeter/billing/charges/flatfee/adapter/charge.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,12 @@ func (a *adapter) CreateCharges(ctx context.Context, in flatfee.CreateChargesInp
208208

209209
entities, err := tx.db.ChargeFlatFee.CreateBulk(creates...).Save(ctx)
210210
if err != nil {
211+
if db.IsConstraintError(err) {
212+
return nil, models.NewGenericPreConditionFailedError(
213+
fmt.Errorf("charge references a resource that does not exist"),
214+
)
215+
}
216+
211217
return nil, err
212218
}
213219

openmeter/billing/charges/service/create.go

Lines changed: 46 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"github.com/openmeterio/openmeter/pkg/clock"
2222
"github.com/openmeterio/openmeter/pkg/currencyx"
2323
"github.com/openmeterio/openmeter/pkg/framework/transaction"
24+
"github.com/openmeterio/openmeter/pkg/models"
2425
"github.com/openmeterio/openmeter/pkg/ref"
2526
"github.com/openmeterio/openmeter/pkg/slicesx"
2627
)
@@ -43,29 +44,9 @@ func (s *service) applyDefaultTaxCodes(ctx context.Context, namespace string, in
4344
})
4445

4546
return slicesx.MapWithErr(intents, func(intent charges.ChargeIntent) (charges.ChargeIntent, error) {
46-
var taxCodeID string
47-
48-
switch intent.Type() {
49-
case meta.ChargeTypeFlatFee:
50-
flatFee, err := intent.AsFlatFeeIntent()
51-
if err != nil {
52-
return charges.ChargeIntent{}, err
53-
}
54-
taxCodeID = flatFee.TaxConfig.TaxCodeID
55-
case meta.ChargeTypeCreditPurchase:
56-
creditPurchase, err := intent.AsCreditPurchaseIntent()
57-
if err != nil {
58-
return charges.ChargeIntent{}, err
59-
}
60-
taxCodeID = creditPurchase.TaxConfig.TaxCodeID
61-
case meta.ChargeTypeUsageBased:
62-
usageBased, err := intent.AsUsageBasedIntent()
63-
if err != nil {
64-
return charges.ChargeIntent{}, err
65-
}
66-
taxCodeID = usageBased.TaxConfig.TaxCodeID
67-
default:
68-
return charges.ChargeIntent{}, fmt.Errorf("unsupported charge type: %s", intent.Type())
47+
taxCodeID, err := intent.TaxCodeID()
48+
if err != nil {
49+
return charges.ChargeIntent{}, err
6950
}
7051

7152
if taxCodeID != "" {
@@ -87,6 +68,44 @@ func (s *service) applyDefaultTaxCodes(ctx context.Context, namespace string, in
8768
})
8869
}
8970

71+
// validateTaxCodesExist verifies every distinct non-empty tax code referenced by the intents
72+
// exists.
73+
func (s *service) validateTaxCodesExist(ctx context.Context, namespace string, intents charges.ChargeIntents) error {
74+
seen := make(map[string]struct{}, len(intents))
75+
76+
for _, intent := range intents {
77+
taxCodeID, err := intent.TaxCodeID()
78+
if err != nil {
79+
return err
80+
}
81+
82+
if taxCodeID == "" {
83+
continue
84+
}
85+
86+
if _, ok := seen[taxCodeID]; ok {
87+
continue
88+
}
89+
seen[taxCodeID] = struct{}{}
90+
91+
_, err = s.taxCodeService.GetTaxCode(ctx, taxcode.GetTaxCodeInput{
92+
NamespacedID: models.NamespacedID{Namespace: namespace, ID: taxCodeID},
93+
})
94+
if err != nil {
95+
if taxcode.IsTaxCodeNotFoundError(err) {
96+
return models.NewGenericValidationError(
97+
models.NewValidationError("tax_code_not_found", fmt.Sprintf("referenced tax code %q does not exist", taxCodeID)).
98+
WithPathString("tax_config", "tax_code"),
99+
)
100+
}
101+
102+
return err
103+
}
104+
}
105+
106+
return nil
107+
}
108+
90109
func (s *service) Create(ctx context.Context, input charges.CreateInput) (charges.Charges, error) {
91110
result, err := s.create(ctx, input)
92111
if err != nil {
@@ -127,6 +146,10 @@ func (s *service) create(ctx context.Context, input charges.CreateInput) (*charg
127146
return nil, err
128147
}
129148

149+
if err := s.validateTaxCodesExist(ctx, input.Namespace, input.Intents); err != nil {
150+
return nil, err
151+
}
152+
130153
result, err := transaction.Run(ctx, s.adapter, func(ctx context.Context) (*chargesWithInvoiceNowActions, error) {
131154
intentsByType, err := input.Intents.ByType()
132155
if err != nil {

openmeter/billing/charges/usagebased/adapter/charge.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,12 @@ func (a *adapter) CreateCharges(ctx context.Context, in usagebased.CreateCharges
186186

187187
entities, err := tx.db.ChargeUsageBased.CreateBulk(creates...).Save(ctx)
188188
if err != nil {
189+
if db.IsConstraintError(err) {
190+
return nil, models.NewGenericPreConditionFailedError(
191+
fmt.Errorf("charge references a resource that does not exist"),
192+
)
193+
}
194+
189195
return nil, err
190196
}
191197

test/credits/creditgrant_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
omtestutils "github.com/openmeterio/openmeter/openmeter/testutils"
3232
"github.com/openmeterio/openmeter/pkg/clock"
3333
"github.com/openmeterio/openmeter/pkg/datetime"
34+
"github.com/openmeterio/openmeter/pkg/models"
3435
billingtest "github.com/openmeterio/openmeter/test/billing"
3536
)
3637

@@ -610,6 +611,30 @@ func (s *CreditGrantTestSuite) TestCreatePromotionalGrantPropagatesTaxConfigToCh
610611
s.Equal(tc.ID, grant.Intent.TaxConfig.TaxCodeID)
611612
}
612613

614+
// TestCreateGrantWithMissingTaxCodeFailsValidation verifies that referencing a non-existent tax code
615+
// is reported as a validation error.
616+
func (s *CreditGrantTestSuite) TestCreateGrantWithMissingTaxCodeFailsValidation() {
617+
ctx := s.T().Context()
618+
ns := s.GetUniqueNamespace("creditgrant-missing-taxcode")
619+
s.ProvisionDefaultTaxCodes(ctx, ns)
620+
621+
cust := s.CreateLedgerBackedCustomer(ns, "test-subject")
622+
623+
_, err := s.CreditGrantService.Create(ctx, creditgrant.CreateInput{
624+
Namespace: ns,
625+
CustomerID: cust.ID,
626+
Name: "grant with missing tax code",
627+
Currency: USD,
628+
Amount: alpacadecimal.NewFromInt(10),
629+
FundingMethod: creditgrant.FundingMethodNone,
630+
TaxConfig: &productcatalog.TaxConfig{
631+
TaxCodeID: lo.ToPtr("01KW0000000000000000000000"),
632+
},
633+
})
634+
s.Require().Error(err)
635+
s.True(models.IsGenericValidationError(err), "a reference to a non-existent tax code must be a validation error, got: %v", err)
636+
}
637+
613638
func (s *CreditGrantTestSuite) mustCreatePromotionalCreditGrant(ctx context.Context, namespace string, customerID customer.CustomerID, name string, amount alpacadecimal.Decimal) creditpurchase.Charge {
614639
s.T().Helper()
615640

0 commit comments

Comments
 (0)