Skip to content
Merged
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
1 change: 0 additions & 1 deletion api/v3/handlers/billingprofiles/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ var (
// goverter:ignore TaxCode
ToAPIBillingTaxConfig func(config *productcatalog.TaxConfig) (*api.BillingTaxConfig, error)
// goverter:map Stripe Stripe
// goverter:ignore TaxCode
FromAPIBillingTaxConfig func(config *api.BillingTaxConfig) (*productcatalog.TaxConfig, error)
)

Expand Down
2 changes: 1 addition & 1 deletion openmeter/app/stripe/appinvoice.go
Original file line number Diff line number Diff line change
Expand Up @@ -571,7 +571,7 @@ func getCreditStripeAddInvoiceItemParams(calculator StripeCalculator, line billi
return params
}

func applyTaxSettingsToInvoiceItem(add *stripe.InvoiceItemParams, taxConfig *productcatalog.TaxConfig) *stripe.InvoiceItemParams {
func applyTaxSettingsToInvoiceItem(add *stripe.InvoiceItemParams, taxConfig *billing.TaxConfig) *stripe.InvoiceItemParams {
if taxConfig != nil && !lo.IsEmpty(taxConfig) {
if taxConfig.Behavior != nil {
add.TaxBehavior = getStripeTaxBehavior(taxConfig.Behavior)
Expand Down
4 changes: 2 additions & 2 deletions openmeter/billing/adapter/gatheringlines.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ func (a *adapter) updateGatheringLines(ctx context.Context, lines billing.Gather
}

if line.TaxConfig != nil {
create = create.SetTaxConfig(*line.TaxConfig).
create = create.SetNillableTaxConfig(billing.FromProductCatalog(line.TaxConfig)).
SetNillableTaxCodeID(line.TaxConfig.TaxCodeID).
SetNillableTaxBehavior(line.TaxConfig.Behavior)
}
Expand Down Expand Up @@ -310,7 +310,7 @@ func (a *adapter) mapGatheringInvoiceLineFromDB(schemaLevel int, dbLine *db.Bill
Currency: dbLine.Currency,

TaxConfig: productcatalog.BackfillTaxConfig(
lo.EmptyableToPtr(dbLine.TaxConfig),
lo.EmptyableToPtr(dbLine.TaxConfig).ToProductCatalog(),
dbLine.TaxBehavior,
taxCodeFromInvoiceLineEdge(dbLine),
),
Expand Down
20 changes: 12 additions & 8 deletions openmeter/billing/adapter/stdinvoicelinemapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -318,28 +318,32 @@ func taxCodeFromInvoiceLineEdge(dbLine *db.BillingInvoiceLine) *taxcode.TaxCode
// TODO[later]: change the billing-facing types to expose TaxCodeSnapshot and TaxCodeReference
// fields explicitly so it is obvious what is the immutable invoice snapshot and what is the live
// reference to the tax entity.
func backfillTaxConfigReferences(snapshottedTaxConfig *productcatalog.TaxConfig, persistedTaxBehavior *productcatalog.TaxBehavior, resolvedTaxCode *taxcode.TaxCode) *productcatalog.TaxConfig {
backfilledTaxConfig := productcatalog.BackfillTaxConfig(snapshottedTaxConfig, persistedTaxBehavior, resolvedTaxCode)
func backfillTaxConfigReferences(snapshottedTaxConfig *billing.TaxConfig, persistedTaxBehavior *productcatalog.TaxBehavior, resolvedTaxCode *taxcode.TaxCode) *billing.TaxConfig {
if snapshottedTaxConfig == nil {
return billing.FromProductCatalog(productcatalog.BackfillTaxConfig(nil, persistedTaxBehavior, resolvedTaxCode))
}

backfilledTaxConfig := productcatalog.BackfillTaxConfig(snapshottedTaxConfig.ToProductCatalog(), persistedTaxBehavior, resolvedTaxCode)

if backfilledTaxConfig == nil || resolvedTaxCode == nil {
return backfilledTaxConfig
return billing.FromProductCatalog(backfilledTaxConfig)
}

if backfilledTaxConfig.TaxCodeID != nil && *backfilledTaxConfig.TaxCodeID != resolvedTaxCode.ID {
return backfilledTaxConfig
return billing.FromProductCatalog(backfilledTaxConfig)
}

if backfilledTaxConfig.Stripe != nil {
mapping, ok := resolvedTaxCode.GetAppMapping(app.AppTypeStripe)
if !ok || mapping.TaxCode != backfilledTaxConfig.Stripe.Code {
return backfilledTaxConfig
return billing.FromProductCatalog(backfilledTaxConfig)
}
}

cloned := backfilledTaxConfig.Clone()
cloned.TaxCode = resolvedTaxCode
result := billing.FromProductCatalog(backfilledTaxConfig)
result.TaxCode = resolvedTaxCode

return &cloned
return result
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

func (a *adapter) mapStandardInvoiceDetailedLineAmountDiscountFromDB(dbDiscount *db.BillingStandardInvoiceDetailedLineAmountDiscount) (billing.AmountLineDiscountManaged, error) {
Expand Down
4 changes: 2 additions & 2 deletions openmeter/billing/gatheringinvoice.go
Original file line number Diff line number Diff line change
Expand Up @@ -717,9 +717,9 @@ func (g GatheringLine) AsNewStandardLine(invoiceID string) (*StandardLine, error
return nil, fmt.Errorf("cloning annotations: %w", err)
}

var taxConfig *productcatalog.TaxConfig
var taxConfig *TaxConfig
if g.TaxConfig != nil {
taxConfig = lo.ToPtr(g.TaxConfig.Clone())
taxConfig = FromProductCatalog(lo.ToPtr(g.TaxConfig.Clone()))
}

var subscription *SubscriptionReference
Expand Down
14 changes: 7 additions & 7 deletions openmeter/billing/httpdriver/invoiceline.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ func mapCreateLineToEntity(line api.InvoicePendingLineCreate, ns string) (*billi
},

InvoiceAt: line.InvoiceAt,
TaxConfig: rateCardParsed.TaxConfig,
TaxConfig: billing.FromProductCatalog(rateCardParsed.TaxConfig),
RateCardDiscounts: rateCardParsed.Discounts,
},
UsageBased: &billing.UsageBasedLine{
Expand Down Expand Up @@ -324,7 +324,7 @@ func mapInvoiceLineToAPI(line *billing.StandardLine) (api.InvoiceLine, error) {
return api.InvoiceLine{}, fmt.Errorf("failed to map price: %w", err)
}

children, err := mapDetailedLinesToAPI(line.DetailedLines, line.InvoiceAt, line.TaxConfig)
children, err := mapDetailedLinesToAPI(line.DetailedLines, line.InvoiceAt, line.TaxConfig.ToProductCatalog())
if err != nil {
return api.InvoiceLine{}, fmt.Errorf("failed to map children: %w", err)
}
Expand Down Expand Up @@ -362,7 +362,7 @@ func mapInvoiceLineToAPI(line *billing.StandardLine) (api.InvoiceLine, error) {
To: line.Period.To,
},

TaxConfig: mapTaxConfigToAPI(line.TaxConfig),
TaxConfig: mapTaxConfigToAPI(line.TaxConfig.ToProductCatalog()),

FeatureKey: lo.EmptyableToPtr(line.UsageBased.FeatureKey),
MeteredQuantity: decimalPtrToStringPtrIfNotEqual(line.UsageBased.MeteredQuantity, line.UsageBased.Quantity),
Expand All @@ -373,7 +373,7 @@ func mapInvoiceLineToAPI(line *billing.StandardLine) (api.InvoiceLine, error) {
Price: lo.ToPtr(price),

RateCard: &api.InvoiceUsageBasedRateCard{
TaxConfig: mapTaxConfigToAPI(line.TaxConfig),
TaxConfig: mapTaxConfigToAPI(line.TaxConfig.ToProductCatalog()),
Price: lo.ToPtr(price),
FeatureKey: lo.EmptyableToPtr(line.UsageBased.FeatureKey),
},
Expand Down Expand Up @@ -634,7 +634,7 @@ func mapSimulationLineToEntity(line api.InvoiceSimulationLine) (*billing.Standar
},

InvoiceAt: line.InvoiceAt.Truncate(streaming.MinimumWindowSizeDuration),
TaxConfig: rateCardParsed.TaxConfig,
TaxConfig: billing.FromProductCatalog(rateCardParsed.TaxConfig),
RateCardDiscounts: rateCardParsed.Discounts,
},
UsageBased: &billing.UsageBasedLine{
Expand Down Expand Up @@ -679,7 +679,7 @@ func standardLineFromInvoiceLineReplaceUpdate(line api.InvoiceLineReplaceUpdate,
},
InvoiceAt: line.InvoiceAt.Truncate(streaming.MinimumWindowSizeDuration),

TaxConfig: rateCardParsed.TaxConfig,
TaxConfig: billing.FromProductCatalog(rateCardParsed.TaxConfig),
RateCardDiscounts: rateCardParsed.Discounts,
},
UsageBased: &billing.UsageBasedLine{
Expand Down Expand Up @@ -760,7 +760,7 @@ func mergeStandardLineFromInvoiceLineReplaceUpdate(existing *billing.StandardLin
existing.Period.To = line.Period.To.Truncate(streaming.MinimumWindowSizeDuration)
existing.InvoiceAt = line.InvoiceAt.Truncate(streaming.MinimumWindowSizeDuration)

existing.TaxConfig = rateCardParsed.TaxConfig
existing.TaxConfig = billing.FromProductCatalog(rateCardParsed.TaxConfig)
existing.UsageBased.Price = rateCardParsed.Price
existing.UsageBased.FeatureKey = rateCardParsed.FeatureKey

Expand Down
3 changes: 1 addition & 2 deletions openmeter/billing/service/invoicecalc/taxconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"github.com/samber/lo"

"github.com/openmeterio/openmeter/openmeter/billing"
"github.com/openmeterio/openmeter/openmeter/productcatalog"
)

// SnapshotTaxConfigIntoLines merges the invoice's DefaultTaxConfig into each line and
Expand All @@ -16,7 +15,7 @@ func SnapshotTaxConfigIntoLines(invoice *billing.StandardInvoice, deps StandardI
}

for _, line := range invoice.Lines.OrEmpty() {
line.TaxConfig = productcatalog.MergeTaxConfigs(invoice.Workflow.Config.Invoicing.DefaultTaxConfig, line.TaxConfig)
line.TaxConfig = billing.MergeTaxConfigs(billing.FromProductCatalog(invoice.Workflow.Config.Invoicing.DefaultTaxConfig), line.TaxConfig)

if line.TaxConfig == nil || line.TaxConfig.Stripe == nil {
continue
Expand Down
125 changes: 101 additions & 24 deletions openmeter/billing/service/invoicecalc/taxconfig_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,71 @@ import (
"github.com/openmeterio/openmeter/pkg/models"
)

// TaxConfigEqualDetectsTaxCode is the regression guard for the fix: billing.TaxConfig.Equal
// now includes the resolved TaxCode entity. Two configs that are identical except for TaxCode
// (one nil, one stamped) must compare as NOT equal, so the adapter diff guard re-upserts the line
// and the snapshot is persisted to the tax_config JSONB column.
func TestTaxConfigEqualDetectsTaxCode(t *testing.T) {
tc1 := taxcode.TaxCode{
NamespacedID: models.NamespacedID{Namespace: "ns", ID: "tc-1"},
AppMappings: taxcode.TaxCodeAppMappings{
{AppType: app.AppTypeStripe, TaxCode: "txcd_10000000"},
},
}

// Persisted state: TaxCodeID set, but the TaxCode entity snapshot is absent.
persistedState := &billing.TaxConfig{
TaxConfig: productcatalog.TaxConfig{
Stripe: &productcatalog.StripeTaxConfig{Code: "txcd_10000000"},
Behavior: lo.ToPtr(productcatalog.ExclusiveTaxBehavior),
TaxCodeID: lo.ToPtr("tc-1"),
},
TaxCode: nil,
}

// In-memory state after SnapshotTaxConfigIntoLines stamps the entity. Identical except TaxCode.
expectedState := &billing.TaxConfig{
TaxConfig: productcatalog.TaxConfig{
Stripe: &productcatalog.StripeTaxConfig{Code: "txcd_10000000"},
Behavior: lo.ToPtr(productcatalog.ExclusiveTaxBehavior),
TaxCodeID: lo.ToPtr("tc-1"),
},
TaxCode: &tc1,
}

assert.False(t, persistedState.Equal(expectedState),
"Equal must detect the stamped TaxCode so the line is re-upserted and the snapshot persisted")

// Same-ID sub-case: two configs whose TaxCode shares the same ID but differs in other fields
// (e.g. AppMappings) must compare as equal — equality is ID-only.
tc2 := taxcode.TaxCode{
NamespacedID: models.NamespacedID{Namespace: "ns", ID: "tc-1"},
AppMappings: taxcode.TaxCodeAppMappings{
{AppType: app.AppTypeStripe, TaxCode: "txcd_20000000"},
},
}

leftConfig := &billing.TaxConfig{
TaxConfig: productcatalog.TaxConfig{
Stripe: &productcatalog.StripeTaxConfig{Code: "txcd_10000000"},
Behavior: lo.ToPtr(productcatalog.ExclusiveTaxBehavior),
TaxCodeID: lo.ToPtr("tc-1"),
},
TaxCode: &tc1,
}
rightConfig := &billing.TaxConfig{
TaxConfig: productcatalog.TaxConfig{
Stripe: &productcatalog.StripeTaxConfig{Code: "txcd_10000000"},
Behavior: lo.ToPtr(productcatalog.ExclusiveTaxBehavior),
TaxCodeID: lo.ToPtr("tc-1"),
},
TaxCode: &tc2,
}

assert.True(t, leftConfig.Equal(rightConfig),
"Equal uses ID-only comparison: same TaxCode.ID means equal regardless of other TaxCode fields")
}

func TestSnapshotTaxConfigIntoLines(t *testing.T) {
tc1 := taxcode.TaxCode{
NamespacedID: models.NamespacedID{Namespace: "ns", ID: "tc-1"},
Expand Down Expand Up @@ -47,7 +112,7 @@ func TestSnapshotTaxConfigIntoLines(t *testing.T) {
newLine := func(tc *productcatalog.TaxConfig) *billing.StandardLine {
return &billing.StandardLine{
StandardLineBase: billing.StandardLineBase{
TaxConfig: tc,
TaxConfig: billing.FromProductCatalog(tc),
},
}
}
Expand All @@ -56,7 +121,7 @@ func TestSnapshotTaxConfigIntoLines(t *testing.T) {
name string
invoice billing.StandardInvoice
deps StandardInvoiceCalculatorDependencies
wantTC *productcatalog.TaxConfig
wantTC *billing.TaxConfig
wantNoErr bool
}{
{
Expand All @@ -69,7 +134,7 @@ func TestSnapshotTaxConfigIntoLines(t *testing.T) {
}),
),
deps: StandardInvoiceCalculatorDependencies{TaxCodes: TaxCodes{"txcd_10000000": tc1}},
wantTC: &productcatalog.TaxConfig{Stripe: &productcatalog.StripeTaxConfig{Code: "txcd_10000000"}},
wantTC: &billing.TaxConfig{TaxConfig: productcatalog.TaxConfig{Stripe: &productcatalog.StripeTaxConfig{Code: "txcd_10000000"}}},
wantNoErr: true,
},
{
Expand All @@ -82,10 +147,12 @@ func TestSnapshotTaxConfigIntoLines(t *testing.T) {
}),
),
deps: StandardInvoiceCalculatorDependencies{TaxCodes: TaxCodes{"txcd_10000000": tc1}},
wantTC: &productcatalog.TaxConfig{
Stripe: &productcatalog.StripeTaxConfig{Code: "txcd_10000000"},
TaxCodeID: lo.ToPtr("tc-1"),
TaxCode: &tc1,
wantTC: &billing.TaxConfig{
TaxConfig: productcatalog.TaxConfig{
Stripe: &productcatalog.StripeTaxConfig{Code: "txcd_10000000"},
TaxCodeID: lo.ToPtr("tc-1"),
},
TaxCode: &tc1,
},
wantNoErr: true,
},
Expand All @@ -100,10 +167,12 @@ func TestSnapshotTaxConfigIntoLines(t *testing.T) {
}),
),
deps: StandardInvoiceCalculatorDependencies{TaxCodes: TaxCodes{"txcd_10000000": tc1}},
wantTC: &productcatalog.TaxConfig{
Stripe: &productcatalog.StripeTaxConfig{Code: "txcd_10000000"},
TaxCodeID: lo.ToPtr("already-set"),
TaxCode: &tc1,
wantTC: &billing.TaxConfig{
TaxConfig: productcatalog.TaxConfig{
Stripe: &productcatalog.StripeTaxConfig{Code: "txcd_10000000"},
TaxCodeID: lo.ToPtr("already-set"),
},
TaxCode: &tc1,
},
wantNoErr: true,
},
Expand All @@ -117,8 +186,10 @@ func TestSnapshotTaxConfigIntoLines(t *testing.T) {
}),
),
deps: StandardInvoiceCalculatorDependencies{TaxCodes: TaxCodes{}},
wantTC: &productcatalog.TaxConfig{
Stripe: &productcatalog.StripeTaxConfig{Code: "txcd_10000000"},
wantTC: &billing.TaxConfig{
TaxConfig: productcatalog.TaxConfig{
Stripe: &productcatalog.StripeTaxConfig{Code: "txcd_10000000"},
},
},
wantNoErr: true,
},
Expand All @@ -133,11 +204,13 @@ func TestSnapshotTaxConfigIntoLines(t *testing.T) {
newLine(nil),
),
deps: StandardInvoiceCalculatorDependencies{TaxCodes: TaxCodes{"txcd_10000000": tc1}},
wantTC: &productcatalog.TaxConfig{
Behavior: lo.ToPtr(productcatalog.InclusiveTaxBehavior),
Stripe: &productcatalog.StripeTaxConfig{Code: "txcd_10000000"},
TaxCodeID: lo.ToPtr("tc-1"),
TaxCode: &tc1,
wantTC: &billing.TaxConfig{
TaxConfig: productcatalog.TaxConfig{
Behavior: lo.ToPtr(productcatalog.InclusiveTaxBehavior),
Stripe: &productcatalog.StripeTaxConfig{Code: "txcd_10000000"},
TaxCodeID: lo.ToPtr("tc-1"),
},
TaxCode: &tc1,
},
wantNoErr: true,
},
Expand All @@ -156,10 +229,12 @@ func TestSnapshotTaxConfigIntoLines(t *testing.T) {
"txcd_10000000": tc1,
"txcd_20000000": tc2,
}},
wantTC: &productcatalog.TaxConfig{
Stripe: &productcatalog.StripeTaxConfig{Code: "txcd_20000000"},
TaxCodeID: lo.ToPtr("tc-2"),
TaxCode: &tc2,
wantTC: &billing.TaxConfig{
TaxConfig: productcatalog.TaxConfig{
Stripe: &productcatalog.StripeTaxConfig{Code: "txcd_20000000"},
TaxCodeID: lo.ToPtr("tc-2"),
},
TaxCode: &tc2,
},
wantNoErr: true,
},
Expand All @@ -173,8 +248,10 @@ func TestSnapshotTaxConfigIntoLines(t *testing.T) {
}),
),
deps: StandardInvoiceCalculatorDependencies{TaxCodes: TaxCodes{"txcd_10000000": tc1}},
wantTC: &productcatalog.TaxConfig{
Behavior: lo.ToPtr(productcatalog.ExclusiveTaxBehavior),
wantTC: &billing.TaxConfig{
TaxConfig: productcatalog.TaxConfig{
Behavior: lo.ToPtr(productcatalog.ExclusiveTaxBehavior),
},
},
wantNoErr: true,
},
Expand Down
5 changes: 2 additions & 3 deletions openmeter/billing/stdinvoice.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import (
"github.com/openmeterio/openmeter/openmeter/billing/models/externalid"
"github.com/openmeterio/openmeter/openmeter/billing/models/totals"
"github.com/openmeterio/openmeter/openmeter/customer"
"github.com/openmeterio/openmeter/openmeter/productcatalog"
"github.com/openmeterio/openmeter/openmeter/streaming"
"github.com/openmeterio/openmeter/pkg/currencyx"
"github.com/openmeterio/openmeter/pkg/expand"
Expand Down Expand Up @@ -450,7 +449,7 @@ func (i *StandardInvoice) getLeafLines() DetailedLines {

type DetailedLineWithResolvedTaxConfig struct {
DetailedLine
TaxConfig *productcatalog.TaxConfig
TaxConfig *TaxConfig
}

// GetLeafLinesWithResolvedTaxConfig returns invoice leaf lines together with the effective tax
Expand All @@ -459,7 +458,7 @@ func (i *StandardInvoice) GetLeafLinesWithResolvedTaxConfig() []DetailedLineWith
out := make([]DetailedLineWithResolvedTaxConfig, 0)

for _, parentLine := range i.Lines.OrEmpty() {
taxConfig := productcatalog.MergeTaxConfigs(i.Workflow.Config.Invoicing.DefaultTaxConfig, parentLine.TaxConfig)
taxConfig := MergeTaxConfigs(FromProductCatalog(i.Workflow.Config.Invoicing.DefaultTaxConfig), parentLine.TaxConfig)
for _, line := range parentLine.DetailedLines {
out = append(out, DetailedLineWithResolvedTaxConfig{
DetailedLine: line,
Expand Down
Loading
Loading