Skip to content

Commit 7a906df

Browse files
committed
feat(taxcode): prevent deletion of plan-referenced tax codes and validate tax codes on plan publish
1 parent ec4da11 commit 7a906df

17 files changed

Lines changed: 366 additions & 3 deletions

File tree

app/common/productcatalog.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"github.com/openmeterio/openmeter/openmeter/productcatalog/planaddon"
2626
planaddonadapter "github.com/openmeterio/openmeter/openmeter/productcatalog/planaddon/adapter"
2727
planaddonservice "github.com/openmeterio/openmeter/openmeter/productcatalog/planaddon/service"
28+
"github.com/openmeterio/openmeter/openmeter/productcatalog/taxcoderesolver"
2829
"github.com/openmeterio/openmeter/openmeter/streaming"
2930
"github.com/openmeterio/openmeter/openmeter/taxcode"
3031
"github.com/openmeterio/openmeter/openmeter/watermill/eventbus"
@@ -49,6 +50,7 @@ var Cost = wire.NewSet(
4950

5051
var Plan = wire.NewSet(
5152
NewPlanService,
53+
NewTaxCodeResolver,
5254
)
5355

5456
var Addon = wire.NewSet(
@@ -71,6 +73,8 @@ func NewFeatureConnector(
7173

7274
var NewFeatureResolver = featureresolver.New
7375

76+
var NewTaxCodeResolver = taxcoderesolver.New
77+
7478
func NewCostService(
7579
featureConnector feature.FeatureConnector,
7680
meterService meter.Service,
@@ -88,6 +92,7 @@ func NewPlanService(
8892
logger *slog.Logger,
8993
db *entdb.Client,
9094
featureResolver productcatalog.FeatureResolver,
95+
taxCodeResolver productcatalog.TaxCodeResolver,
9196
taxCodeService taxcode.Service,
9297
publisher eventbus.Publisher,
9398
) (plan.Service, error) {
@@ -102,6 +107,7 @@ func NewPlanService(
102107
return planservice.New(planservice.Config{
103108
Adapter: adapter,
104109
FeatureResolver: featureResolver,
110+
TaxCodeResolver: taxCodeResolver,
105111
TaxCode: taxCodeService,
106112
Logger: logger.With("subsystem", "productcatalog.plan"),
107113
Publisher: publisher,

cmd/billing-worker/wire_gen.go

Lines changed: 13 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cmd/jobs/internal/wire_gen.go

Lines changed: 13 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cmd/server/wire_gen.go

Lines changed: 13 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

openmeter/productcatalog/errors.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,16 @@ var ErrRateCardFeatureArchived = models.NewValidationIssue(
121121
commonhttp.WithHTTPStatusCodeAttribute(http.StatusBadRequest),
122122
)
123123

124+
const ErrCodeRateCardTaxCodeNotFound models.ErrorCode = "rate_card_tax_code_not_found"
125+
126+
var ErrRateCardTaxCodeNotFound = models.NewValidationIssue(
127+
ErrCodeRateCardTaxCodeNotFound,
128+
"tax code not found",
129+
models.WithFieldString("taxConfig"),
130+
models.WithCriticalSeverity(),
131+
commonhttp.WithHTTPStatusCodeAttribute(http.StatusBadRequest),
132+
)
133+
124134
const ErrCodeRateCardFeatureMismatch models.ErrorCode = "rate_card_feature_mismatch"
125135

126136
var ErrRateCardFeatureMismatch = models.NewValidationIssue(

openmeter/productcatalog/plan.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,3 +365,24 @@ func ValidatePlanWithFeatures(ctx context.Context, resolver NamespacedFeatureRes
365365
return errors.Join(errs...)
366366
}
367367
}
368+
369+
func ValidatePlanWithTaxCodes(ctx context.Context, resolver NamespacedTaxCodeResolver) models.ValidatorFunc[Plan] {
370+
return func(p Plan) error {
371+
var errs []error
372+
373+
for _, phase := range p.Phases {
374+
phaseFieldSelector := models.NewFieldSelectorGroup(
375+
models.NewFieldSelector("phases").
376+
WithExpression(
377+
models.NewFieldAttrValue("key", phase.Key),
378+
),
379+
)
380+
381+
if err := ValidateRateCardsWithTaxCodes(ctx, resolver)(phase.RateCards); err != nil {
382+
errs = append(errs, models.ErrorWithFieldPrefix(phaseFieldSelector, err))
383+
}
384+
}
385+
386+
return errors.Join(errs...)
387+
}
388+
}

openmeter/productcatalog/plan/service/plan.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,16 @@ func (s service) PublishPlan(ctx context.Context, params plan.PublishPlanInput)
437437
)
438438
}
439439

440+
// Validate plan with tax codes
441+
err = pp.ValidateWith(
442+
productcatalog.ValidatePlanWithTaxCodes(ctx, s.taxCodeResolver.WithNamespace(params.Namespace)),
443+
)
444+
if err != nil {
445+
errs = append(errs, fmt.Errorf("invalid plan [id=%s key=%s version=%d]: %w",
446+
p.ID, p.Key, p.Version, err),
447+
)
448+
}
449+
440450
// Check for incompatible add-ons assigned to this plan
441451

442452
if p.Addons == nil {

openmeter/productcatalog/plan/service/service.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ type Config struct {
1717
Publisher eventbus.Publisher
1818

1919
FeatureResolver productcatalog.FeatureResolver
20+
TaxCodeResolver productcatalog.TaxCodeResolver
2021
}
2122

2223
func New(config Config) (plan.Service, error) {
@@ -28,6 +29,10 @@ func New(config Config) (plan.Service, error) {
2829
return nil, errors.New("feature resolver is required")
2930
}
3031

32+
if config.TaxCodeResolver == nil {
33+
return nil, errors.New("tax code resolver is required")
34+
}
35+
3136
if config.Logger == nil {
3237
return nil, errors.New("logger is required")
3338
}
@@ -47,6 +52,7 @@ func New(config Config) (plan.Service, error) {
4752
publisher: config.Publisher,
4853

4954
featureResolver: config.FeatureResolver,
55+
taxCodeResolver: config.TaxCodeResolver,
5056
}, nil
5157
}
5258

@@ -59,4 +65,5 @@ type service struct {
5965
publisher eventbus.Publisher
6066

6167
featureResolver productcatalog.FeatureResolver
68+
taxCodeResolver productcatalog.TaxCodeResolver
6269
}

openmeter/productcatalog/ratecard.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -937,3 +937,41 @@ func ValidateRateCardsWithFeatures(ctx context.Context, resolver NamespacedFeatu
937937
return errors.Join(errs...)
938938
}
939939
}
940+
941+
func ValidateRateCardsWithTaxCodes(ctx context.Context, resolver NamespacedTaxCodeResolver) func(cards RateCards) error {
942+
return func(rateCards RateCards) error {
943+
var errs []error
944+
945+
for _, rateCard := range rateCards {
946+
rc := rateCard.AsMeta()
947+
948+
rateCardFieldSelector := models.NewFieldSelectorGroup(
949+
models.NewFieldSelector("rateCards").
950+
WithExpression(
951+
models.NewFieldAttrValue("key", rateCard.Key()),
952+
),
953+
)
954+
955+
if rc.TaxConfig == nil || rc.TaxConfig.TaxCodeID == nil {
956+
continue
957+
}
958+
959+
tc, err := resolver.ResolveTaxCode(ctx, *rc.TaxConfig.TaxCodeID)
960+
if err != nil {
961+
if models.IsGenericNotFoundError(err) || taxcode.IsTaxCodeNotFoundError(err) {
962+
errs = append(errs, models.ErrorWithFieldPrefix(rateCardFieldSelector, ErrRateCardTaxCodeNotFound))
963+
} else {
964+
errs = append(errs, fmt.Errorf("failed to resolve tax code for ratecard: %w", err))
965+
}
966+
967+
continue
968+
}
969+
970+
if tc == nil || tc.DeletedAt != nil {
971+
errs = append(errs, models.ErrorWithFieldPrefix(rateCardFieldSelector, ErrRateCardTaxCodeNotFound))
972+
}
973+
}
974+
975+
return errors.Join(errs...)
976+
}
977+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package productcatalog
2+
3+
import (
4+
"context"
5+
6+
"github.com/openmeterio/openmeter/openmeter/taxcode"
7+
)
8+
9+
// TaxCodeResolver resolves tax codes across namespaces and can create a namespace-scoped resolver.
10+
type TaxCodeResolver interface {
11+
ResolveTaxCode(ctx context.Context, namespace string, id string) (*taxcode.TaxCode, error)
12+
WithNamespace(namespace string) NamespacedTaxCodeResolver
13+
}
14+
15+
// NamespacedTaxCodeResolver resolves tax codes within a fixed namespace.
16+
type NamespacedTaxCodeResolver interface {
17+
ResolveTaxCode(ctx context.Context, id string) (*taxcode.TaxCode, error)
18+
Namespace() string
19+
}

0 commit comments

Comments
 (0)