Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions app/common/productcatalog.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"github.com/openmeterio/openmeter/openmeter/productcatalog/planaddon"
planaddonadapter "github.com/openmeterio/openmeter/openmeter/productcatalog/planaddon/adapter"
planaddonservice "github.com/openmeterio/openmeter/openmeter/productcatalog/planaddon/service"
"github.com/openmeterio/openmeter/openmeter/productcatalog/taxcoderesolver"
"github.com/openmeterio/openmeter/openmeter/streaming"
"github.com/openmeterio/openmeter/openmeter/taxcode"
"github.com/openmeterio/openmeter/openmeter/watermill/eventbus"
Expand All @@ -49,6 +50,7 @@ var Cost = wire.NewSet(

var Plan = wire.NewSet(
NewPlanService,
NewTaxCodeResolver,
)

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

var NewFeatureResolver = featureresolver.New

var NewTaxCodeResolver = taxcoderesolver.New

func NewCostService(
featureConnector feature.FeatureConnector,
meterService meter.Service,
Expand All @@ -88,6 +92,7 @@ func NewPlanService(
logger *slog.Logger,
db *entdb.Client,
featureResolver productcatalog.FeatureResolver,
taxCodeResolver productcatalog.TaxCodeResolver,
taxCodeService taxcode.Service,
publisher eventbus.Publisher,
) (plan.Service, error) {
Expand All @@ -102,6 +107,7 @@ func NewPlanService(
return planservice.New(planservice.Config{
Adapter: adapter,
FeatureResolver: featureResolver,
TaxCodeResolver: taxCodeResolver,
TaxCode: taxCodeService,
Logger: logger.With("subsystem", "productcatalog.plan"),
Publisher: publisher,
Expand All @@ -112,6 +118,7 @@ func NewAddonService(
logger *slog.Logger,
db *entdb.Client,
featureResolver productcatalog.FeatureResolver,
taxCodeResolver productcatalog.TaxCodeResolver,
taxCodeService taxcode.Service,
publisher eventbus.Publisher,
) (addon.Service, error) {
Expand All @@ -126,6 +133,7 @@ func NewAddonService(
return addonservice.New(addonservice.Config{
Adapter: adapter,
FeatureResolver: featureResolver,
TaxCodeResolver: taxCodeResolver,
TaxCode: taxCodeService,
Logger: logger.With("subsystem", "productcatalog.addon"),
Publisher: publisher,
Expand Down
23 changes: 23 additions & 0 deletions app/common/taxcode.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/openmeterio/openmeter/app/config"
"github.com/openmeterio/openmeter/openmeter/app"
entdb "github.com/openmeterio/openmeter/openmeter/ent/db"
"github.com/openmeterio/openmeter/openmeter/productcatalog/addon"
"github.com/openmeterio/openmeter/openmeter/productcatalog/plan"
"github.com/openmeterio/openmeter/openmeter/taxcode"
taxcodeadapter "github.com/openmeterio/openmeter/openmeter/taxcode/adapter"
Expand Down Expand Up @@ -67,6 +68,28 @@ func NewTaxCodePlanServiceHook(
return h, nil
}

// TaxCodeAddonHook prevents deleting tax codes that are still referenced by add-ons.
type TaxCodeAddonHook taxcodehooks.AddonHook

// NewTaxCodeAddonServiceHook builds the add-on-reference hook and registers it
// on the tax code service. It depends on both the add-on and tax code services so wire constructs
// it only after both exist, avoiding a construction cycle (add-on already depends on tax code).
func NewTaxCodeAddonServiceHook(
addonService addon.Service,
taxCodeService taxcode.Service,
) (TaxCodeAddonHook, error) {
h, err := taxcodehooks.NewAddonHook(taxcodehooks.AddonHookConfig{
AddonService: addonService,
})
if err != nil {
return nil, fmt.Errorf("failed to create tax code add-on hook: %w", err)
}

taxCodeService.RegisterHooks(h)

return h, nil
}

func NewTaxCodeNamespaceHandler(
logger *slog.Logger,
service taxcode.Service,
Expand Down
16 changes: 14 additions & 2 deletions cmd/billing-worker/wire_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 14 additions & 2 deletions cmd/jobs/internal/wire_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions cmd/server/wire.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ type Application struct {
TaxCodeNamespaceHandler *taxcode.NamespaceHandler
TaxCodeService taxcode.Service
TaxCodePlanHook common.TaxCodePlanHook
TaxCodeAddonHook common.TaxCodeAddonHook
TelemetryServer common.TelemetryServer
TerminationChecker *common.TerminationChecker
RuntimeMetricsCollector common.RuntimeMetricsCollector
Expand Down Expand Up @@ -150,6 +151,7 @@ func initializeApplication(ctx context.Context, conf config.Configuration) (Appl
common.TaxCode,
common.TaxCodeNamespaceHandler,
common.NewTaxCodePlanServiceHook,
common.NewTaxCodeAddonServiceHook,
common.Subscription,
common.Lockr,
common.Secret,
Expand Down
29 changes: 27 additions & 2 deletions cmd/server/wire_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions openmeter/productcatalog/addon.go
Original file line number Diff line number Diff line change
Expand Up @@ -332,3 +332,9 @@ func ValidateAddonWithFeatures(ctx context.Context, resolver NamespacedFeatureRe
return ValidateRateCardsWithFeatures(ctx, resolver)(a.RateCards)
}
}

func ValidateAddonWithTaxCodes(ctx context.Context, resolver NamespacedTaxCodeResolver) models.ValidatorFunc[Addon] {
return func(a Addon) error {
return ValidateRateCardsWithTaxCodes(ctx, resolver)(a.RateCards)
}
}
106 changes: 106 additions & 0 deletions openmeter/productcatalog/addon/adapter/adapter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/openmeterio/openmeter/openmeter/app"
"github.com/openmeterio/openmeter/openmeter/meter"
"github.com/openmeterio/openmeter/openmeter/productcatalog"
"github.com/openmeterio/openmeter/openmeter/productcatalog/addon"
"github.com/openmeterio/openmeter/openmeter/productcatalog/feature"
pctestutils "github.com/openmeterio/openmeter/openmeter/productcatalog/testutils"
"github.com/openmeterio/openmeter/openmeter/taxcode"
tctestutils "github.com/openmeterio/openmeter/openmeter/taxcode/testutils"
"github.com/openmeterio/openmeter/openmeter/testutils"
"github.com/openmeterio/openmeter/pkg/clock"
"github.com/openmeterio/openmeter/pkg/datetime"
Expand Down Expand Up @@ -438,5 +441,108 @@ func TestPostgresAdapter(t *testing.T) {
})
}
})

t.Run("ListTaxCodeFilter", func(t *testing.T) {
testListAddonTaxCodeFilter(t, env)
})
})
}

func testListAddonTaxCodeFilter(t *testing.T, env *pctestutils.TestEnv) {
t.Helper()

ns := pctestutils.NewTestNamespace(t)

// Create two tax codes with distinct Stripe app mappings so the add-on service
// can resolve them and populate the rate-card tax_code_id FK.
tcEnv := tctestutils.NewTestEnvFromClient(t, env.Client, env.Logger)

taxA := tcEnv.CreateTaxCode(t, ns, taxcode.CreateTaxCodeInput{
Key: "tax-a",
Name: "Tax A",
AppMappings: taxcode.TaxCodeAppMappings{
{AppType: app.AppTypeStripe, TaxCode: "txcd_20000001"},
},
})

taxB := tcEnv.CreateTaxCode(t, ns, taxcode.CreateTaxCodeInput{
Key: "tax-b",
Name: "Tax B",
AppMappings: taxcode.TaxCodeAppMappings{
{AppType: app.AppTypeStripe, TaxCode: "txcd_20000002"},
},
})

// Helper: build a minimal add-on input with a FlatFeeRateCard referencing the given taxCodeID.
makeAddonInput := func(key string, taxCodeID string) addon.CreateAddonInput {
input := pctestutils.NewTestAddon(t, ns,
&productcatalog.FlatFeeRateCard{
RateCardMeta: productcatalog.RateCardMeta{
Key: "rc-1",
Name: "RC 1",
TaxConfig: &productcatalog.TaxConfig{
TaxCodeID: lo.ToPtr(taxCodeID),
},
},
},
)
input.Addon.AddonMeta.Key = key
return input
}

// given: two add-ons each referencing a different tax code
addonA, err := env.Addon.CreateAddon(t.Context(), makeAddonInput("addon-a", taxA.ID))
require.NoError(t, err, "creating addonA must not fail")

addonB, err := env.Addon.CreateAddon(t.Context(), makeAddonInput("addon-b", taxB.ID))
require.NoError(t, err, "creating addonB must not fail")

t.Run("filter by taxA returns only addonA", func(t *testing.T) {
// when: listing add-ons filtered by taxA
result, err := env.AddonRepository.ListAddons(t.Context(), addon.ListAddonsInput{
Namespaces: []string{ns},
TaxCodes: &filter.FilterString{In: lo.ToPtr([]string{taxA.ID})},
Page: pagination.Page{PageSize: 100, PageNumber: 1},
})

// then: only addonA is returned
require.NoError(t, err)
ids := addonIDs(result)
require.ElementsMatch(t, []string{addonA.ID}, ids)
})

t.Run("filter by taxB returns only addonB", func(t *testing.T) {
// when: listing add-ons filtered by taxB
result, err := env.AddonRepository.ListAddons(t.Context(), addon.ListAddonsInput{
Namespaces: []string{ns},
TaxCodes: &filter.FilterString{In: lo.ToPtr([]string{taxB.ID})},
Page: pagination.Page{PageSize: 100, PageNumber: 1},
})

// then: only addonB is returned
require.NoError(t, err)
ids := addonIDs(result)
require.ElementsMatch(t, []string{addonB.ID}, ids)
})

t.Run("filter by nonexistent tax code returns empty", func(t *testing.T) {
// when: listing add-ons filtered by a tax code id that doesn't exist
result, err := env.AddonRepository.ListAddons(t.Context(), addon.ListAddonsInput{
Namespaces: []string{ns},
TaxCodes: &filter.FilterString{In: lo.ToPtr([]string{"01000000000000000000000000"})},
Page: pagination.Page{PageSize: 100, PageNumber: 1},
})

// then: no add-ons are returned
require.NoError(t, err)
require.Empty(t, result.Items)
})
}

func addonIDs(result pagination.Result[addon.Addon]) []string {
ids := make([]string, 0, len(result.Items))
for _, a := range result.Items {
ids = append(ids, a.ID)
}
return ids
}
Loading