diff --git a/api/spec/packages/aip/src/productcatalog/ratecard.tsp b/api/spec/packages/aip/src/productcatalog/ratecard.tsp index 7301c7d6e2..bd789f7f16 100644 --- a/api/spec/packages/aip/src/productcatalog/ratecard.tsp +++ b/api/spec/packages/aip/src/productcatalog/ratecard.tsp @@ -3,6 +3,7 @@ import "../billing/tax.tsp"; import "../features/index.tsp"; import "../tax/codes.tsp"; import "./price.tsp"; +import "./unitconfig.tsp"; namespace ProductCatalog; @@ -54,6 +55,20 @@ model RateCard { @summary("Price") price: Price; + /** + * Unit conversion configuration for the rate card. + * + * When set, transforms the raw metered quantity into a billing quantity before + * pricing. Valid only with unit, graduated, or volume prices. + * + * For plans authored with v1 dynamic or package prices, the unit config is + * synthesized on read: dynamic prices map to a unit price with a multiply unit + * config, and package prices map to a unit price with a divide unit config. + */ + @visibility(Lifecycle.Read, Lifecycle.Create, Lifecycle.Update) + @summary("Unit config") + unit_config?: UnitConfig; + /** * The payment term of the rate card. In advance payment term can only be used for * flat prices. diff --git a/api/v3/api.gen.go b/api/v3/api.gen.go index 81ad7a8682..6df6fd49a3 100644 --- a/api/v3/api.gen.go +++ b/api/v3/api.gen.go @@ -806,6 +806,48 @@ func (e BillingTaxBehavior) Valid() bool { } } +// Defines values for BillingUnitConfigOperation. +const ( + BillingUnitConfigOperationDivide BillingUnitConfigOperation = "divide" + BillingUnitConfigOperationMultiply BillingUnitConfigOperation = "multiply" +) + +// Valid indicates whether the value is a known member of the BillingUnitConfigOperation enum. +func (e BillingUnitConfigOperation) Valid() bool { + switch e { + case BillingUnitConfigOperationDivide: + return true + case BillingUnitConfigOperationMultiply: + return true + default: + return false + } +} + +// Defines values for BillingUnitConfigRoundingMode. +const ( + BillingUnitConfigRoundingModeCeiling BillingUnitConfigRoundingMode = "ceiling" + BillingUnitConfigRoundingModeFloor BillingUnitConfigRoundingMode = "floor" + BillingUnitConfigRoundingModeHalfUp BillingUnitConfigRoundingMode = "half_up" + BillingUnitConfigRoundingModeNone BillingUnitConfigRoundingMode = "none" +) + +// Valid indicates whether the value is a known member of the BillingUnitConfigRoundingMode enum. +func (e BillingUnitConfigRoundingMode) Valid() bool { + switch e { + case BillingUnitConfigRoundingModeCeiling: + return true + case BillingUnitConfigRoundingModeFloor: + return true + case BillingUnitConfigRoundingModeHalfUp: + return true + case BillingUnitConfigRoundingModeNone: + return true + default: + return false + } +} + // Defines values for BillingUsageBasedChargeType. const ( BillingUsageBasedChargeTypeUsageBased BillingUsageBasedChargeType = "usage_based" @@ -2967,6 +3009,16 @@ type BillingRateCard struct { // TaxConfig The tax config of the rate card. TaxConfig *BillingRateCardTaxConfig `json:"tax_config,omitempty"` + + // UnitConfig Unit conversion configuration for the rate card. + // + // When set, transforms the raw metered quantity into a billing quantity before + // pricing. Valid only with unit, graduated, or volume prices. + // + // For plans authored with v1 dynamic or package prices, the unit config is + // synthesized on read: dynamic prices map to a unit price with a multiply unit + // config, and package prices map to a unit price with a divide unit config. + UnitConfig *BillingUnitConfig `json:"unit_config,omitempty"` } // BillingRateCardDiscounts Discount configuration for a rate card. @@ -3349,6 +3401,78 @@ type BillingTotals struct { Total Numeric `json:"total"` } +// BillingUnitConfig Unit conversion configuration. +// +// Transforms raw metered quantities into billing-ready units before pricing and +// entitlement evaluation. Applied at the rate card level so the same feature can +// be billed in different units across plans. +// +// Examples: +// +// - Meter bytes, bill GB: operation=divide, conversionFactor=1e9, +// rounding=ceiling, displayUnit="GB" +// - Meter seconds, bill hours: operation=divide, conversionFactor=3600, +// rounding=ceiling, displayUnit="hours" +// - Cost + 20% margin: operation=multiply, conversionFactor=1.2 +// - Bill per million tokens: operation=divide, conversionFactor=1e6, +// rounding=ceiling, displayUnit="M" +// +// v1 equivalents: +// +// - DynamicPrice(multiplier): operation=multiply, conversionFactor=multiplier + +// UnitPrice(amount=1) +// - PackagePrice(amount, quantityPerPkg): operation=divide, +// conversionFactor=quantityPerPkg, rounding=ceiling + UnitPrice(amount) +type BillingUnitConfig struct { + // ConversionFactor The factor used in the conversion operation. + // + // - For `divide`: `converted = raw / conversionFactor`. + // - For `multiply`: `converted = raw × conversionFactor`. + // + // Must be a positive non-zero value. + ConversionFactor Numeric `json:"conversion_factor"` + + // DisplayUnit A human-readable label for the converted unit shown on invoices and in the + // customer portal (e.g., "GB", "hours", "M tokens"). + // + // Optional. When omitted, no unit label is rendered. + DisplayUnit *string `json:"display_unit,omitempty"` + + // Operation The arithmetic operation to apply to the raw metered quantity. + Operation BillingUnitConfigOperation `json:"operation"` + + // Precision The number of decimal places to retain after rounding. + // + // Only meaningful when rounding is not "none". Defaults to 0 (round to whole + // numbers). + Precision *int `json:"precision,omitempty"` + + // Rounding The rounding mode applied to the converted quantity for invoicing. + // + // Defaults to none (no rounding). Entitlement checks always use the precise + // (unrounded) value. + Rounding *BillingUnitConfigRoundingMode `json:"rounding,omitempty"` +} + +// BillingUnitConfigOperation The arithmetic operation used to convert raw metered units into billing units. +// +// - `divide`: Divide the metered quantity by the conversion factor (e.g., bytes ÷ +// 1e9 = GB). +// - `multiply`: Multiply the metered quantity by the conversion factor (e.g., cost +// × 1.2 = cost + 20% margin). +type BillingUnitConfigOperation string + +// BillingUnitConfigRoundingMode The rounding mode applied to the converted quantity for invoicing. +// +// Rounding is applied only to the invoiced quantity. Entitlement balance checks +// use the precise decimal value after conversion. +// +// - `ceiling`: Round up to the next integer (typical for package-style billing). +// - `floor`: Round down to the previous integer. +// - `half_up`: Round to the nearest integer, with 0.5 rounding up. +// - `none`: No rounding; the converted value is used as-is. +type BillingUnitConfigRoundingMode string + // BillingUsageBasedCharge A usage-based charge for a customer. type BillingUsageBasedCharge struct { // AdvanceAfter The earliest time when the charge should be advanced again by background @@ -10280,650 +10404,670 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "qOJSV1dHd76dfft/AQAA//9a3d/1JgYEAA==", + "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/api/v3/handlers/plans/convert.go b/api/v3/handlers/plans/convert.go index 39025b9aad..32ee12920c 100644 --- a/api/v3/handlers/plans/convert.go +++ b/api/v3/handlers/plans/convert.go @@ -16,28 +16,6 @@ import ( "github.com/openmeterio/openmeter/pkg/models" ) -var unsupportedV3PriceTypes = map[productcatalog.PriceType]struct{}{ - productcatalog.DynamicPriceType: {}, - productcatalog.PackagePriceType: {}, -} - -func hasUnsupportedV3Price(p plan.Plan) bool { - for _, phase := range p.Phases { - for _, rc := range phase.RateCards { - price := rc.AsMeta().Price - if price == nil { - continue - } - - if _, unsupported := unsupportedV3PriceTypes[price.Type()]; unsupported { - return true - } - } - } - - return false -} - func ToAPIBillingPlan(p plan.Plan) (api.BillingPlan, error) { validationIssues, _ := p.AsProductCatalogPlan().ValidationErrors() @@ -163,9 +141,97 @@ func ToAPIBillingRateCard(rc productcatalog.RateCard) (api.BillingRateCard, erro result.Price = price + if meta.UnitConfig != nil { + result.UnitConfig = ToAPIBillingUnitConfig(meta.UnitConfig) + } else { + unitConfig, err := ToAPIBillingRateCardUnitConfig(meta.Price) + if err != nil { + return result, fmt.Errorf("failed to convert unit config: %w", err) + } + + result.UnitConfig = unitConfig + } + return result, nil } +func ToAPIBillingUnitConfig(uc *productcatalog.UnitConfig) *api.BillingUnitConfig { + if uc == nil { + return nil + } + + out := &api.BillingUnitConfig{ + Operation: api.BillingUnitConfigOperation(uc.Operation), + ConversionFactor: uc.ConversionFactor.String(), + DisplayUnit: uc.DisplayUnit, + Precision: uc.Precision, + } + + if uc.Rounding != nil { + out.Rounding = lo.ToPtr(api.BillingUnitConfigRoundingMode(*uc.Rounding)) + } + + return out +} + +func FromAPIBillingUnitConfig(a *api.BillingUnitConfig) (*productcatalog.UnitConfig, error) { + if a == nil { + return nil, nil + } + + factor, err := decimal.NewFromString(a.ConversionFactor) + if err != nil { + return nil, fmt.Errorf("invalid unit config conversion factor: %w", err) + } + + uc := &productcatalog.UnitConfig{ + Operation: productcatalog.UnitConfigOperation(a.Operation), + ConversionFactor: factor, + DisplayUnit: a.DisplayUnit, + Precision: a.Precision, + } + + if a.Rounding != nil { + uc.Rounding = lo.ToPtr(productcatalog.UnitConfigRoundingMode(*a.Rounding)) + } + + return uc, nil +} + +func ToAPIBillingRateCardUnitConfig(p *productcatalog.Price) (*api.BillingUnitConfig, error) { + if p == nil { + return nil, nil + } + + switch p.Type() { + case productcatalog.DynamicPriceType: + dynamic, err := p.AsDynamic() + if err != nil { + return nil, fmt.Errorf("failed to read dynamic price: %w", err) + } + + return &api.BillingUnitConfig{ + Operation: api.BillingUnitConfigOperationMultiply, + ConversionFactor: dynamic.Multiplier.String(), + }, nil + + case productcatalog.PackagePriceType: + pkg, err := p.AsPackage() + if err != nil { + return nil, fmt.Errorf("failed to read package price: %w", err) + } + + return &api.BillingUnitConfig{ + Operation: api.BillingUnitConfigOperationDivide, + ConversionFactor: pkg.QuantityPerPackage.String(), + Rounding: lo.ToPtr(api.BillingUnitConfigRoundingModeCeiling), + }, nil + + default: + return nil, nil + } +} + func ToAPIBillingPrice(p *productcatalog.Price) (api.BillingPrice, error) { var result api.BillingPrice @@ -236,10 +302,29 @@ func ToAPIBillingPrice(p *productcatalog.Price) (api.BillingPrice, error) { } case productcatalog.DynamicPriceType: - return result, models.NewGenericConflictError(fmt.Errorf("dynamic price is not supported in v3 API")) + // Dynamic prices are surfaced in v3 as a unit price of amount 1; the + // multiplier is carried separately on the rate card's unit config. + if err := result.FromBillingPriceUnit(api.BillingPriceUnit{ + Amount: "1", + Type: api.BillingPriceUnitType("unit"), + }); err != nil { + return result, fmt.Errorf("failed to set unit price for dynamic price: %w", err) + } case productcatalog.PackagePriceType: - return result, models.NewGenericConflictError(fmt.Errorf("package price is not supported in v3 API")) + // Package prices are surfaced in v3 as a unit price; the package size + // is carried separately on the rate card's unit config. + pkg, err := p.AsPackage() + if err != nil { + return result, fmt.Errorf("failed to read package price: %w", err) + } + + if err = result.FromBillingPriceUnit(api.BillingPriceUnit{ + Amount: pkg.Amount.String(), + Type: api.BillingPriceUnitType("unit"), + }); err != nil { + return result, fmt.Errorf("failed to set unit price for package price: %w", err) + } default: return result, fmt.Errorf("unknown price type: %s", p.Type()) @@ -485,6 +570,11 @@ func FromAPIBillingPlanPhase(p api.BillingPlanPhase) (productcatalog.Phase, erro } func FromAPIBillingRateCard(rc api.BillingRateCard) (productcatalog.RateCard, error) { + unitConfig, err := FromAPIBillingUnitConfig(rc.UnitConfig) + if err != nil { + return nil, fmt.Errorf("failed to convert unit config: %w", err) + } + priceType, err := rc.Price.Discriminator() if err != nil { return nil, fmt.Errorf("failed to read price type: %w", err) @@ -500,6 +590,7 @@ func FromAPIBillingRateCard(rc api.BillingRateCard) (productcatalog.RateCard, er Name: rc.Name, Description: rc.Description, Metadata: labelMeta, + UnitConfig: unitConfig, } if rc.Feature != nil { diff --git a/api/v3/handlers/plans/convert_test.go b/api/v3/handlers/plans/convert_test.go index f44246c5a8..48c459080a 100644 --- a/api/v3/handlers/plans/convert_test.go +++ b/api/v3/handlers/plans/convert_test.go @@ -529,25 +529,227 @@ func TestFromBillingPrice(t *testing.T) { assert.Equal(t, "volume", disc) }) - t.Run("dynamic price returns conflict error", func(t *testing.T) { - p := productcatalog.NewPriceFrom(productcatalog.DynamicPrice{}) + t.Run("dynamic price translates to unit price of amount 1", func(t *testing.T) { + p := productcatalog.NewPriceFrom(productcatalog.DynamicPrice{ + Multiplier: decimal.NewFromFloat(1.2), + }) - _, err := ToAPIBillingPrice(p) - require.Error(t, err) - assert.True(t, models.IsGenericConflictError(err)) - assert.Contains(t, err.Error(), "dynamic price is not supported in v3 API") + result, err := ToAPIBillingPrice(p) + require.NoError(t, err) + + disc, err := result.Discriminator() + require.NoError(t, err) + assert.Equal(t, "unit", disc) + + unit, err := result.AsBillingPriceUnit() + require.NoError(t, err) + assert.Equal(t, api.Numeric("1"), unit.Amount) }) - t.Run("package price returns conflict error", func(t *testing.T) { + t.Run("package price translates to unit price with package amount", func(t *testing.T) { + p := productcatalog.NewPriceFrom(productcatalog.PackagePrice{ + Amount: decimal.NewFromFloat(0.5), + QuantityPerPackage: decimal.NewFromInt(1000), + }) + + result, err := ToAPIBillingPrice(p) + require.NoError(t, err) + + disc, err := result.Discriminator() + require.NoError(t, err) + assert.Equal(t, "unit", disc) + + unit, err := result.AsBillingPriceUnit() + require.NoError(t, err) + assert.Equal(t, api.Numeric("0.5"), unit.Amount) + }) +} + +func TestToAPIBillingRateCardUnitConfig(t *testing.T) { + t.Run("nil price has no unit config", func(t *testing.T) { + result, err := ToAPIBillingRateCardUnitConfig(nil) + require.NoError(t, err) + assert.Nil(t, result) + }) + + t.Run("flat price has no unit config", func(t *testing.T) { + p := productcatalog.NewPriceFrom(productcatalog.FlatPrice{Amount: decimal.NewFromFloat(5)}) + + result, err := ToAPIBillingRateCardUnitConfig(p) + require.NoError(t, err) + assert.Nil(t, result) + }) + + t.Run("unit price has no unit config", func(t *testing.T) { + p := productcatalog.NewPriceFrom(productcatalog.UnitPrice{Amount: decimal.NewFromFloat(0.05)}) + + result, err := ToAPIBillingRateCardUnitConfig(p) + require.NoError(t, err) + assert.Nil(t, result) + }) + + t.Run("dynamic price produces multiply unit config", func(t *testing.T) { + p := productcatalog.NewPriceFrom(productcatalog.DynamicPrice{ + Multiplier: decimal.NewFromFloat(1.2), + }) + + result, err := ToAPIBillingRateCardUnitConfig(p) + require.NoError(t, err) + require.NotNil(t, result) + assert.Equal(t, api.BillingUnitConfigOperationMultiply, result.Operation) + assert.Equal(t, api.Numeric("1.2"), result.ConversionFactor) + assert.Nil(t, result.Rounding) + assert.Nil(t, result.Precision) + assert.Nil(t, result.DisplayUnit) + }) + + t.Run("package price produces divide unit config with ceiling rounding", func(t *testing.T) { p := productcatalog.NewPriceFrom(productcatalog.PackagePrice{ Amount: decimal.NewFromFloat(10), - QuantityPerPackage: decimal.NewFromInt(100), + QuantityPerPackage: decimal.NewFromInt(1000), }) - _, err := ToAPIBillingPrice(p) - require.Error(t, err) - assert.True(t, models.IsGenericConflictError(err)) - assert.Contains(t, err.Error(), "package price is not supported in v3 API") + result, err := ToAPIBillingRateCardUnitConfig(p) + require.NoError(t, err) + require.NotNil(t, result) + assert.Equal(t, api.BillingUnitConfigOperationDivide, result.Operation) + assert.Equal(t, api.Numeric("1000"), result.ConversionFactor) + require.NotNil(t, result.Rounding) + assert.Equal(t, api.BillingUnitConfigRoundingModeCeiling, *result.Rounding) + }) +} + +func TestFromRateCard_DynamicAndPackagePrices(t *testing.T) { + cadence, err := datetime.ISODurationString("P1M").Parse() + require.NoError(t, err) + + t.Run("dynamic price renders as unit price plus multiply unit config and preserves commitments", func(t *testing.T) { + minAmt := decimal.NewFromFloat(10) + maxAmt := decimal.NewFromFloat(100) + price := productcatalog.NewPriceFrom(productcatalog.DynamicPrice{ + Multiplier: decimal.NewFromFloat(1.2), + Commitments: productcatalog.Commitments{ + MinimumAmount: &minAmt, + MaximumAmount: &maxAmt, + }, + }) + + rc := &productcatalog.UsageBasedRateCard{ + RateCardMeta: productcatalog.RateCardMeta{ + Key: "tokens", + Name: "Tokens", + Price: price, + }, + BillingCadence: cadence, + } + + result, err := ToAPIBillingRateCard(rc) + require.NoError(t, err) + + disc, err := result.Price.Discriminator() + require.NoError(t, err) + assert.Equal(t, "unit", disc) + + unit, err := result.Price.AsBillingPriceUnit() + require.NoError(t, err) + assert.Equal(t, api.Numeric("1"), unit.Amount) + + require.NotNil(t, result.UnitConfig) + assert.Equal(t, api.BillingUnitConfigOperationMultiply, result.UnitConfig.Operation) + assert.Equal(t, api.Numeric("1.2"), result.UnitConfig.ConversionFactor) + assert.Nil(t, result.UnitConfig.Rounding) + + require.NotNil(t, result.Commitments) + assert.Equal(t, lo.ToPtr(api.Numeric("10")), result.Commitments.MinimumAmount) + assert.Equal(t, lo.ToPtr(api.Numeric("100")), result.Commitments.MaximumAmount) + }) + + t.Run("package price renders as unit price plus divide unit config and preserves commitments", func(t *testing.T) { + minAmt := decimal.NewFromFloat(5) + price := productcatalog.NewPriceFrom(productcatalog.PackagePrice{ + Amount: decimal.NewFromFloat(0.5), + QuantityPerPackage: decimal.NewFromInt(1000), + Commitments: productcatalog.Commitments{ + MinimumAmount: &minAmt, + }, + }) + + rc := &productcatalog.UsageBasedRateCard{ + RateCardMeta: productcatalog.RateCardMeta{ + Key: "api-calls", + Name: "API Calls", + Price: price, + }, + BillingCadence: cadence, + } + + result, err := ToAPIBillingRateCard(rc) + require.NoError(t, err) + + disc, err := result.Price.Discriminator() + require.NoError(t, err) + assert.Equal(t, "unit", disc) + + unit, err := result.Price.AsBillingPriceUnit() + require.NoError(t, err) + assert.Equal(t, api.Numeric("0.5"), unit.Amount) + + require.NotNil(t, result.UnitConfig) + assert.Equal(t, api.BillingUnitConfigOperationDivide, result.UnitConfig.Operation) + assert.Equal(t, api.Numeric("1000"), result.UnitConfig.ConversionFactor) + require.NotNil(t, result.UnitConfig.Rounding) + assert.Equal(t, api.BillingUnitConfigRoundingModeCeiling, *result.UnitConfig.Rounding) + + require.NotNil(t, result.Commitments) + assert.Equal(t, lo.ToPtr(api.Numeric("5")), result.Commitments.MinimumAmount) + assert.Nil(t, result.Commitments.MaximumAmount) + }) + + t.Run("unit price has no unit config on rate card", func(t *testing.T) { + price := productcatalog.NewPriceFrom(productcatalog.UnitPrice{ + Amount: decimal.NewFromFloat(0.05), + }) + + rc := &productcatalog.UsageBasedRateCard{ + RateCardMeta: productcatalog.RateCardMeta{ + Key: "api-calls", + Name: "API Calls", + Price: price, + }, + BillingCadence: cadence, + } + + result, err := ToAPIBillingRateCard(rc) + require.NoError(t, err) + assert.Nil(t, result.UnitConfig) + }) + + t.Run("persisted unit config is returned verbatim, not re-synthesized", func(t *testing.T) { + price := productcatalog.NewPriceFrom(productcatalog.UnitPrice{ + Amount: decimal.NewFromInt(1), + }) + + rc := &productcatalog.UsageBasedRateCard{ + RateCardMeta: productcatalog.RateCardMeta{ + Key: "tokens", + Name: "Tokens", + Price: price, + UnitConfig: &productcatalog.UnitConfig{ + Operation: productcatalog.UnitConfigOperationMultiply, + ConversionFactor: decimal.NewFromFloat(1.5), + DisplayUnit: lo.ToPtr("tokens"), + }, + }, + BillingCadence: cadence, + } + + result, err := ToAPIBillingRateCard(rc) + require.NoError(t, err) + require.NotNil(t, result.UnitConfig) + assert.Equal(t, api.BillingUnitConfigOperationMultiply, result.UnitConfig.Operation) + assert.Equal(t, api.Numeric("1.5"), result.UnitConfig.ConversionFactor) + require.NotNil(t, result.UnitConfig.DisplayUnit) + assert.Equal(t, "tokens", *result.UnitConfig.DisplayUnit) }) } @@ -1126,6 +1328,88 @@ func TestToRateCard(t *testing.T) { require.NoError(t, err) assert.Equal(t, productcatalog.VolumeTieredPrice, tiered.Mode) }) + + t.Run("parses unit_config with multiply operation", func(t *testing.T) { + var price api.BillingPrice + require.NoError(t, price.FromBillingPriceUnit(api.BillingPriceUnit{Amount: "1", Type: "unit"})) + + bc := api.ISO8601Duration("P1M") + + rc := api.BillingRateCard{ + Key: "tokens", + Name: "Tokens", + Price: price, + BillingCadence: &bc, + UnitConfig: &api.BillingUnitConfig{ + Operation: api.BillingUnitConfigOperationMultiply, + ConversionFactor: "1.2", + }, + } + + result, err := FromAPIBillingRateCard(rc) + require.NoError(t, err) + + uc := result.AsMeta().UnitConfig + require.NotNil(t, uc) + assert.Equal(t, productcatalog.UnitConfigOperationMultiply, uc.Operation) + assert.True(t, uc.ConversionFactor.Equal(decimal.NewFromFloat(1.2))) + assert.Nil(t, uc.Rounding) + assert.Nil(t, uc.DisplayUnit) + }) + + t.Run("parses unit_config with divide and rounding", func(t *testing.T) { + var price api.BillingPrice + require.NoError(t, price.FromBillingPriceUnit(api.BillingPriceUnit{Amount: "10", Type: "unit"})) + + bc := api.ISO8601Duration("P1M") + + rc := api.BillingRateCard{ + Key: "bytes", + Name: "Bytes", + Price: price, + BillingCadence: &bc, + UnitConfig: &api.BillingUnitConfig{ + Operation: api.BillingUnitConfigOperationDivide, + ConversionFactor: "1000000000", + Rounding: lo.ToPtr(api.BillingUnitConfigRoundingModeCeiling), + DisplayUnit: lo.ToPtr("GB"), + }, + } + + result, err := FromAPIBillingRateCard(rc) + require.NoError(t, err) + + uc := result.AsMeta().UnitConfig + require.NotNil(t, uc) + assert.Equal(t, productcatalog.UnitConfigOperationDivide, uc.Operation) + assert.True(t, uc.ConversionFactor.Equal(decimal.NewFromInt(1_000_000_000))) + require.NotNil(t, uc.Rounding) + assert.Equal(t, productcatalog.UnitConfigRoundingModeCeiling, *uc.Rounding) + require.NotNil(t, uc.DisplayUnit) + assert.Equal(t, "GB", *uc.DisplayUnit) + }) + + t.Run("rejects invalid conversion_factor decimal", func(t *testing.T) { + var price api.BillingPrice + require.NoError(t, price.FromBillingPriceUnit(api.BillingPriceUnit{Amount: "1", Type: "unit"})) + + bc := api.ISO8601Duration("P1M") + + rc := api.BillingRateCard{ + Key: "tokens", + Name: "Tokens", + Price: price, + BillingCadence: &bc, + UnitConfig: &api.BillingUnitConfig{ + Operation: api.BillingUnitConfigOperationMultiply, + ConversionFactor: "not-a-number", + }, + } + + _, err := FromAPIBillingRateCard(rc) + require.Error(t, err) + assert.Contains(t, err.Error(), "unit config") + }) } func TestToBillingPrice(t *testing.T) { diff --git a/api/v3/handlers/plans/list.go b/api/v3/handlers/plans/list.go index 0829c64e00..0f664a7fc6 100644 --- a/api/v3/handlers/plans/list.go +++ b/api/v3/handlers/plans/list.go @@ -111,11 +111,6 @@ func (h *handler) ListPlans() ListPlansHandler { items := make([]api.BillingPlan, 0, len(result.Items)) for _, p := range result.Items { - // FIXME: For now we skip plans containing price types not representable in v3 (e.g., package, dynamic). We'll add full bidirectional transform later on. - if hasUnsupportedV3Price(p) { - continue - } - billingPlan, err := ToAPIBillingPlan(p) if err != nil { return ListPlansResponse{}, err diff --git a/api/v3/openapi.yaml b/api/v3/openapi.yaml index 2a469ac32c..732de6cf9a 100644 --- a/api/v3/openapi.yaml +++ b/api/v3/openapi.yaml @@ -5704,6 +5704,19 @@ components: - $ref: '#/components/schemas/BillingPrice' description: The price of the rate card. title: Price + unit_config: + allOf: + - $ref: '#/components/schemas/BillingUnitConfig' + description: |- + Unit conversion configuration for the rate card. + + When set, transforms the raw metered quantity into a billing quantity before + pricing. Valid only with unit, graduated, or volume prices. + + For plans authored with v1 dynamic or package prices, the unit config is + synthesized on read: dynamic prices map to a unit price with a multiply unit + config, and package prices map to a unit price with a divide unit config. + title: Unit config payment_term: allOf: - $ref: '#/components/schemas/BillingPricePaymentTerm' @@ -6369,6 +6382,108 @@ components: readOnly: true additionalProperties: false description: Totals contains the summaries of all calculations for a billing resource. + BillingUnitConfig: + type: object + required: + - operation + - conversion_factor + properties: + operation: + allOf: + - $ref: '#/components/schemas/BillingUnitConfigOperation' + description: The arithmetic operation to apply to the raw metered quantity. + title: Conversion operation + conversion_factor: + allOf: + - $ref: '#/components/schemas/Numeric' + description: |- + The factor used in the conversion operation. + + - For `divide`: `converted = raw / conversionFactor`. + - For `multiply`: `converted = raw × conversionFactor`. + + Must be a positive non-zero value. + title: Conversion factor + rounding: + allOf: + - $ref: '#/components/schemas/BillingUnitConfigRoundingMode' + description: |- + The rounding mode applied to the converted quantity for invoicing. + + Defaults to none (no rounding). Entitlement checks always use the precise + (unrounded) value. + title: Rounding mode + default: none + precision: + type: integer + description: |- + The number of decimal places to retain after rounding. + + Only meaningful when rounding is not "none". Defaults to 0 (round to whole + numbers). + title: Rounding precision (decimal places) + default: 0 + display_unit: + type: string + description: |- + A human-readable label for the converted unit shown on invoices and in the + customer portal (e.g., "GB", "hours", "M tokens"). + + Optional. When omitted, no unit label is rendered. + title: Display unit label + additionalProperties: false + description: |- + Unit conversion configuration. + + Transforms raw metered quantities into billing-ready units before pricing and + entitlement evaluation. Applied at the rate card level so the same feature can + be billed in different units across plans. + + Examples: + + - Meter bytes, bill GB: operation=divide, conversionFactor=1e9, + rounding=ceiling, displayUnit="GB" + - Meter seconds, bill hours: operation=divide, conversionFactor=3600, + rounding=ceiling, displayUnit="hours" + - Cost + 20% margin: operation=multiply, conversionFactor=1.2 + - Bill per million tokens: operation=divide, conversionFactor=1e6, + rounding=ceiling, displayUnit="M" + + v1 equivalents: + + - DynamicPrice(multiplier): operation=multiply, conversionFactor=multiplier + + UnitPrice(amount=1) + - PackagePrice(amount, quantityPerPkg): operation=divide, + conversionFactor=quantityPerPkg, rounding=ceiling + UnitPrice(amount) + BillingUnitConfigOperation: + type: string + enum: + - divide + - multiply + description: |- + The arithmetic operation used to convert raw metered units into billing units. + + - `divide`: Divide the metered quantity by the conversion factor (e.g., bytes ÷ + 1e9 = GB). + - `multiply`: Multiply the metered quantity by the conversion factor (e.g., cost + × 1.2 = cost + 20% margin). + BillingUnitConfigRoundingMode: + type: string + enum: + - ceiling + - floor + - half_up + - none + description: |- + The rounding mode applied to the converted quantity for invoicing. + + Rounding is applied only to the invoiced quantity. Entitlement balance checks + use the precise decimal value after conversion. + + - `ceiling`: Round up to the next integer (typical for package-style billing). + - `floor`: Round down to the previous integer. + - `half_up`: Round to the nearest integer, with 0.5 rounding up. + - `none`: No rounding; the converted value is used as-is. BillingUsageBasedCharge: type: object required: diff --git a/e2e/plans_v3_test.go b/e2e/plans_v3_test.go index 9d8a3e1f80..abe4f86c45 100644 --- a/e2e/plans_v3_test.go +++ b/e2e/plans_v3_test.go @@ -4,9 +4,11 @@ import ( "net/http" "testing" + "github.com/samber/lo" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + api "github.com/openmeterio/openmeter/api/client/go" apiv3 "github.com/openmeterio/openmeter/api/v3" ) @@ -443,3 +445,161 @@ func TestV3PlanInvalidCurrency(t *testing.T) { assert.Equal(t, http.StatusBadRequest, status, "problem: %+v", problem) assertValidationCode(t, problem, "currency_invalid") } + +// TestV3PlanReadTranslatesV1DynamicAndPackagePrices verifies the v3 read-side +// translation of v1 dynamic and package prices into a unit price plus a +// synthesized unit_config on the rate card. +// +// Mapping (api/v3/handlers/plans/convert.go): +// - dynamic(multiplier=m) → unit(amount=1) + unit_config{operation=multiply, conversion_factor=m} +// - package(amount=a, qpp=q) → unit(amount=a) + unit_config{operation=divide, conversion_factor=q, rounding=ceiling} +// +// Spend commitments (min/max amount) carried on the v1 price flow through to +// the v3 rate card's commitments field unchanged. +// +// The plan is authored via the v1 SDK and read via v3 GET and v3 LIST. The +// unit_config field is read-only on v3, so this test does not exercise a v3 +// write path for it. +func TestV3PlanReadTranslatesV1DynamicAndPackagePrices(t *testing.T) { + v1 := initClient(t) + v3 := newV3Client(t) + + suffix := uniqueKey("v3_translates_v1") + planKey := suffix + dynamicRCKey := "dynamic_rc_" + suffix + packageRCKey := "package_rc_" + suffix + + dynamicPrice := api.RateCardUsageBasedPrice{} + require.NoError(t, dynamicPrice.FromDynamicPriceWithCommitments(api.DynamicPriceWithCommitments{ + Type: api.DynamicPriceWithCommitmentsTypeDynamic, + Multiplier: lo.ToPtr(api.Numeric("1.2")), + MinimumAmount: lo.ToPtr(api.Numeric("10")), + MaximumAmount: lo.ToPtr(api.Numeric("100")), + })) + + dynamicRC := api.RateCard{} + require.NoError(t, dynamicRC.FromRateCardUsageBased(api.RateCardUsageBased{ + Type: api.RateCardUsageBasedTypeUsageBased, + Name: "Dynamic RC", + Key: dynamicRCKey, + BillingCadence: "P1M", + Price: &dynamicPrice, + })) + + packagePrice := api.RateCardUsageBasedPrice{} + require.NoError(t, packagePrice.FromPackagePriceWithCommitments(api.PackagePriceWithCommitments{ + Type: api.PackagePriceWithCommitmentsTypePackage, + Amount: "0.5", + QuantityPerPackage: "1000", + MinimumAmount: lo.ToPtr(api.Numeric("5")), + })) + + packageRC := api.RateCard{} + require.NoError(t, packageRC.FromRateCardUsageBased(api.RateCardUsageBased{ + Type: api.RateCardUsageBasedTypeUsageBased, + Name: "Package RC", + Key: packageRCKey, + BillingCadence: "P1M", + Price: &packagePrice, + })) + + planCreate := api.PlanCreate{ + Currency: api.CurrencyCode("USD"), + Name: "v1 Plan with Dynamic and Package Prices", + Key: planKey, + BillingCadence: "P1M", + Phases: []api.PlanPhase{ + { + Name: "Phase 1", + Key: "phase_1_" + suffix, + RateCards: []api.RateCard{dynamicRC, packageRC}, + }, + }, + } + + var planID string + + t.Run("Should create the v1 plan with dynamic and package rate cards", func(t *testing.T) { + resp, err := v1.CreatePlanWithResponse(t.Context(), planCreate) + require.NoError(t, err) + require.Equal(t, http.StatusCreated, resp.StatusCode(), "body: %s", resp.Body) + require.NotNil(t, resp.JSON201) + + planID = resp.JSON201.Id + }) + + t.Run("v3 GET should translate dynamic price to unit + multiply unit_config", func(t *testing.T) { + require.NotEmpty(t, planID) + + status, plan, problem := v3.GetPlan(planID) + require.Equal(t, http.StatusOK, status, "problem: %+v", problem) + require.NotNil(t, plan) + + rc := findRateCardByKey(t, plan, dynamicRCKey) + + assertUnitPriceAmount(t, rc, "1") + + require.NotNil(t, rc.UnitConfig, "expected synthesized unit_config") + assert.Equal(t, apiv3.BillingUnitConfigOperationMultiply, rc.UnitConfig.Operation) + assert.Equal(t, apiv3.Numeric("1.2"), rc.UnitConfig.ConversionFactor) + assert.Nil(t, rc.UnitConfig.Rounding, "dynamic translation does not set rounding") + + require.NotNil(t, rc.Commitments, "v1 commitments should round-trip via v3") + assert.Equal(t, lo.ToPtr(apiv3.Numeric("10")), rc.Commitments.MinimumAmount) + assert.Equal(t, lo.ToPtr(apiv3.Numeric("100")), rc.Commitments.MaximumAmount) + }) + + t.Run("v3 GET should translate package price to unit + divide+ceiling unit_config", func(t *testing.T) { + require.NotEmpty(t, planID) + + status, plan, problem := v3.GetPlan(planID) + require.Equal(t, http.StatusOK, status, "problem: %+v", problem) + require.NotNil(t, plan) + + rc := findRateCardByKey(t, plan, packageRCKey) + + assertUnitPriceAmount(t, rc, "0.5") + + require.NotNil(t, rc.UnitConfig, "expected synthesized unit_config") + assert.Equal(t, apiv3.BillingUnitConfigOperationDivide, rc.UnitConfig.Operation) + assert.Equal(t, apiv3.Numeric("1000"), rc.UnitConfig.ConversionFactor) + require.NotNil(t, rc.UnitConfig.Rounding, "package translation must set rounding=ceiling") + assert.Equal(t, apiv3.BillingUnitConfigRoundingModeCeiling, *rc.UnitConfig.Rounding) + + require.NotNil(t, rc.Commitments, "v1 commitments should round-trip via v3") + assert.Equal(t, lo.ToPtr(apiv3.Numeric("5")), rc.Commitments.MinimumAmount) + assert.Nil(t, rc.Commitments.MaximumAmount) + }) + + t.Run("v3 LIST should include the plan with both rate cards translated", func(t *testing.T) { + require.NotEmpty(t, planID) + + // Bump page size so a fresh fixture isn't pushed off page 1 on a shared DB. + status, page, problem := v3.ListPlans(withPageSize(1000)) + require.Equal(t, http.StatusOK, status, "problem: %+v", problem) + require.NotNil(t, page) + + var found *apiv3.BillingPlan + for i := range page.Data { + if page.Data[i].Id == planID { + found = &page.Data[i] + break + } + } + require.NotNil(t, found, "created plan not in list response (the v3 list handler should no longer skip plans with v1 dynamic/package prices)") + + dynRC := findRateCardByKey(t, found, dynamicRCKey) + assertUnitPriceAmount(t, dynRC, "1") + require.NotNil(t, dynRC.UnitConfig) + assert.Equal(t, apiv3.BillingUnitConfigOperationMultiply, dynRC.UnitConfig.Operation) + assert.Equal(t, apiv3.Numeric("1.2"), dynRC.UnitConfig.ConversionFactor) + + pkgRC := findRateCardByKey(t, found, packageRCKey) + assertUnitPriceAmount(t, pkgRC, "0.5") + require.NotNil(t, pkgRC.UnitConfig) + assert.Equal(t, apiv3.BillingUnitConfigOperationDivide, pkgRC.UnitConfig.Operation) + assert.Equal(t, apiv3.Numeric("1000"), pkgRC.UnitConfig.ConversionFactor) + require.NotNil(t, pkgRC.UnitConfig.Rounding) + assert.Equal(t, apiv3.BillingUnitConfigRoundingModeCeiling, *pkgRC.UnitConfig.Rounding) + }) +} diff --git a/e2e/v3helpers_test.go b/e2e/v3helpers_test.go index 90fba3e8ce..3af4b8d940 100644 --- a/e2e/v3helpers_test.go +++ b/e2e/v3helpers_test.go @@ -512,3 +512,36 @@ func assertInvalidParameterRule(t *testing.T, problem *v3Problem, rule string) { } assert.Failf(t, "invalid parameter rule not found", "expected %q, got %v", rule, rules) } + +// findRateCardByKey looks up a rate card by key across all phases of a plan. +// Fails the test if no match is found. +func findRateCardByKey(t *testing.T, plan *apiv3.BillingPlan, key string) *apiv3.BillingRateCard { + t.Helper() + + for i := range plan.Phases { + for j := range plan.Phases[i].RateCards { + rc := &plan.Phases[i].RateCards[j] + if rc.Key == key { + return rc + } + } + } + + require.FailNow(t, "rate card not found", "key=%s", key) + return nil +} + +// assertUnitPriceAmount asserts the rate card's price discriminates as "unit" +// and carries the given amount. Used to verify the synthesized unit price that +// replaces v1 dynamic and package prices on the v3 read path. +func assertUnitPriceAmount(t *testing.T, rc *apiv3.BillingRateCard, want string) { + t.Helper() + + disc, err := rc.Price.Discriminator() + require.NoError(t, err) + require.Equal(t, "unit", disc, "expected synthesized unit price") + + unit, err := rc.Price.AsBillingPriceUnit() + require.NoError(t, err) + assert.Equal(t, want, unit.Amount) +} diff --git a/go.sum b/go.sum index 3459992a85..fbc082dc28 100644 --- a/go.sum +++ b/go.sum @@ -511,6 +511,8 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME= github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/clipperhouse/displaywidth v0.6.2 h1:ZDpTkFfpHOKte4RG5O/BOyf3ysnvFswpyYrV7z2uAKo= +github.com/clipperhouse/displaywidth v0.6.2/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o= github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4= @@ -1411,6 +1413,14 @@ github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= +github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc= +github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0= +github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM= +github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= +github.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0 h1:jrYnow5+hy3WRDCBypUFvVKNSPPCdqgSXIE9eJDD8LM= +github.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0/go.mod h1:b52bVQRRPObe+yyBl0TxNfhesL0nedD4Cht0/zx55Ew= +github.com/olekukonko/tablewriter v1.1.3 h1:VSHhghXxrP0JHl+0NnKid7WoEmd9/urKRJLysb70nnA= +github.com/olekukonko/tablewriter v1.1.3/go.mod h1:9VU0knjhmMkXjnMKrZ3+L2JhhtsQ/L38BbL3CRNE8tM= github.com/oliveagle/jsonpath v0.1.4 h1:Sr/ffH5YSyQKjSNfvDFkQqAqh3kn/QxF/7j2jjpfOAI= github.com/oliveagle/jsonpath v0.1.4/go.mod h1:diWEHhuLqib29heQcHYHyaLcxFC3KpKa/5ihkZBs1Z8= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= diff --git a/openmeter/billing/adapter/stdinvoicelinemapper.go b/openmeter/billing/adapter/stdinvoicelinemapper.go index e849c82a9c..908ad45f34 100644 --- a/openmeter/billing/adapter/stdinvoicelinemapper.go +++ b/openmeter/billing/adapter/stdinvoicelinemapper.go @@ -138,6 +138,8 @@ func (a *adapter) mapStandardInvoiceLineWithoutReferences(dbLine *db.BillingInvo MeteredQuantity: ubpLine.MeteredQuantity, PreLinePeriodQuantity: ubpLine.PreLinePeriodQuantity, MeteredPreLinePeriodQuantity: ubpLine.MeteredPreLinePeriodQuantity, + ConvertedQuantity: ubpLine.ConvertedQuantity, + AppliedUnitConfig: ubpLine.AppliedUnitConfig, } if len(dbLine.Edges.LineUsageDiscounts) > 0 { diff --git a/openmeter/billing/adapter/stdinvoicelines.go b/openmeter/billing/adapter/stdinvoicelines.go index 915472efe3..bd7401b720 100644 --- a/openmeter/billing/adapter/stdinvoicelines.go +++ b/openmeter/billing/adapter/stdinvoicelines.go @@ -534,7 +534,9 @@ func (a *adapter) upsertUsageBasedConfig(ctx context.Context, lineDiffs entitydi SetID(line.UsageBased.ConfigID). SetNillablePreLinePeriodQuantity(line.UsageBased.PreLinePeriodQuantity). SetNillableMeteredQuantity(line.UsageBased.MeteredQuantity). - SetNillableMeteredPreLinePeriodQuantity(line.UsageBased.MeteredPreLinePeriodQuantity) + SetNillableMeteredPreLinePeriodQuantity(line.UsageBased.MeteredPreLinePeriodQuantity). + SetNillableConvertedQuantity(line.UsageBased.ConvertedQuantity). + SetAppliedUnitConfig(line.UsageBased.AppliedUnitConfig) return create, nil }, diff --git a/openmeter/billing/charges/usagebased/adapter/charge.go b/openmeter/billing/charges/usagebased/adapter/charge.go index 3f315e60ef..4fcb5c4a28 100644 --- a/openmeter/billing/charges/usagebased/adapter/charge.go +++ b/openmeter/billing/charges/usagebased/adapter/charge.go @@ -35,6 +35,7 @@ func (a *adapter) UpdateCharge(ctx context.Context, charge usagebased.ChargeBase update := tx.db.ChargeUsageBased.UpdateOneID(charge.ID). Where(dbchargeusagebased.NamespaceEQ(charge.Namespace)). SetDiscounts(&charge.Intent.Discounts). + SetUnitConfig(charge.Intent.UnitConfig). SetFeatureID(charge.State.FeatureID). SetInvoiceAt(meta.NormalizeTimestamp(charge.Intent.InvoiceAt).In(time.UTC)). SetRatingEngine(charge.State.RatingEngine). @@ -227,6 +228,7 @@ func expandRealizations(query *db.ChargeUsageBasedQuery, expands meta.Expands) * func (a *adapter) buildCreateUsageBasedCharge(ctx context.Context, ns string, intent usagebased.CreateIntent) (*db.ChargeUsageBasedCreate, error) { create := a.db.ChargeUsageBased.Create(). SetDiscounts(&intent.Discounts). + SetUnitConfig(intent.UnitConfig). SetFeatureID(intent.FeatureID). SetRatingEngine(intent.RatingEngine). SetPrice(&intent.Price). diff --git a/openmeter/billing/charges/usagebased/adapter/mapper.go b/openmeter/billing/charges/usagebased/adapter/mapper.go index f3f4f4a234..24d93c21ed 100644 --- a/openmeter/billing/charges/usagebased/adapter/mapper.go +++ b/openmeter/billing/charges/usagebased/adapter/mapper.go @@ -51,6 +51,7 @@ func MapChargeBaseFromDB(entity *entdb.ChargeUsageBased) usagebased.ChargeBase { FeatureKey: entity.FeatureKey, Discounts: lo.FromPtr(entity.Discounts), Price: lo.FromPtr(entity.Price), + UnitConfig: entity.UnitConfig, }, State: usagebased.State{ CurrentRealizationRunID: entity.CurrentRealizationRunID, diff --git a/openmeter/billing/charges/usagebased/charge.go b/openmeter/billing/charges/usagebased/charge.go index a89f1f171d..62cb3eed17 100644 --- a/openmeter/billing/charges/usagebased/charge.go +++ b/openmeter/billing/charges/usagebased/charge.go @@ -193,6 +193,11 @@ type Intent struct { Price productcatalog.Price `json:"price"` Discounts productcatalog.Discounts `json:"discounts"` + + // UnitConfig optionally transforms the raw metered quantity into the + // billing quantity before rating. Snapshotted from the rate card at + // charge creation; immutable for the charge's lifetime. + UnitConfig *productcatalog.UnitConfig `json:"unitConfig,omitempty"` } func (i Intent) Normalized() Intent { @@ -225,6 +230,10 @@ func (i Intent) Validate() error { errs = append(errs, fmt.Errorf("price: %w", err)) } + if err := i.UnitConfig.Validate(); err != nil { + errs = append(errs, fmt.Errorf("unit config: %w", err)) + } + if i.FeatureKey == "" { errs = append(errs, fmt.Errorf("feature key is required")) } diff --git a/openmeter/billing/charges/usagebased/service/lineengine.go b/openmeter/billing/charges/usagebased/service/lineengine.go index dc2644d08f..25741f2640 100644 --- a/openmeter/billing/charges/usagebased/service/lineengine.go +++ b/openmeter/billing/charges/usagebased/service/lineengine.go @@ -168,7 +168,7 @@ func (e *LineEngine) OnStandardInvoiceCreated(ctx context.Context, input billing return nil, fmt.Errorf("getting current realization run for charge[%s]: %w", charge.ID, err) } - if err := populateUsageBasedStandardLineFromRun(stdLine, currentRun, charge.Realizations); err != nil { + if err := populateUsageBasedStandardLineFromRun(stdLine, charge.Intent, currentRun, charge.Realizations); err != nil { return nil, fmt.Errorf("populating standard line from run for charge[%s]: %w", charge.ID, err) } @@ -219,7 +219,7 @@ func (e *LineEngine) OnCollectionCompleted(ctx context.Context, input billing.On return nil, fmt.Errorf("getting current realization run for charge[%s]: %w", charge.ID, err) } - if err := populateUsageBasedStandardLineFromRun(stdLine, currentRun, charge.Realizations); err != nil { + if err := populateUsageBasedStandardLineFromRun(stdLine, charge.Intent, currentRun, charge.Realizations); err != nil { return nil, fmt.Errorf("populating standard line from run for charge[%s]: %w", charge.ID, err) } diff --git a/openmeter/billing/charges/usagebased/service/linemapper.go b/openmeter/billing/charges/usagebased/service/linemapper.go index 2caf72a5ec..69336814fb 100644 --- a/openmeter/billing/charges/usagebased/service/linemapper.go +++ b/openmeter/billing/charges/usagebased/service/linemapper.go @@ -14,7 +14,7 @@ import ( "github.com/openmeterio/openmeter/pkg/currencyx" ) -func populateUsageBasedStandardLineFromRun(stdLine *billing.StandardLine, run usagebased.RealizationRun, runs usagebased.RealizationRuns) error { +func populateUsageBasedStandardLineFromRun(stdLine *billing.StandardLine, intent usagebased.Intent, run usagebased.RealizationRun, runs usagebased.RealizationRuns) error { if stdLine.UsageBased == nil { stdLine.UsageBased = &billing.UsageBasedLine{} } @@ -33,6 +33,23 @@ func populateUsageBasedStandardLineFromRun(stdLine *billing.StandardLine, run us stdLine.UsageBased.MeteredQuantity = lo.ToPtr(billingMeteredQuantity.LinePeriod) stdLine.UsageBased.MeteredPreLinePeriodQuantity = lo.ToPtr(billingMeteredQuantity.PreLinePeriod) + // Snapshot UnitConfig + converted line-period quantity when the rate card + // has a UnitConfig. Cumulative-then-diff: converted line-period = + // Apply(cumulative_current).converted - Apply(cumulative_prior).converted. + // This matches the rating-input semantics (rating sees the invoiced + // cumulative), so for ceiling/floor rounding the line totals are correct + // even when raw usage moves within a single package boundary. + if intent.UnitConfig != nil { + cumulativeCurrent := billingMeteredQuantity.PreLinePeriod.Add(billingMeteredQuantity.LinePeriod) + convertedCurrent, _ := intent.UnitConfig.Apply(cumulativeCurrent) + convertedPrior, _ := intent.UnitConfig.Apply(billingMeteredQuantity.PreLinePeriod) + convertedLinePeriod := convertedCurrent.Sub(convertedPrior) + + stdLine.UsageBased.ConvertedQuantity = lo.ToPtr(convertedLinePeriod) + unitConfigSnapshot := intent.UnitConfig.Clone() + stdLine.UsageBased.AppliedUnitConfig = &unitConfigSnapshot + } + // Charge runs store cumulative raw metered quantity. Billing lines expose the raw // metered values separately from net billable quantities and consumed usage discounts, // so reuse the standard billing usage-discount mutator contract here. diff --git a/openmeter/billing/charges/usagebased/service/linemapper_test.go b/openmeter/billing/charges/usagebased/service/linemapper_test.go index c40c0700de..17e840ec93 100644 --- a/openmeter/billing/charges/usagebased/service/linemapper_test.go +++ b/openmeter/billing/charges/usagebased/service/linemapper_test.go @@ -97,7 +97,7 @@ func TestPopulateUsageBasedStandardLineFromRunProjectsDetailsAndCredits(t *testi }, } - err := populateUsageBasedStandardLineFromRun(line, run, usagebased.RealizationRuns{priorRun, run}) + err := populateUsageBasedStandardLineFromRun(line, usagebased.Intent{}, run, usagebased.RealizationRuns{priorRun, run}) require.NoError(t, err) require.Len(t, line.DetailedLines, 2) @@ -163,7 +163,7 @@ func TestPopulateUsageBasedStandardLineFromRunAppliesUsageDiscount(t *testing.T) }, } - err := populateUsageBasedStandardLineFromRun(line, run, usagebased.RealizationRuns{priorRun, run}) + err := populateUsageBasedStandardLineFromRun(line, usagebased.Intent{}, run, usagebased.RealizationRuns{priorRun, run}) require.NoError(t, err) require.Equal(t, float64(10), lo.FromPtr(line.UsageBased.Quantity).InexactFloat64()) @@ -183,6 +183,108 @@ func TestPopulateUsageBasedStandardLineFromRunAppliesUsageDiscount(t *testing.T) require.Equal(t, float64(10), reason.Quantity.InexactFloat64()) } +func TestPopulateUsageBasedStandardLineFromRunSnapshotsUnitConfig(t *testing.T) { + period := timeutil.ClosedPeriod{ + From: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC), + To: time.Date(2026, 2, 1, 0, 0, 0, 0, time.UTC), + } + + line := newUsageBasedStandardLineForTest(period) + + intent := usagebased.Intent{ + UnitConfig: &productcatalog.UnitConfig{ + Operation: productcatalog.UnitConfigOperationDivide, + ConversionFactor: alpacadecimal.NewFromInt(1000), + Rounding: lo.ToPtr(productcatalog.UnitConfigRoundingModeCeiling), + DisplayUnit: lo.ToPtr("packages"), + }, + } + + // Cumulative raw: prior=1000, current=2247. Line-period raw = 1247. + // Apply(divide 1000): converted_prior=1.000, converted_current=2.247. + // Converted line-period = 2.247 - 1.000 = 1.247. + priorRun := usagebased.RealizationRun{ + RealizationRunBase: usagebased.RealizationRunBase{ + ID: usagebased.RealizationRunID{ + Namespace: line.Namespace, + ID: "prior-run-id", + }, + Type: usagebased.RealizationRunTypePartialInvoice, + StoredAtLT: period.From, + ServicePeriodTo: period.From.Add(24 * time.Hour), + MeteredQuantity: alpacadecimal.NewFromInt(1000), + }, + } + + run := usagebased.RealizationRun{ + RealizationRunBase: usagebased.RealizationRunBase{ + ID: usagebased.RealizationRunID{ + Namespace: line.Namespace, + ID: "run-id", + }, + StoredAtLT: period.To, + ServicePeriodTo: period.To, + MeteredQuantity: alpacadecimal.NewFromInt(2247), + Totals: totals.Totals{ + Amount: alpacadecimal.NewFromInt(20), + Total: alpacadecimal.NewFromInt(20), + }, + }, + DetailedLines: mo.Some(usagebased.DetailedLines{ + newUsageBasedDetailedLineForTest("usage-a", period, alpacadecimal.NewFromInt(20)), + }), + } + + err := populateUsageBasedStandardLineFromRun(line, intent, run, usagebased.RealizationRuns{priorRun, run}) + require.NoError(t, err) + + require.Equal(t, float64(1247), lo.FromPtr(line.UsageBased.MeteredQuantity).InexactFloat64(), "raw line-period unchanged") + require.Equal(t, float64(1000), lo.FromPtr(line.UsageBased.MeteredPreLinePeriodQuantity).InexactFloat64(), "raw cumulative prior unchanged") + + require.NotNil(t, line.UsageBased.ConvertedQuantity) + require.Equal(t, "1.247", line.UsageBased.ConvertedQuantity.String(), "precise converted line-period via cumulative-then-diff") + + require.NotNil(t, line.UsageBased.AppliedUnitConfig) + require.Equal(t, productcatalog.UnitConfigOperationDivide, line.UsageBased.AppliedUnitConfig.Operation) + require.Equal(t, "1000", line.UsageBased.AppliedUnitConfig.ConversionFactor.String()) + require.Equal(t, productcatalog.UnitConfigRoundingModeCeiling, *line.UsageBased.AppliedUnitConfig.Rounding) + require.Equal(t, "packages", *line.UsageBased.AppliedUnitConfig.DisplayUnit) +} + +func TestPopulateUsageBasedStandardLineFromRunNoUnitConfigLeavesFieldsNil(t *testing.T) { + period := timeutil.ClosedPeriod{ + From: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC), + To: time.Date(2026, 2, 1, 0, 0, 0, 0, time.UTC), + } + + line := newUsageBasedStandardLineForTest(period) + + run := usagebased.RealizationRun{ + RealizationRunBase: usagebased.RealizationRunBase{ + ID: usagebased.RealizationRunID{ + Namespace: line.Namespace, + ID: "run-id", + }, + StoredAtLT: period.To, + ServicePeriodTo: period.To, + MeteredQuantity: alpacadecimal.NewFromInt(10), + Totals: totals.Totals{ + Amount: alpacadecimal.NewFromInt(10), + Total: alpacadecimal.NewFromInt(10), + }, + }, + DetailedLines: mo.Some(usagebased.DetailedLines{ + newUsageBasedDetailedLineForTest("usage-a", period, alpacadecimal.NewFromInt(10)), + }), + } + + err := populateUsageBasedStandardLineFromRun(line, usagebased.Intent{}, run, usagebased.RealizationRuns{run}) + require.NoError(t, err) + + require.Nil(t, line.UsageBased.ConvertedQuantity, "no UnitConfig → nil ConvertedQuantity (would be redundant with MeteredQuantity)") + require.Nil(t, line.UsageBased.AppliedUnitConfig, "no UnitConfig → nil snapshot") +} + func TestPopulateUsageBasedStandardLineFromRunRequiresExpandedDetails(t *testing.T) { period := timeutil.ClosedPeriod{ From: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC), @@ -206,7 +308,7 @@ func TestPopulateUsageBasedStandardLineFromRunRequiresExpandedDetails(t *testing }, } - err := populateUsageBasedStandardLineFromRun(line, run, usagebased.RealizationRuns{run}) + err := populateUsageBasedStandardLineFromRun(line, usagebased.Intent{}, run, usagebased.RealizationRuns{run}) require.ErrorContains(t, err, "detailed lines must be expanded") } diff --git a/openmeter/billing/charges/usagebased/service/rating/delta/base_test.go b/openmeter/billing/charges/usagebased/service/rating/delta/base_test.go index d5ff04ce42..8a7a6d9f8c 100644 --- a/openmeter/billing/charges/usagebased/service/rating/delta/base_test.go +++ b/openmeter/billing/charges/usagebased/service/rating/delta/base_test.go @@ -22,9 +22,10 @@ type deltaRatingTestPeriodsValue struct { } type deltaRatingTestCase struct { - price productcatalog.Price - discounts productcatalog.Discounts - phases []deltaRatingPhase + price productcatalog.Price + discounts productcatalog.Discounts + unitConfig *productcatalog.UnitConfig + phases []deltaRatingPhase } type deltaRatingPhase struct { @@ -45,6 +46,7 @@ func runDeltaRatingTestCase(t *testing.T, tc deltaRatingTestCase) { } intent := ratingtestutils.NewIntentForTest(t, fullServicePeriod, tc.price, tc.discounts) + intent.UnitConfig = tc.unitConfig engine := New(billingratingservice.New()) bookedDetailedLinesByPhase := make([]usagebased.DetailedLines, len(tc.phases)) diff --git a/openmeter/billing/charges/usagebased/service/rating/delta/engine.go b/openmeter/billing/charges/usagebased/service/rating/delta/engine.go index d7b86cbd8a..d8b14df1c8 100644 --- a/openmeter/billing/charges/usagebased/service/rating/delta/engine.go +++ b/openmeter/billing/charges/usagebased/service/rating/delta/engine.go @@ -83,10 +83,15 @@ func (e Engine) Rate(_ context.Context, in Input) (Result, error) { opts = append(opts, billingrating.WithMinimumCommitmentIgnored()) } + // Rating sees the billing quantity. Apply UnitConfig to the cumulative raw + // metered quantity once here so downstream rating tier/package math + // operates on converted units. Identity when UnitConfig is nil. + _, billingQuantity := in.Intent.UnitConfig.Apply(in.CurrentPeriod.MeteredQuantity) + billingDetailedLines, err := e.ratingService.GenerateDetailedLines(usagebased.RateableIntent{ Intent: in.Intent, ServicePeriod: in.CurrentPeriod.ServicePeriod, - MeterValue: in.CurrentPeriod.MeteredQuantity, + MeterValue: billingQuantity, }, opts...) if err != nil { return Result{}, fmt.Errorf("generating detailed lines: %w", err) diff --git a/openmeter/billing/charges/usagebased/service/rating/delta/unitconfig_test.go b/openmeter/billing/charges/usagebased/service/rating/delta/unitconfig_test.go new file mode 100644 index 0000000000..9c0013f656 --- /dev/null +++ b/openmeter/billing/charges/usagebased/service/rating/delta/unitconfig_test.go @@ -0,0 +1,161 @@ +package delta + +import ( + "testing" + + "github.com/alpacahq/alpacadecimal" + "github.com/samber/lo" + + ratingtestutils "github.com/openmeterio/openmeter/openmeter/billing/charges/usagebased/service/rating/testutils" + "github.com/openmeterio/openmeter/openmeter/billing/models/stddetailedline" + billingrating "github.com/openmeterio/openmeter/openmeter/billing/rating" + "github.com/openmeterio/openmeter/openmeter/productcatalog" +) + +// TestUnitConfigMultiplyAppliedToRatedQuantity asserts that +// UnitPrice(1) + UnitConfig{multiply, m} rates raw qty × m as the billable +// quantity. This is the v3 equivalent of v1 DynamicPrice{multiplier: m} for +// the same raw qty (line totals match; only ChildUniqueReferenceID differs +// because v3 surfaces as a unit price). +func TestUnitConfigMultiplyAppliedToRatedQuantity(t *testing.T) { + t.Parallel() + + periods := deltaRatingTestPeriods() + + runDeltaRatingTestCase(t, deltaRatingTestCase{ + price: *productcatalog.NewPriceFrom(productcatalog.UnitPrice{ + Amount: alpacadecimal.NewFromInt(1), + }), + unitConfig: &productcatalog.UnitConfig{ + Operation: productcatalog.UnitConfigOperationMultiply, + ConversionFactor: alpacadecimal.NewFromFloat(1.5), + }, + phases: []deltaRatingPhase{ + { + period: periods.period1, + meteredQuantity: 10, + expectedDetailedLines: []ratingtestutils.ExpectedDetailedLine{ + { + ChildUniqueReferenceID: billingrating.UnitPriceUsageChildUniqueReferenceID, + Category: stddetailedline.CategoryRegular, + ServicePeriod: lo.ToPtr(periods.period1), + PerUnitAmount: 1, + Quantity: 15, + Totals: ratingtestutils.ExpectedTotals{ + Amount: 15, + Total: 15, + }, + }, + }, + expectedTotals: ratingtestutils.ExpectedTotals{ + Amount: 15, + Total: 15, + }, + }, + }, + }) +} + +// TestUnitConfigDivideCeilingAppliedToRatedQuantity asserts that +// UnitPrice(amount) + UnitConfig{divide, qty, ceiling} rates raw qty ÷ qty +// (rounded up) as the billable quantity. v3 equivalent of v1 +// PackagePrice{amount, qty} for the same raw qty. +func TestUnitConfigDivideCeilingAppliedToRatedQuantity(t *testing.T) { + t.Parallel() + + periods := deltaRatingTestPeriods() + + runDeltaRatingTestCase(t, deltaRatingTestCase{ + price: *productcatalog.NewPriceFrom(productcatalog.UnitPrice{ + Amount: alpacadecimal.NewFromInt(10), + }), + unitConfig: &productcatalog.UnitConfig{ + Operation: productcatalog.UnitConfigOperationDivide, + ConversionFactor: alpacadecimal.NewFromInt(1000), + Rounding: lo.ToPtr(productcatalog.UnitConfigRoundingModeCeiling), + }, + phases: []deltaRatingPhase{ + { + period: periods.period1, + meteredQuantity: 1247, + expectedDetailedLines: []ratingtestutils.ExpectedDetailedLine{ + { + ChildUniqueReferenceID: billingrating.UnitPriceUsageChildUniqueReferenceID, + Category: stddetailedline.CategoryRegular, + ServicePeriod: lo.ToPtr(periods.period1), + PerUnitAmount: 10, + Quantity: 2, + Totals: ratingtestutils.ExpectedTotals{ + Amount: 20, + Total: 20, + }, + }, + }, + expectedTotals: ratingtestutils.ExpectedTotals{ + Amount: 20, + Total: 20, + }, + }, + }, + }) +} + +// TestUnitConfigDivideCeilingCumulativeNoDoubleBilling asserts a key +// correctness property: applying UnitConfig{divide, ceiling} to cumulative +// raw quantity (not the per-run diff), then letting the delta engine's +// existing cumulative-minus-prior subtraction run, prevents double-billing +// when the customer adds raw usage that does not cross a new package +// boundary. +// +// Run 1: raw=1247 → invoiced=ceil(1.247)=2 packages → bill 2 packages. +// Run 2: raw=1500 → invoiced=ceil(1.500)=2 packages → same cumulative, no +// new line. +// +// Wrong design (apply UnitConfig to the diff) would charge: +// ceil((1500-1247)/1000) = ceil(0.253) = 1 additional package. +// Right design (apply to cumulative, diff in invoiced space) charges 0. +func TestUnitConfigDivideCeilingCumulativeNoDoubleBilling(t *testing.T) { + t.Parallel() + + periods := deltaRatingTestPeriods() + + runDeltaRatingTestCase(t, deltaRatingTestCase{ + price: *productcatalog.NewPriceFrom(productcatalog.UnitPrice{ + Amount: alpacadecimal.NewFromInt(10), + }), + unitConfig: &productcatalog.UnitConfig{ + Operation: productcatalog.UnitConfigOperationDivide, + ConversionFactor: alpacadecimal.NewFromInt(1000), + Rounding: lo.ToPtr(productcatalog.UnitConfigRoundingModeCeiling), + }, + phases: []deltaRatingPhase{ + { + period: periods.period1, + meteredQuantity: 1247, + expectedDetailedLines: []ratingtestutils.ExpectedDetailedLine{ + { + ChildUniqueReferenceID: billingrating.UnitPriceUsageChildUniqueReferenceID, + Category: stddetailedline.CategoryRegular, + ServicePeriod: lo.ToPtr(periods.period1), + PerUnitAmount: 10, + Quantity: 2, + Totals: ratingtestutils.ExpectedTotals{ + Amount: 20, + Total: 20, + }, + }, + }, + expectedTotals: ratingtestutils.ExpectedTotals{ + Amount: 20, + Total: 20, + }, + }, + { + period: periods.period2, + meteredQuantity: 1500, + expectedDetailedLines: []ratingtestutils.ExpectedDetailedLine{}, + expectedTotals: ratingtestutils.ExpectedTotals{}, + }, + }, + }) +} diff --git a/openmeter/billing/charges/usagebased/service/rating/totals.go b/openmeter/billing/charges/usagebased/service/rating/totals.go index 90b048038a..efa42ff27a 100644 --- a/openmeter/billing/charges/usagebased/service/rating/totals.go +++ b/openmeter/billing/charges/usagebased/service/rating/totals.go @@ -57,6 +57,11 @@ func (s *service) GetTotalsForUsage(ctx context.Context, in GetTotalsForUsageInp return totals.Totals{}, fmt.Errorf("get snapshot quantity: %w", err) } + // Rating sees the billing quantity. Apply UnitConfig to the cumulative raw + // snapshot once here so downstream rating tier/package math operates on + // converted units. Identity when UnitConfig is nil. + _, billingQuantity := in.Charge.Intent.UnitConfig.Apply(snapshotQuantity) + // Totals must stay gross before charge credit allocation; run creation applies credits later and expects gross rating totals here. opts := []billingrating.GenerateDetailedLinesOption{ billingrating.WithCreditsMutatorDisabled(), @@ -67,7 +72,7 @@ func (s *service) GetTotalsForUsage(ctx context.Context, in GetTotalsForUsageInp ratingResult, err := s.ratingService.GenerateDetailedLines(usagebased.RateableIntent{ Intent: in.Charge.Intent, - MeterValue: snapshotQuantity, + MeterValue: billingQuantity, ServicePeriod: in.Charge.Intent.ServicePeriod, }, opts...) if err != nil { diff --git a/openmeter/billing/stdinvoiceline.go b/openmeter/billing/stdinvoiceline.go index 73179a64ab..1b68af04f7 100644 --- a/openmeter/billing/stdinvoiceline.go +++ b/openmeter/billing/stdinvoiceline.go @@ -999,6 +999,15 @@ type UsageBasedLine struct { PreLinePeriodQuantity *alpacadecimal.Decimal `json:"preLinePeriodQuantity,omitempty"` MeteredPreLinePeriodQuantity *alpacadecimal.Decimal `json:"meteredPreLinePeriodQuantity,omitempty"` + + // ConvertedQuantity is the precise (unrounded) line-period quantity after + // UnitConfig conversion. Nil when AppliedUnitConfig is nil. Differs from + // the invoiced quantity only when UnitConfig.Rounding is set. + ConvertedQuantity *alpacadecimal.Decimal `json:"convertedQuantity,omitempty"` + + // AppliedUnitConfig is the UnitConfig snapshot in effect when this line + // was rated. Nil when the rate card had no UnitConfig. + AppliedUnitConfig *productcatalog.UnitConfig `json:"appliedUnitConfig,omitempty"` } func (i UsageBasedLine) Equal(other *UsageBasedLine) bool { diff --git a/openmeter/billing/worker/subscriptionsync/service/reconciler/patchchargeusagebased.go b/openmeter/billing/worker/subscriptionsync/service/reconciler/patchchargeusagebased.go index 7dc89d3ae0..de4ddab09c 100644 --- a/openmeter/billing/worker/subscriptionsync/service/reconciler/patchchargeusagebased.go +++ b/openmeter/billing/worker/subscriptionsync/service/reconciler/patchchargeusagebased.go @@ -112,6 +112,7 @@ func newUsageBasedChargeIntent(target targetstate.StateItem) (charges.ChargeInte FeatureKey: lo.FromPtr(rateCardMeta.FeatureKey), Price: *price, Discounts: rateCardMeta.Discounts, + UnitConfig: rateCardMeta.UnitConfig, }) return intent, nil diff --git a/openmeter/ent/db/addonratecard.go b/openmeter/ent/db/addonratecard.go index fb79f60fba..ac73ab36bc 100644 --- a/openmeter/ent/db/addonratecard.go +++ b/openmeter/ent/db/addonratecard.go @@ -55,6 +55,8 @@ type AddonRateCard struct { BillingCadence *datetime.ISODurationString `json:"billing_cadence,omitempty"` // Price holds the value of the "price" field. Price *productcatalog.Price `json:"price,omitempty"` + // UnitConfig holds the value of the "unit_config" field. + UnitConfig *productcatalog.UnitConfig `json:"unit_config,omitempty"` // Discounts holds the value of the "discounts" field. Discounts *productcatalog.Discounts `json:"discounts,omitempty"` // The add-on identifier the ratecard is assigned to. @@ -130,6 +132,8 @@ func (*AddonRateCard) scanValues(columns []string) ([]any, error) { values[i] = addonratecard.ValueScanner.TaxConfig.ScanValue() case addonratecard.FieldPrice: values[i] = addonratecard.ValueScanner.Price.ScanValue() + case addonratecard.FieldUnitConfig: + values[i] = addonratecard.ValueScanner.UnitConfig.ScanValue() case addonratecard.FieldDiscounts: values[i] = addonratecard.ValueScanner.Discounts.ScanValue() default: @@ -257,6 +261,12 @@ func (_m *AddonRateCard) assignValues(columns []string, values []any) error { } else { _m.Price = value } + case addonratecard.FieldUnitConfig: + if value, err := addonratecard.ValueScanner.UnitConfig.FromValue(values[i]); err != nil { + return err + } else { + _m.UnitConfig = value + } case addonratecard.FieldDiscounts: if value, err := addonratecard.ValueScanner.Discounts.FromValue(values[i]); err != nil { return err @@ -393,6 +403,11 @@ func (_m *AddonRateCard) String() string { builder.WriteString(fmt.Sprintf("%v", *v)) } builder.WriteString(", ") + if v := _m.UnitConfig; v != nil { + builder.WriteString("unit_config=") + builder.WriteString(fmt.Sprintf("%v", *v)) + } + builder.WriteString(", ") if v := _m.Discounts; v != nil { builder.WriteString("discounts=") builder.WriteString(fmt.Sprintf("%v", *v)) diff --git a/openmeter/ent/db/addonratecard/addonratecard.go b/openmeter/ent/db/addonratecard/addonratecard.go index b6ab8e93d1..c71ae5aa79 100644 --- a/openmeter/ent/db/addonratecard/addonratecard.go +++ b/openmeter/ent/db/addonratecard/addonratecard.go @@ -49,6 +49,8 @@ const ( FieldBillingCadence = "billing_cadence" // FieldPrice holds the string denoting the price field in the database. FieldPrice = "price" + // FieldUnitConfig holds the string denoting the unit_config field in the database. + FieldUnitConfig = "unit_config" // FieldDiscounts holds the string denoting the discounts field in the database. FieldDiscounts = "discounts" // FieldAddonID holds the string denoting the addon_id field in the database. @@ -105,6 +107,7 @@ var Columns = []string{ FieldTaxConfig, FieldBillingCadence, FieldPrice, + FieldUnitConfig, FieldDiscounts, FieldAddonID, FieldFeatureID, @@ -140,6 +143,7 @@ var ( EntitlementTemplate field.TypeValueScanner[*productcatalog.EntitlementTemplate] TaxConfig field.TypeValueScanner[*productcatalog.TaxConfig] Price field.TypeValueScanner[*productcatalog.Price] + UnitConfig field.TypeValueScanner[*productcatalog.UnitConfig] Discounts field.TypeValueScanner[*productcatalog.Discounts] } ) @@ -247,6 +251,11 @@ func ByPrice(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldPrice, opts...).ToFunc() } +// ByUnitConfig orders the results by the unit_config field. +func ByUnitConfig(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldUnitConfig, opts...).ToFunc() +} + // ByDiscounts orders the results by the discounts field. func ByDiscounts(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldDiscounts, opts...).ToFunc() diff --git a/openmeter/ent/db/addonratecard/where.go b/openmeter/ent/db/addonratecard/where.go index 35f1f785d1..dba5a807e4 100644 --- a/openmeter/ent/db/addonratecard/where.go +++ b/openmeter/ent/db/addonratecard/where.go @@ -882,6 +882,16 @@ func PriceNotNil() predicate.AddonRateCard { return predicate.AddonRateCard(sql.FieldNotNull(FieldPrice)) } +// UnitConfigIsNil applies the IsNil predicate on the "unit_config" field. +func UnitConfigIsNil() predicate.AddonRateCard { + return predicate.AddonRateCard(sql.FieldIsNull(FieldUnitConfig)) +} + +// UnitConfigNotNil applies the NotNil predicate on the "unit_config" field. +func UnitConfigNotNil() predicate.AddonRateCard { + return predicate.AddonRateCard(sql.FieldNotNull(FieldUnitConfig)) +} + // DiscountsIsNil applies the IsNil predicate on the "discounts" field. func DiscountsIsNil() predicate.AddonRateCard { return predicate.AddonRateCard(sql.FieldIsNull(FieldDiscounts)) diff --git a/openmeter/ent/db/addonratecard_create.go b/openmeter/ent/db/addonratecard_create.go index 111de519b5..3e1c9250e8 100644 --- a/openmeter/ent/db/addonratecard_create.go +++ b/openmeter/ent/db/addonratecard_create.go @@ -188,6 +188,12 @@ func (_c *AddonRateCardCreate) SetPrice(v *productcatalog.Price) *AddonRateCardC return _c } +// SetUnitConfig sets the "unit_config" field. +func (_c *AddonRateCardCreate) SetUnitConfig(v *productcatalog.UnitConfig) *AddonRateCardCreate { + _c.mutation.SetUnitConfig(v) + return _c +} + // SetDiscounts sets the "discounts" field. func (_c *AddonRateCardCreate) SetDiscounts(v *productcatalog.Discounts) *AddonRateCardCreate { _c.mutation.SetDiscounts(v) @@ -361,6 +367,11 @@ func (_c *AddonRateCardCreate) check() error { return &ValidationError{Name: "price", err: fmt.Errorf(`db: validator failed for field "AddonRateCard.price": %w`, err)} } } + if v, ok := _c.mutation.UnitConfig(); ok { + if err := v.Validate(); err != nil { + return &ValidationError{Name: "unit_config", err: fmt.Errorf(`db: validator failed for field "AddonRateCard.unit_config": %w`, err)} + } + } if v, ok := _c.mutation.Discounts(); ok { if err := v.Validate(); err != nil { return &ValidationError{Name: "discounts", err: fmt.Errorf(`db: validator failed for field "AddonRateCard.discounts": %w`, err)} @@ -488,6 +499,14 @@ func (_c *AddonRateCardCreate) createSpec() (*AddonRateCard, *sqlgraph.CreateSpe _spec.SetField(addonratecard.FieldPrice, field.TypeString, vv) _node.Price = value } + if value, ok := _c.mutation.UnitConfig(); ok { + vv, err := addonratecard.ValueScanner.UnitConfig.Value(value) + if err != nil { + return nil, nil, err + } + _spec.SetField(addonratecard.FieldUnitConfig, field.TypeString, vv) + _node.UnitConfig = value + } if value, ok := _c.mutation.Discounts(); ok { vv, err := addonratecard.ValueScanner.Discounts.Value(value) if err != nil { @@ -803,6 +822,24 @@ func (u *AddonRateCardUpsert) ClearPrice() *AddonRateCardUpsert { return u } +// SetUnitConfig sets the "unit_config" field. +func (u *AddonRateCardUpsert) SetUnitConfig(v *productcatalog.UnitConfig) *AddonRateCardUpsert { + u.Set(addonratecard.FieldUnitConfig, v) + return u +} + +// UpdateUnitConfig sets the "unit_config" field to the value that was provided on create. +func (u *AddonRateCardUpsert) UpdateUnitConfig() *AddonRateCardUpsert { + u.SetExcluded(addonratecard.FieldUnitConfig) + return u +} + +// ClearUnitConfig clears the value of the "unit_config" field. +func (u *AddonRateCardUpsert) ClearUnitConfig() *AddonRateCardUpsert { + u.SetNull(addonratecard.FieldUnitConfig) + return u +} + // SetDiscounts sets the "discounts" field. func (u *AddonRateCardUpsert) SetDiscounts(v *productcatalog.Discounts) *AddonRateCardUpsert { u.Set(addonratecard.FieldDiscounts, v) @@ -1149,6 +1186,27 @@ func (u *AddonRateCardUpsertOne) ClearPrice() *AddonRateCardUpsertOne { }) } +// SetUnitConfig sets the "unit_config" field. +func (u *AddonRateCardUpsertOne) SetUnitConfig(v *productcatalog.UnitConfig) *AddonRateCardUpsertOne { + return u.Update(func(s *AddonRateCardUpsert) { + s.SetUnitConfig(v) + }) +} + +// UpdateUnitConfig sets the "unit_config" field to the value that was provided on create. +func (u *AddonRateCardUpsertOne) UpdateUnitConfig() *AddonRateCardUpsertOne { + return u.Update(func(s *AddonRateCardUpsert) { + s.UpdateUnitConfig() + }) +} + +// ClearUnitConfig clears the value of the "unit_config" field. +func (u *AddonRateCardUpsertOne) ClearUnitConfig() *AddonRateCardUpsertOne { + return u.Update(func(s *AddonRateCardUpsert) { + s.ClearUnitConfig() + }) +} + // SetDiscounts sets the "discounts" field. func (u *AddonRateCardUpsertOne) SetDiscounts(v *productcatalog.Discounts) *AddonRateCardUpsertOne { return u.Update(func(s *AddonRateCardUpsert) { @@ -1673,6 +1731,27 @@ func (u *AddonRateCardUpsertBulk) ClearPrice() *AddonRateCardUpsertBulk { }) } +// SetUnitConfig sets the "unit_config" field. +func (u *AddonRateCardUpsertBulk) SetUnitConfig(v *productcatalog.UnitConfig) *AddonRateCardUpsertBulk { + return u.Update(func(s *AddonRateCardUpsert) { + s.SetUnitConfig(v) + }) +} + +// UpdateUnitConfig sets the "unit_config" field to the value that was provided on create. +func (u *AddonRateCardUpsertBulk) UpdateUnitConfig() *AddonRateCardUpsertBulk { + return u.Update(func(s *AddonRateCardUpsert) { + s.UpdateUnitConfig() + }) +} + +// ClearUnitConfig clears the value of the "unit_config" field. +func (u *AddonRateCardUpsertBulk) ClearUnitConfig() *AddonRateCardUpsertBulk { + return u.Update(func(s *AddonRateCardUpsert) { + s.ClearUnitConfig() + }) +} + // SetDiscounts sets the "discounts" field. func (u *AddonRateCardUpsertBulk) SetDiscounts(v *productcatalog.Discounts) *AddonRateCardUpsertBulk { return u.Update(func(s *AddonRateCardUpsert) { diff --git a/openmeter/ent/db/addonratecard_update.go b/openmeter/ent/db/addonratecard_update.go index ea3b544a4a..311c25a1c9 100644 --- a/openmeter/ent/db/addonratecard_update.go +++ b/openmeter/ent/db/addonratecard_update.go @@ -221,6 +221,18 @@ func (_u *AddonRateCardUpdate) ClearPrice() *AddonRateCardUpdate { return _u } +// SetUnitConfig sets the "unit_config" field. +func (_u *AddonRateCardUpdate) SetUnitConfig(v *productcatalog.UnitConfig) *AddonRateCardUpdate { + _u.mutation.SetUnitConfig(v) + return _u +} + +// ClearUnitConfig clears the value of the "unit_config" field. +func (_u *AddonRateCardUpdate) ClearUnitConfig() *AddonRateCardUpdate { + _u.mutation.ClearUnitConfig() + return _u +} + // SetDiscounts sets the "discounts" field. func (_u *AddonRateCardUpdate) SetDiscounts(v *productcatalog.Discounts) *AddonRateCardUpdate { _u.mutation.SetDiscounts(v) @@ -377,6 +389,11 @@ func (_u *AddonRateCardUpdate) check() error { return &ValidationError{Name: "price", err: fmt.Errorf(`db: validator failed for field "AddonRateCard.price": %w`, err)} } } + if v, ok := _u.mutation.UnitConfig(); ok { + if err := v.Validate(); err != nil { + return &ValidationError{Name: "unit_config", err: fmt.Errorf(`db: validator failed for field "AddonRateCard.unit_config": %w`, err)} + } + } if v, ok := _u.mutation.Discounts(); ok { if err := v.Validate(); err != nil { return &ValidationError{Name: "discounts", err: fmt.Errorf(`db: validator failed for field "AddonRateCard.discounts": %w`, err)} @@ -477,6 +494,16 @@ func (_u *AddonRateCardUpdate) sqlSave(ctx context.Context) (_node int, err erro if _u.mutation.PriceCleared() { _spec.ClearField(addonratecard.FieldPrice, field.TypeString) } + if value, ok := _u.mutation.UnitConfig(); ok { + vv, err := addonratecard.ValueScanner.UnitConfig.Value(value) + if err != nil { + return 0, err + } + _spec.SetField(addonratecard.FieldUnitConfig, field.TypeString, vv) + } + if _u.mutation.UnitConfigCleared() { + _spec.ClearField(addonratecard.FieldUnitConfig, field.TypeString) + } if value, ok := _u.mutation.Discounts(); ok { vv, err := addonratecard.ValueScanner.Discounts.Value(value) if err != nil { @@ -782,6 +809,18 @@ func (_u *AddonRateCardUpdateOne) ClearPrice() *AddonRateCardUpdateOne { return _u } +// SetUnitConfig sets the "unit_config" field. +func (_u *AddonRateCardUpdateOne) SetUnitConfig(v *productcatalog.UnitConfig) *AddonRateCardUpdateOne { + _u.mutation.SetUnitConfig(v) + return _u +} + +// ClearUnitConfig clears the value of the "unit_config" field. +func (_u *AddonRateCardUpdateOne) ClearUnitConfig() *AddonRateCardUpdateOne { + _u.mutation.ClearUnitConfig() + return _u +} + // SetDiscounts sets the "discounts" field. func (_u *AddonRateCardUpdateOne) SetDiscounts(v *productcatalog.Discounts) *AddonRateCardUpdateOne { _u.mutation.SetDiscounts(v) @@ -951,6 +990,11 @@ func (_u *AddonRateCardUpdateOne) check() error { return &ValidationError{Name: "price", err: fmt.Errorf(`db: validator failed for field "AddonRateCard.price": %w`, err)} } } + if v, ok := _u.mutation.UnitConfig(); ok { + if err := v.Validate(); err != nil { + return &ValidationError{Name: "unit_config", err: fmt.Errorf(`db: validator failed for field "AddonRateCard.unit_config": %w`, err)} + } + } if v, ok := _u.mutation.Discounts(); ok { if err := v.Validate(); err != nil { return &ValidationError{Name: "discounts", err: fmt.Errorf(`db: validator failed for field "AddonRateCard.discounts": %w`, err)} @@ -1068,6 +1112,16 @@ func (_u *AddonRateCardUpdateOne) sqlSave(ctx context.Context) (_node *AddonRate if _u.mutation.PriceCleared() { _spec.ClearField(addonratecard.FieldPrice, field.TypeString) } + if value, ok := _u.mutation.UnitConfig(); ok { + vv, err := addonratecard.ValueScanner.UnitConfig.Value(value) + if err != nil { + return nil, err + } + _spec.SetField(addonratecard.FieldUnitConfig, field.TypeString, vv) + } + if _u.mutation.UnitConfigCleared() { + _spec.ClearField(addonratecard.FieldUnitConfig, field.TypeString) + } if value, ok := _u.mutation.Discounts(); ok { vv, err := addonratecard.ValueScanner.Discounts.Value(value) if err != nil { diff --git a/openmeter/ent/db/billinginvoiceusagebasedlineconfig.go b/openmeter/ent/db/billinginvoiceusagebasedlineconfig.go index 30220b91eb..64a7a3ae4d 100644 --- a/openmeter/ent/db/billinginvoiceusagebasedlineconfig.go +++ b/openmeter/ent/db/billinginvoiceusagebasedlineconfig.go @@ -32,7 +32,11 @@ type BillingInvoiceUsageBasedLineConfig struct { MeteredPreLinePeriodQuantity *alpacadecimal.Decimal `json:"metered_pre_line_period_quantity,omitempty"` // MeteredQuantity holds the value of the "metered_quantity" field. MeteredQuantity *alpacadecimal.Decimal `json:"metered_quantity,omitempty"` - selectValues sql.SelectValues + // ConvertedQuantity holds the value of the "converted_quantity" field. + ConvertedQuantity *alpacadecimal.Decimal `json:"converted_quantity,omitempty"` + // AppliedUnitConfig holds the value of the "applied_unit_config" field. + AppliedUnitConfig *productcatalog.UnitConfig `json:"applied_unit_config,omitempty"` + selectValues sql.SelectValues } // scanValues returns the types for scanning values from sql.Rows. @@ -40,12 +44,14 @@ func (*BillingInvoiceUsageBasedLineConfig) scanValues(columns []string) ([]any, values := make([]any, len(columns)) for i := range columns { switch columns[i] { - case billinginvoiceusagebasedlineconfig.FieldPreLinePeriodQuantity, billinginvoiceusagebasedlineconfig.FieldMeteredPreLinePeriodQuantity, billinginvoiceusagebasedlineconfig.FieldMeteredQuantity: + case billinginvoiceusagebasedlineconfig.FieldPreLinePeriodQuantity, billinginvoiceusagebasedlineconfig.FieldMeteredPreLinePeriodQuantity, billinginvoiceusagebasedlineconfig.FieldMeteredQuantity, billinginvoiceusagebasedlineconfig.FieldConvertedQuantity: values[i] = &sql.NullScanner{S: new(alpacadecimal.Decimal)} case billinginvoiceusagebasedlineconfig.FieldID, billinginvoiceusagebasedlineconfig.FieldNamespace, billinginvoiceusagebasedlineconfig.FieldPriceType, billinginvoiceusagebasedlineconfig.FieldFeatureKey: values[i] = new(sql.NullString) case billinginvoiceusagebasedlineconfig.FieldPrice: values[i] = billinginvoiceusagebasedlineconfig.ValueScanner.Price.ScanValue() + case billinginvoiceusagebasedlineconfig.FieldAppliedUnitConfig: + values[i] = billinginvoiceusagebasedlineconfig.ValueScanner.AppliedUnitConfig.ScanValue() default: values[i] = new(sql.UnknownType) } @@ -113,6 +119,19 @@ func (_m *BillingInvoiceUsageBasedLineConfig) assignValues(columns []string, val _m.MeteredQuantity = new(alpacadecimal.Decimal) *_m.MeteredQuantity = *value.S.(*alpacadecimal.Decimal) } + case billinginvoiceusagebasedlineconfig.FieldConvertedQuantity: + if value, ok := values[i].(*sql.NullScanner); !ok { + return fmt.Errorf("unexpected type %T for field converted_quantity", values[i]) + } else if value.Valid { + _m.ConvertedQuantity = new(alpacadecimal.Decimal) + *_m.ConvertedQuantity = *value.S.(*alpacadecimal.Decimal) + } + case billinginvoiceusagebasedlineconfig.FieldAppliedUnitConfig: + if value, err := billinginvoiceusagebasedlineconfig.ValueScanner.AppliedUnitConfig.FromValue(values[i]); err != nil { + return err + } else { + _m.AppliedUnitConfig = value + } default: _m.selectValues.Set(columns[i], values[i]) } @@ -177,6 +196,16 @@ func (_m *BillingInvoiceUsageBasedLineConfig) String() string { builder.WriteString("metered_quantity=") builder.WriteString(fmt.Sprintf("%v", *v)) } + builder.WriteString(", ") + if v := _m.ConvertedQuantity; v != nil { + builder.WriteString("converted_quantity=") + builder.WriteString(fmt.Sprintf("%v", *v)) + } + builder.WriteString(", ") + if v := _m.AppliedUnitConfig; v != nil { + builder.WriteString("applied_unit_config=") + builder.WriteString(fmt.Sprintf("%v", *v)) + } builder.WriteByte(')') return builder.String() } diff --git a/openmeter/ent/db/billinginvoiceusagebasedlineconfig/billinginvoiceusagebasedlineconfig.go b/openmeter/ent/db/billinginvoiceusagebasedlineconfig/billinginvoiceusagebasedlineconfig.go index 123442fa73..0f60f42d0f 100644 --- a/openmeter/ent/db/billinginvoiceusagebasedlineconfig/billinginvoiceusagebasedlineconfig.go +++ b/openmeter/ent/db/billinginvoiceusagebasedlineconfig/billinginvoiceusagebasedlineconfig.go @@ -29,6 +29,10 @@ const ( FieldMeteredPreLinePeriodQuantity = "metered_pre_line_period_quantity" // FieldMeteredQuantity holds the string denoting the metered_quantity field in the database. FieldMeteredQuantity = "metered_quantity" + // FieldConvertedQuantity holds the string denoting the converted_quantity field in the database. + FieldConvertedQuantity = "converted_quantity" + // FieldAppliedUnitConfig holds the string denoting the applied_unit_config field in the database. + FieldAppliedUnitConfig = "applied_unit_config" // Table holds the table name of the billinginvoiceusagebasedlineconfig in the database. Table = "billing_invoice_usage_based_line_configs" ) @@ -43,6 +47,8 @@ var Columns = []string{ FieldPreLinePeriodQuantity, FieldMeteredPreLinePeriodQuantity, FieldMeteredQuantity, + FieldConvertedQuantity, + FieldAppliedUnitConfig, } // ValidColumn reports if the column name is valid (part of the table columns). @@ -62,7 +68,8 @@ var ( DefaultID func() string // ValueScanner of all BillingInvoiceUsageBasedLineConfig fields. ValueScanner struct { - Price field.TypeValueScanner[*productcatalog.Price] + Price field.TypeValueScanner[*productcatalog.Price] + AppliedUnitConfig field.TypeValueScanner[*productcatalog.UnitConfig] } ) @@ -118,3 +125,13 @@ func ByMeteredPreLinePeriodQuantity(opts ...sql.OrderTermOption) OrderOption { func ByMeteredQuantity(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldMeteredQuantity, opts...).ToFunc() } + +// ByConvertedQuantity orders the results by the converted_quantity field. +func ByConvertedQuantity(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldConvertedQuantity, opts...).ToFunc() +} + +// ByAppliedUnitConfig orders the results by the applied_unit_config field. +func ByAppliedUnitConfig(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldAppliedUnitConfig, opts...).ToFunc() +} diff --git a/openmeter/ent/db/billinginvoiceusagebasedlineconfig/where.go b/openmeter/ent/db/billinginvoiceusagebasedlineconfig/where.go index 23fb795d4b..ef2955b70c 100644 --- a/openmeter/ent/db/billinginvoiceusagebasedlineconfig/where.go +++ b/openmeter/ent/db/billinginvoiceusagebasedlineconfig/where.go @@ -89,6 +89,11 @@ func MeteredQuantity(v alpacadecimal.Decimal) predicate.BillingInvoiceUsageBased return predicate.BillingInvoiceUsageBasedLineConfig(sql.FieldEQ(FieldMeteredQuantity, v)) } +// ConvertedQuantity applies equality check predicate on the "converted_quantity" field. It's identical to ConvertedQuantityEQ. +func ConvertedQuantity(v alpacadecimal.Decimal) predicate.BillingInvoiceUsageBasedLineConfig { + return predicate.BillingInvoiceUsageBasedLineConfig(sql.FieldEQ(FieldConvertedQuantity, v)) +} + // NamespaceEQ applies the EQ predicate on the "namespace" field. func NamespaceEQ(v string) predicate.BillingInvoiceUsageBasedLineConfig { return predicate.BillingInvoiceUsageBasedLineConfig(sql.FieldEQ(FieldNamespace, v)) @@ -409,6 +414,66 @@ func MeteredQuantityNotNil() predicate.BillingInvoiceUsageBasedLineConfig { return predicate.BillingInvoiceUsageBasedLineConfig(sql.FieldNotNull(FieldMeteredQuantity)) } +// ConvertedQuantityEQ applies the EQ predicate on the "converted_quantity" field. +func ConvertedQuantityEQ(v alpacadecimal.Decimal) predicate.BillingInvoiceUsageBasedLineConfig { + return predicate.BillingInvoiceUsageBasedLineConfig(sql.FieldEQ(FieldConvertedQuantity, v)) +} + +// ConvertedQuantityNEQ applies the NEQ predicate on the "converted_quantity" field. +func ConvertedQuantityNEQ(v alpacadecimal.Decimal) predicate.BillingInvoiceUsageBasedLineConfig { + return predicate.BillingInvoiceUsageBasedLineConfig(sql.FieldNEQ(FieldConvertedQuantity, v)) +} + +// ConvertedQuantityIn applies the In predicate on the "converted_quantity" field. +func ConvertedQuantityIn(vs ...alpacadecimal.Decimal) predicate.BillingInvoiceUsageBasedLineConfig { + return predicate.BillingInvoiceUsageBasedLineConfig(sql.FieldIn(FieldConvertedQuantity, vs...)) +} + +// ConvertedQuantityNotIn applies the NotIn predicate on the "converted_quantity" field. +func ConvertedQuantityNotIn(vs ...alpacadecimal.Decimal) predicate.BillingInvoiceUsageBasedLineConfig { + return predicate.BillingInvoiceUsageBasedLineConfig(sql.FieldNotIn(FieldConvertedQuantity, vs...)) +} + +// ConvertedQuantityGT applies the GT predicate on the "converted_quantity" field. +func ConvertedQuantityGT(v alpacadecimal.Decimal) predicate.BillingInvoiceUsageBasedLineConfig { + return predicate.BillingInvoiceUsageBasedLineConfig(sql.FieldGT(FieldConvertedQuantity, v)) +} + +// ConvertedQuantityGTE applies the GTE predicate on the "converted_quantity" field. +func ConvertedQuantityGTE(v alpacadecimal.Decimal) predicate.BillingInvoiceUsageBasedLineConfig { + return predicate.BillingInvoiceUsageBasedLineConfig(sql.FieldGTE(FieldConvertedQuantity, v)) +} + +// ConvertedQuantityLT applies the LT predicate on the "converted_quantity" field. +func ConvertedQuantityLT(v alpacadecimal.Decimal) predicate.BillingInvoiceUsageBasedLineConfig { + return predicate.BillingInvoiceUsageBasedLineConfig(sql.FieldLT(FieldConvertedQuantity, v)) +} + +// ConvertedQuantityLTE applies the LTE predicate on the "converted_quantity" field. +func ConvertedQuantityLTE(v alpacadecimal.Decimal) predicate.BillingInvoiceUsageBasedLineConfig { + return predicate.BillingInvoiceUsageBasedLineConfig(sql.FieldLTE(FieldConvertedQuantity, v)) +} + +// ConvertedQuantityIsNil applies the IsNil predicate on the "converted_quantity" field. +func ConvertedQuantityIsNil() predicate.BillingInvoiceUsageBasedLineConfig { + return predicate.BillingInvoiceUsageBasedLineConfig(sql.FieldIsNull(FieldConvertedQuantity)) +} + +// ConvertedQuantityNotNil applies the NotNil predicate on the "converted_quantity" field. +func ConvertedQuantityNotNil() predicate.BillingInvoiceUsageBasedLineConfig { + return predicate.BillingInvoiceUsageBasedLineConfig(sql.FieldNotNull(FieldConvertedQuantity)) +} + +// AppliedUnitConfigIsNil applies the IsNil predicate on the "applied_unit_config" field. +func AppliedUnitConfigIsNil() predicate.BillingInvoiceUsageBasedLineConfig { + return predicate.BillingInvoiceUsageBasedLineConfig(sql.FieldIsNull(FieldAppliedUnitConfig)) +} + +// AppliedUnitConfigNotNil applies the NotNil predicate on the "applied_unit_config" field. +func AppliedUnitConfigNotNil() predicate.BillingInvoiceUsageBasedLineConfig { + return predicate.BillingInvoiceUsageBasedLineConfig(sql.FieldNotNull(FieldAppliedUnitConfig)) +} + // And groups predicates with the AND operator between them. func And(predicates ...predicate.BillingInvoiceUsageBasedLineConfig) predicate.BillingInvoiceUsageBasedLineConfig { return predicate.BillingInvoiceUsageBasedLineConfig(sql.AndPredicates(predicates...)) diff --git a/openmeter/ent/db/billinginvoiceusagebasedlineconfig_create.go b/openmeter/ent/db/billinginvoiceusagebasedlineconfig_create.go index 8b67f3a0ea..74a62682da 100644 --- a/openmeter/ent/db/billinginvoiceusagebasedlineconfig_create.go +++ b/openmeter/ent/db/billinginvoiceusagebasedlineconfig_create.go @@ -98,6 +98,26 @@ func (_c *BillingInvoiceUsageBasedLineConfigCreate) SetNillableMeteredQuantity(v return _c } +// SetConvertedQuantity sets the "converted_quantity" field. +func (_c *BillingInvoiceUsageBasedLineConfigCreate) SetConvertedQuantity(v alpacadecimal.Decimal) *BillingInvoiceUsageBasedLineConfigCreate { + _c.mutation.SetConvertedQuantity(v) + return _c +} + +// SetNillableConvertedQuantity sets the "converted_quantity" field if the given value is not nil. +func (_c *BillingInvoiceUsageBasedLineConfigCreate) SetNillableConvertedQuantity(v *alpacadecimal.Decimal) *BillingInvoiceUsageBasedLineConfigCreate { + if v != nil { + _c.SetConvertedQuantity(*v) + } + return _c +} + +// SetAppliedUnitConfig sets the "applied_unit_config" field. +func (_c *BillingInvoiceUsageBasedLineConfigCreate) SetAppliedUnitConfig(v *productcatalog.UnitConfig) *BillingInvoiceUsageBasedLineConfigCreate { + _c.mutation.SetAppliedUnitConfig(v) + return _c +} + // SetID sets the "id" field. func (_c *BillingInvoiceUsageBasedLineConfigCreate) SetID(v string) *BillingInvoiceUsageBasedLineConfigCreate { _c.mutation.SetID(v) @@ -179,6 +199,11 @@ func (_c *BillingInvoiceUsageBasedLineConfigCreate) check() error { return &ValidationError{Name: "price", err: fmt.Errorf(`db: validator failed for field "BillingInvoiceUsageBasedLineConfig.price": %w`, err)} } } + if v, ok := _c.mutation.AppliedUnitConfig(); ok { + if err := v.Validate(); err != nil { + return &ValidationError{Name: "applied_unit_config", err: fmt.Errorf(`db: validator failed for field "BillingInvoiceUsageBasedLineConfig.applied_unit_config": %w`, err)} + } + } return nil } @@ -250,6 +275,18 @@ func (_c *BillingInvoiceUsageBasedLineConfigCreate) createSpec() (*BillingInvoic _spec.SetField(billinginvoiceusagebasedlineconfig.FieldMeteredQuantity, field.TypeOther, value) _node.MeteredQuantity = &value } + if value, ok := _c.mutation.ConvertedQuantity(); ok { + _spec.SetField(billinginvoiceusagebasedlineconfig.FieldConvertedQuantity, field.TypeOther, value) + _node.ConvertedQuantity = &value + } + if value, ok := _c.mutation.AppliedUnitConfig(); ok { + vv, err := billinginvoiceusagebasedlineconfig.ValueScanner.AppliedUnitConfig.Value(value) + if err != nil { + return nil, nil, err + } + _spec.SetField(billinginvoiceusagebasedlineconfig.FieldAppliedUnitConfig, field.TypeString, vv) + _node.AppliedUnitConfig = value + } return _node, _spec, nil } @@ -380,6 +417,42 @@ func (u *BillingInvoiceUsageBasedLineConfigUpsert) ClearMeteredQuantity() *Billi return u } +// SetConvertedQuantity sets the "converted_quantity" field. +func (u *BillingInvoiceUsageBasedLineConfigUpsert) SetConvertedQuantity(v alpacadecimal.Decimal) *BillingInvoiceUsageBasedLineConfigUpsert { + u.Set(billinginvoiceusagebasedlineconfig.FieldConvertedQuantity, v) + return u +} + +// UpdateConvertedQuantity sets the "converted_quantity" field to the value that was provided on create. +func (u *BillingInvoiceUsageBasedLineConfigUpsert) UpdateConvertedQuantity() *BillingInvoiceUsageBasedLineConfigUpsert { + u.SetExcluded(billinginvoiceusagebasedlineconfig.FieldConvertedQuantity) + return u +} + +// ClearConvertedQuantity clears the value of the "converted_quantity" field. +func (u *BillingInvoiceUsageBasedLineConfigUpsert) ClearConvertedQuantity() *BillingInvoiceUsageBasedLineConfigUpsert { + u.SetNull(billinginvoiceusagebasedlineconfig.FieldConvertedQuantity) + return u +} + +// SetAppliedUnitConfig sets the "applied_unit_config" field. +func (u *BillingInvoiceUsageBasedLineConfigUpsert) SetAppliedUnitConfig(v *productcatalog.UnitConfig) *BillingInvoiceUsageBasedLineConfigUpsert { + u.Set(billinginvoiceusagebasedlineconfig.FieldAppliedUnitConfig, v) + return u +} + +// UpdateAppliedUnitConfig sets the "applied_unit_config" field to the value that was provided on create. +func (u *BillingInvoiceUsageBasedLineConfigUpsert) UpdateAppliedUnitConfig() *BillingInvoiceUsageBasedLineConfigUpsert { + u.SetExcluded(billinginvoiceusagebasedlineconfig.FieldAppliedUnitConfig) + return u +} + +// ClearAppliedUnitConfig clears the value of the "applied_unit_config" field. +func (u *BillingInvoiceUsageBasedLineConfigUpsert) ClearAppliedUnitConfig() *BillingInvoiceUsageBasedLineConfigUpsert { + u.SetNull(billinginvoiceusagebasedlineconfig.FieldAppliedUnitConfig) + return u +} + // UpdateNewValues updates the mutable fields using the new values that were set on create except the ID field. // Using this option is equivalent to using: // @@ -525,6 +598,48 @@ func (u *BillingInvoiceUsageBasedLineConfigUpsertOne) ClearMeteredQuantity() *Bi }) } +// SetConvertedQuantity sets the "converted_quantity" field. +func (u *BillingInvoiceUsageBasedLineConfigUpsertOne) SetConvertedQuantity(v alpacadecimal.Decimal) *BillingInvoiceUsageBasedLineConfigUpsertOne { + return u.Update(func(s *BillingInvoiceUsageBasedLineConfigUpsert) { + s.SetConvertedQuantity(v) + }) +} + +// UpdateConvertedQuantity sets the "converted_quantity" field to the value that was provided on create. +func (u *BillingInvoiceUsageBasedLineConfigUpsertOne) UpdateConvertedQuantity() *BillingInvoiceUsageBasedLineConfigUpsertOne { + return u.Update(func(s *BillingInvoiceUsageBasedLineConfigUpsert) { + s.UpdateConvertedQuantity() + }) +} + +// ClearConvertedQuantity clears the value of the "converted_quantity" field. +func (u *BillingInvoiceUsageBasedLineConfigUpsertOne) ClearConvertedQuantity() *BillingInvoiceUsageBasedLineConfigUpsertOne { + return u.Update(func(s *BillingInvoiceUsageBasedLineConfigUpsert) { + s.ClearConvertedQuantity() + }) +} + +// SetAppliedUnitConfig sets the "applied_unit_config" field. +func (u *BillingInvoiceUsageBasedLineConfigUpsertOne) SetAppliedUnitConfig(v *productcatalog.UnitConfig) *BillingInvoiceUsageBasedLineConfigUpsertOne { + return u.Update(func(s *BillingInvoiceUsageBasedLineConfigUpsert) { + s.SetAppliedUnitConfig(v) + }) +} + +// UpdateAppliedUnitConfig sets the "applied_unit_config" field to the value that was provided on create. +func (u *BillingInvoiceUsageBasedLineConfigUpsertOne) UpdateAppliedUnitConfig() *BillingInvoiceUsageBasedLineConfigUpsertOne { + return u.Update(func(s *BillingInvoiceUsageBasedLineConfigUpsert) { + s.UpdateAppliedUnitConfig() + }) +} + +// ClearAppliedUnitConfig clears the value of the "applied_unit_config" field. +func (u *BillingInvoiceUsageBasedLineConfigUpsertOne) ClearAppliedUnitConfig() *BillingInvoiceUsageBasedLineConfigUpsertOne { + return u.Update(func(s *BillingInvoiceUsageBasedLineConfigUpsert) { + s.ClearAppliedUnitConfig() + }) +} + // Exec executes the query. func (u *BillingInvoiceUsageBasedLineConfigUpsertOne) Exec(ctx context.Context) error { if len(u.create.conflict) == 0 { @@ -840,6 +955,48 @@ func (u *BillingInvoiceUsageBasedLineConfigUpsertBulk) ClearMeteredQuantity() *B }) } +// SetConvertedQuantity sets the "converted_quantity" field. +func (u *BillingInvoiceUsageBasedLineConfigUpsertBulk) SetConvertedQuantity(v alpacadecimal.Decimal) *BillingInvoiceUsageBasedLineConfigUpsertBulk { + return u.Update(func(s *BillingInvoiceUsageBasedLineConfigUpsert) { + s.SetConvertedQuantity(v) + }) +} + +// UpdateConvertedQuantity sets the "converted_quantity" field to the value that was provided on create. +func (u *BillingInvoiceUsageBasedLineConfigUpsertBulk) UpdateConvertedQuantity() *BillingInvoiceUsageBasedLineConfigUpsertBulk { + return u.Update(func(s *BillingInvoiceUsageBasedLineConfigUpsert) { + s.UpdateConvertedQuantity() + }) +} + +// ClearConvertedQuantity clears the value of the "converted_quantity" field. +func (u *BillingInvoiceUsageBasedLineConfigUpsertBulk) ClearConvertedQuantity() *BillingInvoiceUsageBasedLineConfigUpsertBulk { + return u.Update(func(s *BillingInvoiceUsageBasedLineConfigUpsert) { + s.ClearConvertedQuantity() + }) +} + +// SetAppliedUnitConfig sets the "applied_unit_config" field. +func (u *BillingInvoiceUsageBasedLineConfigUpsertBulk) SetAppliedUnitConfig(v *productcatalog.UnitConfig) *BillingInvoiceUsageBasedLineConfigUpsertBulk { + return u.Update(func(s *BillingInvoiceUsageBasedLineConfigUpsert) { + s.SetAppliedUnitConfig(v) + }) +} + +// UpdateAppliedUnitConfig sets the "applied_unit_config" field to the value that was provided on create. +func (u *BillingInvoiceUsageBasedLineConfigUpsertBulk) UpdateAppliedUnitConfig() *BillingInvoiceUsageBasedLineConfigUpsertBulk { + return u.Update(func(s *BillingInvoiceUsageBasedLineConfigUpsert) { + s.UpdateAppliedUnitConfig() + }) +} + +// ClearAppliedUnitConfig clears the value of the "applied_unit_config" field. +func (u *BillingInvoiceUsageBasedLineConfigUpsertBulk) ClearAppliedUnitConfig() *BillingInvoiceUsageBasedLineConfigUpsertBulk { + return u.Update(func(s *BillingInvoiceUsageBasedLineConfigUpsert) { + s.ClearAppliedUnitConfig() + }) +} + // Exec executes the query. func (u *BillingInvoiceUsageBasedLineConfigUpsertBulk) Exec(ctx context.Context) error { if u.create.err != nil { diff --git a/openmeter/ent/db/billinginvoiceusagebasedlineconfig_update.go b/openmeter/ent/db/billinginvoiceusagebasedlineconfig_update.go index e6844ed264..90c60711e9 100644 --- a/openmeter/ent/db/billinginvoiceusagebasedlineconfig_update.go +++ b/openmeter/ent/db/billinginvoiceusagebasedlineconfig_update.go @@ -109,6 +109,38 @@ func (_u *BillingInvoiceUsageBasedLineConfigUpdate) ClearMeteredQuantity() *Bill return _u } +// SetConvertedQuantity sets the "converted_quantity" field. +func (_u *BillingInvoiceUsageBasedLineConfigUpdate) SetConvertedQuantity(v alpacadecimal.Decimal) *BillingInvoiceUsageBasedLineConfigUpdate { + _u.mutation.SetConvertedQuantity(v) + return _u +} + +// SetNillableConvertedQuantity sets the "converted_quantity" field if the given value is not nil. +func (_u *BillingInvoiceUsageBasedLineConfigUpdate) SetNillableConvertedQuantity(v *alpacadecimal.Decimal) *BillingInvoiceUsageBasedLineConfigUpdate { + if v != nil { + _u.SetConvertedQuantity(*v) + } + return _u +} + +// ClearConvertedQuantity clears the value of the "converted_quantity" field. +func (_u *BillingInvoiceUsageBasedLineConfigUpdate) ClearConvertedQuantity() *BillingInvoiceUsageBasedLineConfigUpdate { + _u.mutation.ClearConvertedQuantity() + return _u +} + +// SetAppliedUnitConfig sets the "applied_unit_config" field. +func (_u *BillingInvoiceUsageBasedLineConfigUpdate) SetAppliedUnitConfig(v *productcatalog.UnitConfig) *BillingInvoiceUsageBasedLineConfigUpdate { + _u.mutation.SetAppliedUnitConfig(v) + return _u +} + +// ClearAppliedUnitConfig clears the value of the "applied_unit_config" field. +func (_u *BillingInvoiceUsageBasedLineConfigUpdate) ClearAppliedUnitConfig() *BillingInvoiceUsageBasedLineConfigUpdate { + _u.mutation.ClearAppliedUnitConfig() + return _u +} + // Mutation returns the BillingInvoiceUsageBasedLineConfigMutation object of the builder. func (_u *BillingInvoiceUsageBasedLineConfigUpdate) Mutation() *BillingInvoiceUsageBasedLineConfigMutation { return _u.mutation @@ -153,6 +185,11 @@ func (_u *BillingInvoiceUsageBasedLineConfigUpdate) check() error { return &ValidationError{Name: "price", err: fmt.Errorf(`db: validator failed for field "BillingInvoiceUsageBasedLineConfig.price": %w`, err)} } } + if v, ok := _u.mutation.AppliedUnitConfig(); ok { + if err := v.Validate(); err != nil { + return &ValidationError{Name: "applied_unit_config", err: fmt.Errorf(`db: validator failed for field "BillingInvoiceUsageBasedLineConfig.applied_unit_config": %w`, err)} + } + } return nil } @@ -199,6 +236,22 @@ func (_u *BillingInvoiceUsageBasedLineConfigUpdate) sqlSave(ctx context.Context) if _u.mutation.MeteredQuantityCleared() { _spec.ClearField(billinginvoiceusagebasedlineconfig.FieldMeteredQuantity, field.TypeOther) } + if value, ok := _u.mutation.ConvertedQuantity(); ok { + _spec.SetField(billinginvoiceusagebasedlineconfig.FieldConvertedQuantity, field.TypeOther, value) + } + if _u.mutation.ConvertedQuantityCleared() { + _spec.ClearField(billinginvoiceusagebasedlineconfig.FieldConvertedQuantity, field.TypeOther) + } + if value, ok := _u.mutation.AppliedUnitConfig(); ok { + vv, err := billinginvoiceusagebasedlineconfig.ValueScanner.AppliedUnitConfig.Value(value) + if err != nil { + return 0, err + } + _spec.SetField(billinginvoiceusagebasedlineconfig.FieldAppliedUnitConfig, field.TypeString, vv) + } + if _u.mutation.AppliedUnitConfigCleared() { + _spec.ClearField(billinginvoiceusagebasedlineconfig.FieldAppliedUnitConfig, field.TypeString) + } if _node, err = sqlgraph.UpdateNodes(ctx, _u.driver, _spec); err != nil { if _, ok := err.(*sqlgraph.NotFoundError); ok { err = &NotFoundError{billinginvoiceusagebasedlineconfig.Label} @@ -299,6 +352,38 @@ func (_u *BillingInvoiceUsageBasedLineConfigUpdateOne) ClearMeteredQuantity() *B return _u } +// SetConvertedQuantity sets the "converted_quantity" field. +func (_u *BillingInvoiceUsageBasedLineConfigUpdateOne) SetConvertedQuantity(v alpacadecimal.Decimal) *BillingInvoiceUsageBasedLineConfigUpdateOne { + _u.mutation.SetConvertedQuantity(v) + return _u +} + +// SetNillableConvertedQuantity sets the "converted_quantity" field if the given value is not nil. +func (_u *BillingInvoiceUsageBasedLineConfigUpdateOne) SetNillableConvertedQuantity(v *alpacadecimal.Decimal) *BillingInvoiceUsageBasedLineConfigUpdateOne { + if v != nil { + _u.SetConvertedQuantity(*v) + } + return _u +} + +// ClearConvertedQuantity clears the value of the "converted_quantity" field. +func (_u *BillingInvoiceUsageBasedLineConfigUpdateOne) ClearConvertedQuantity() *BillingInvoiceUsageBasedLineConfigUpdateOne { + _u.mutation.ClearConvertedQuantity() + return _u +} + +// SetAppliedUnitConfig sets the "applied_unit_config" field. +func (_u *BillingInvoiceUsageBasedLineConfigUpdateOne) SetAppliedUnitConfig(v *productcatalog.UnitConfig) *BillingInvoiceUsageBasedLineConfigUpdateOne { + _u.mutation.SetAppliedUnitConfig(v) + return _u +} + +// ClearAppliedUnitConfig clears the value of the "applied_unit_config" field. +func (_u *BillingInvoiceUsageBasedLineConfigUpdateOne) ClearAppliedUnitConfig() *BillingInvoiceUsageBasedLineConfigUpdateOne { + _u.mutation.ClearAppliedUnitConfig() + return _u +} + // Mutation returns the BillingInvoiceUsageBasedLineConfigMutation object of the builder. func (_u *BillingInvoiceUsageBasedLineConfigUpdateOne) Mutation() *BillingInvoiceUsageBasedLineConfigMutation { return _u.mutation @@ -356,6 +441,11 @@ func (_u *BillingInvoiceUsageBasedLineConfigUpdateOne) check() error { return &ValidationError{Name: "price", err: fmt.Errorf(`db: validator failed for field "BillingInvoiceUsageBasedLineConfig.price": %w`, err)} } } + if v, ok := _u.mutation.AppliedUnitConfig(); ok { + if err := v.Validate(); err != nil { + return &ValidationError{Name: "applied_unit_config", err: fmt.Errorf(`db: validator failed for field "BillingInvoiceUsageBasedLineConfig.applied_unit_config": %w`, err)} + } + } return nil } @@ -419,6 +509,22 @@ func (_u *BillingInvoiceUsageBasedLineConfigUpdateOne) sqlSave(ctx context.Conte if _u.mutation.MeteredQuantityCleared() { _spec.ClearField(billinginvoiceusagebasedlineconfig.FieldMeteredQuantity, field.TypeOther) } + if value, ok := _u.mutation.ConvertedQuantity(); ok { + _spec.SetField(billinginvoiceusagebasedlineconfig.FieldConvertedQuantity, field.TypeOther, value) + } + if _u.mutation.ConvertedQuantityCleared() { + _spec.ClearField(billinginvoiceusagebasedlineconfig.FieldConvertedQuantity, field.TypeOther) + } + if value, ok := _u.mutation.AppliedUnitConfig(); ok { + vv, err := billinginvoiceusagebasedlineconfig.ValueScanner.AppliedUnitConfig.Value(value) + if err != nil { + return nil, err + } + _spec.SetField(billinginvoiceusagebasedlineconfig.FieldAppliedUnitConfig, field.TypeString, vv) + } + if _u.mutation.AppliedUnitConfigCleared() { + _spec.ClearField(billinginvoiceusagebasedlineconfig.FieldAppliedUnitConfig, field.TypeString) + } _node = &BillingInvoiceUsageBasedLineConfig{config: _u.config} _spec.Assign = _node.assignValues _spec.ScanValues = _node.scanValues diff --git a/openmeter/ent/db/chargeusagebased.go b/openmeter/ent/db/chargeusagebased.go index c7050520ee..f967fe52e3 100644 --- a/openmeter/ent/db/chargeusagebased.go +++ b/openmeter/ent/db/chargeusagebased.go @@ -88,6 +88,8 @@ type ChargeUsageBased struct { SettlementMode productcatalog.SettlementMode `json:"settlement_mode,omitempty"` // Discounts holds the value of the "discounts" field. Discounts *productcatalog.Discounts `json:"discounts,omitempty"` + // UnitConfig holds the value of the "unit_config" field. + UnitConfig *productcatalog.UnitConfig `json:"unit_config,omitempty"` // FeatureKey holds the value of the "feature_key" field. FeatureKey string `json:"feature_key,omitempty"` // FeatureID holds the value of the "feature_id" field. @@ -252,6 +254,8 @@ func (*ChargeUsageBased) scanValues(columns []string) ([]any, error) { values[i] = new(sql.NullTime) case chargeusagebased.FieldDiscounts: values[i] = chargeusagebased.ValueScanner.Discounts.ScanValue() + case chargeusagebased.FieldUnitConfig: + values[i] = chargeusagebased.ValueScanner.UnitConfig.ScanValue() case chargeusagebased.FieldPrice: values[i] = chargeusagebased.ValueScanner.Price.ScanValue() default: @@ -456,6 +460,12 @@ func (_m *ChargeUsageBased) assignValues(columns []string, values []any) error { } else { _m.Discounts = value } + case chargeusagebased.FieldUnitConfig: + if value, err := chargeusagebased.ValueScanner.UnitConfig.FromValue(values[i]); err != nil { + return err + } else { + _m.UnitConfig = value + } case chargeusagebased.FieldFeatureKey: if value, ok := values[i].(*sql.NullString); !ok { return fmt.Errorf("unexpected type %T for field feature_key", values[i]) @@ -683,6 +693,11 @@ func (_m *ChargeUsageBased) String() string { builder.WriteString(fmt.Sprintf("%v", *v)) } builder.WriteString(", ") + if v := _m.UnitConfig; v != nil { + builder.WriteString("unit_config=") + builder.WriteString(fmt.Sprintf("%v", *v)) + } + builder.WriteString(", ") builder.WriteString("feature_key=") builder.WriteString(_m.FeatureKey) builder.WriteString(", ") diff --git a/openmeter/ent/db/chargeusagebased/chargeusagebased.go b/openmeter/ent/db/chargeusagebased/chargeusagebased.go index 91be07f723..ea4230b4e2 100644 --- a/openmeter/ent/db/chargeusagebased/chargeusagebased.go +++ b/openmeter/ent/db/chargeusagebased/chargeusagebased.go @@ -76,6 +76,8 @@ const ( FieldSettlementMode = "settlement_mode" // FieldDiscounts holds the string denoting the discounts field in the database. FieldDiscounts = "discounts" + // FieldUnitConfig holds the string denoting the unit_config field in the database. + FieldUnitConfig = "unit_config" // FieldFeatureKey holds the string denoting the feature_key field in the database. FieldFeatureKey = "feature_key" // FieldFeatureID holds the string denoting the feature_id field in the database. @@ -213,6 +215,7 @@ var Columns = []string{ FieldInvoiceAt, FieldSettlementMode, FieldDiscounts, + FieldUnitConfig, FieldFeatureKey, FieldFeatureID, FieldRatingEngine, @@ -252,8 +255,9 @@ var ( DefaultID func() string // ValueScanner of all ChargeUsageBased fields. ValueScanner struct { - Discounts field.TypeValueScanner[*productcatalog.Discounts] - Price field.TypeValueScanner[*productcatalog.Price] + Discounts field.TypeValueScanner[*productcatalog.Discounts] + UnitConfig field.TypeValueScanner[*productcatalog.UnitConfig] + Price field.TypeValueScanner[*productcatalog.Price] } ) @@ -455,6 +459,11 @@ func ByDiscounts(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldDiscounts, opts...).ToFunc() } +// ByUnitConfig orders the results by the unit_config field. +func ByUnitConfig(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldUnitConfig, opts...).ToFunc() +} + // ByFeatureKey orders the results by the feature_key field. func ByFeatureKey(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldFeatureKey, opts...).ToFunc() diff --git a/openmeter/ent/db/chargeusagebased/where.go b/openmeter/ent/db/chargeusagebased/where.go index 53f43962ad..f4131341d1 100644 --- a/openmeter/ent/db/chargeusagebased/where.go +++ b/openmeter/ent/db/chargeusagebased/where.go @@ -1540,6 +1540,16 @@ func DiscountsNotNil() predicate.ChargeUsageBased { return predicate.ChargeUsageBased(sql.FieldNotNull(FieldDiscounts)) } +// UnitConfigIsNil applies the IsNil predicate on the "unit_config" field. +func UnitConfigIsNil() predicate.ChargeUsageBased { + return predicate.ChargeUsageBased(sql.FieldIsNull(FieldUnitConfig)) +} + +// UnitConfigNotNil applies the NotNil predicate on the "unit_config" field. +func UnitConfigNotNil() predicate.ChargeUsageBased { + return predicate.ChargeUsageBased(sql.FieldNotNull(FieldUnitConfig)) +} + // FeatureKeyEQ applies the EQ predicate on the "feature_key" field. func FeatureKeyEQ(v string) predicate.ChargeUsageBased { return predicate.ChargeUsageBased(sql.FieldEQ(FieldFeatureKey, v)) diff --git a/openmeter/ent/db/chargeusagebased_create.go b/openmeter/ent/db/chargeusagebased_create.go index caf82200ad..85461ff53d 100644 --- a/openmeter/ent/db/chargeusagebased_create.go +++ b/openmeter/ent/db/chargeusagebased_create.go @@ -294,6 +294,12 @@ func (_c *ChargeUsageBasedCreate) SetDiscounts(v *productcatalog.Discounts) *Cha return _c } +// SetUnitConfig sets the "unit_config" field. +func (_c *ChargeUsageBasedCreate) SetUnitConfig(v *productcatalog.UnitConfig) *ChargeUsageBasedCreate { + _c.mutation.SetUnitConfig(v) + return _c +} + // SetFeatureKey sets the "feature_key" field. func (_c *ChargeUsageBasedCreate) SetFeatureKey(v string) *ChargeUsageBasedCreate { _c.mutation.SetFeatureKey(v) @@ -589,6 +595,11 @@ func (_c *ChargeUsageBasedCreate) check() error { return &ValidationError{Name: "discounts", err: fmt.Errorf(`db: validator failed for field "ChargeUsageBased.discounts": %w`, err)} } } + if v, ok := _c.mutation.UnitConfig(); ok { + if err := v.Validate(); err != nil { + return &ValidationError{Name: "unit_config", err: fmt.Errorf(`db: validator failed for field "ChargeUsageBased.unit_config": %w`, err)} + } + } if _, ok := _c.mutation.FeatureKey(); !ok { return &ValidationError{Name: "feature_key", err: errors.New(`db: missing required field "ChargeUsageBased.feature_key"`)} } @@ -770,6 +781,14 @@ func (_c *ChargeUsageBasedCreate) createSpec() (*ChargeUsageBased, *sqlgraph.Cre _spec.SetField(chargeusagebased.FieldDiscounts, field.TypeString, vv) _node.Discounts = value } + if value, ok := _c.mutation.UnitConfig(); ok { + vv, err := chargeusagebased.ValueScanner.UnitConfig.Value(value) + if err != nil { + return nil, nil, err + } + _spec.SetField(chargeusagebased.FieldUnitConfig, field.TypeString, vv) + _node.UnitConfig = value + } if value, ok := _c.mutation.FeatureKey(); ok { _spec.SetField(chargeusagebased.FieldFeatureKey, field.TypeString, value) _node.FeatureKey = value @@ -1285,6 +1304,24 @@ func (u *ChargeUsageBasedUpsert) ClearDiscounts() *ChargeUsageBasedUpsert { return u } +// SetUnitConfig sets the "unit_config" field. +func (u *ChargeUsageBasedUpsert) SetUnitConfig(v *productcatalog.UnitConfig) *ChargeUsageBasedUpsert { + u.Set(chargeusagebased.FieldUnitConfig, v) + return u +} + +// UpdateUnitConfig sets the "unit_config" field to the value that was provided on create. +func (u *ChargeUsageBasedUpsert) UpdateUnitConfig() *ChargeUsageBasedUpsert { + u.SetExcluded(chargeusagebased.FieldUnitConfig) + return u +} + +// ClearUnitConfig clears the value of the "unit_config" field. +func (u *ChargeUsageBasedUpsert) ClearUnitConfig() *ChargeUsageBasedUpsert { + u.SetNull(chargeusagebased.FieldUnitConfig) + return u +} + // SetFeatureID sets the "feature_id" field. func (u *ChargeUsageBasedUpsert) SetFeatureID(v string) *ChargeUsageBasedUpsert { u.Set(chargeusagebased.FieldFeatureID, v) @@ -1742,6 +1779,27 @@ func (u *ChargeUsageBasedUpsertOne) ClearDiscounts() *ChargeUsageBasedUpsertOne }) } +// SetUnitConfig sets the "unit_config" field. +func (u *ChargeUsageBasedUpsertOne) SetUnitConfig(v *productcatalog.UnitConfig) *ChargeUsageBasedUpsertOne { + return u.Update(func(s *ChargeUsageBasedUpsert) { + s.SetUnitConfig(v) + }) +} + +// UpdateUnitConfig sets the "unit_config" field to the value that was provided on create. +func (u *ChargeUsageBasedUpsertOne) UpdateUnitConfig() *ChargeUsageBasedUpsertOne { + return u.Update(func(s *ChargeUsageBasedUpsert) { + s.UpdateUnitConfig() + }) +} + +// ClearUnitConfig clears the value of the "unit_config" field. +func (u *ChargeUsageBasedUpsertOne) ClearUnitConfig() *ChargeUsageBasedUpsertOne { + return u.Update(func(s *ChargeUsageBasedUpsert) { + s.ClearUnitConfig() + }) +} + // SetFeatureID sets the "feature_id" field. func (u *ChargeUsageBasedUpsertOne) SetFeatureID(v string) *ChargeUsageBasedUpsertOne { return u.Update(func(s *ChargeUsageBasedUpsert) { @@ -2378,6 +2436,27 @@ func (u *ChargeUsageBasedUpsertBulk) ClearDiscounts() *ChargeUsageBasedUpsertBul }) } +// SetUnitConfig sets the "unit_config" field. +func (u *ChargeUsageBasedUpsertBulk) SetUnitConfig(v *productcatalog.UnitConfig) *ChargeUsageBasedUpsertBulk { + return u.Update(func(s *ChargeUsageBasedUpsert) { + s.SetUnitConfig(v) + }) +} + +// UpdateUnitConfig sets the "unit_config" field to the value that was provided on create. +func (u *ChargeUsageBasedUpsertBulk) UpdateUnitConfig() *ChargeUsageBasedUpsertBulk { + return u.Update(func(s *ChargeUsageBasedUpsert) { + s.UpdateUnitConfig() + }) +} + +// ClearUnitConfig clears the value of the "unit_config" field. +func (u *ChargeUsageBasedUpsertBulk) ClearUnitConfig() *ChargeUsageBasedUpsertBulk { + return u.Update(func(s *ChargeUsageBasedUpsert) { + s.ClearUnitConfig() + }) +} + // SetFeatureID sets the "feature_id" field. func (u *ChargeUsageBasedUpsertBulk) SetFeatureID(v string) *ChargeUsageBasedUpsertBulk { return u.Update(func(s *ChargeUsageBasedUpsert) { diff --git a/openmeter/ent/db/chargeusagebased_update.go b/openmeter/ent/db/chargeusagebased_update.go index 491504cd93..4de83d4a93 100644 --- a/openmeter/ent/db/chargeusagebased_update.go +++ b/openmeter/ent/db/chargeusagebased_update.go @@ -319,6 +319,18 @@ func (_u *ChargeUsageBasedUpdate) ClearDiscounts() *ChargeUsageBasedUpdate { return _u } +// SetUnitConfig sets the "unit_config" field. +func (_u *ChargeUsageBasedUpdate) SetUnitConfig(v *productcatalog.UnitConfig) *ChargeUsageBasedUpdate { + _u.mutation.SetUnitConfig(v) + return _u +} + +// ClearUnitConfig clears the value of the "unit_config" field. +func (_u *ChargeUsageBasedUpdate) ClearUnitConfig() *ChargeUsageBasedUpdate { + _u.mutation.ClearUnitConfig() + return _u +} + // SetFeatureID sets the "feature_id" field. func (_u *ChargeUsageBasedUpdate) SetFeatureID(v string) *ChargeUsageBasedUpdate { _u.mutation.SetFeatureID(v) @@ -563,6 +575,11 @@ func (_u *ChargeUsageBasedUpdate) check() error { return &ValidationError{Name: "discounts", err: fmt.Errorf(`db: validator failed for field "ChargeUsageBased.discounts": %w`, err)} } } + if v, ok := _u.mutation.UnitConfig(); ok { + if err := v.Validate(); err != nil { + return &ValidationError{Name: "unit_config", err: fmt.Errorf(`db: validator failed for field "ChargeUsageBased.unit_config": %w`, err)} + } + } if v, ok := _u.mutation.FeatureID(); ok { if err := chargeusagebased.FeatureIDValidator(v); err != nil { return &ValidationError{Name: "feature_id", err: fmt.Errorf(`db: validator failed for field "ChargeUsageBased.feature_id": %w`, err)} @@ -681,6 +698,16 @@ func (_u *ChargeUsageBasedUpdate) sqlSave(ctx context.Context) (_node int, err e if _u.mutation.DiscountsCleared() { _spec.ClearField(chargeusagebased.FieldDiscounts, field.TypeString) } + if value, ok := _u.mutation.UnitConfig(); ok { + vv, err := chargeusagebased.ValueScanner.UnitConfig.Value(value) + if err != nil { + return 0, err + } + _spec.SetField(chargeusagebased.FieldUnitConfig, field.TypeString, vv) + } + if _u.mutation.UnitConfigCleared() { + _spec.ClearField(chargeusagebased.FieldUnitConfig, field.TypeString) + } if value, ok := _u.mutation.RatingEngine(); ok { _spec.SetField(chargeusagebased.FieldRatingEngine, field.TypeEnum, value) } @@ -1166,6 +1193,18 @@ func (_u *ChargeUsageBasedUpdateOne) ClearDiscounts() *ChargeUsageBasedUpdateOne return _u } +// SetUnitConfig sets the "unit_config" field. +func (_u *ChargeUsageBasedUpdateOne) SetUnitConfig(v *productcatalog.UnitConfig) *ChargeUsageBasedUpdateOne { + _u.mutation.SetUnitConfig(v) + return _u +} + +// ClearUnitConfig clears the value of the "unit_config" field. +func (_u *ChargeUsageBasedUpdateOne) ClearUnitConfig() *ChargeUsageBasedUpdateOne { + _u.mutation.ClearUnitConfig() + return _u +} + // SetFeatureID sets the "feature_id" field. func (_u *ChargeUsageBasedUpdateOne) SetFeatureID(v string) *ChargeUsageBasedUpdateOne { _u.mutation.SetFeatureID(v) @@ -1423,6 +1462,11 @@ func (_u *ChargeUsageBasedUpdateOne) check() error { return &ValidationError{Name: "discounts", err: fmt.Errorf(`db: validator failed for field "ChargeUsageBased.discounts": %w`, err)} } } + if v, ok := _u.mutation.UnitConfig(); ok { + if err := v.Validate(); err != nil { + return &ValidationError{Name: "unit_config", err: fmt.Errorf(`db: validator failed for field "ChargeUsageBased.unit_config": %w`, err)} + } + } if v, ok := _u.mutation.FeatureID(); ok { if err := chargeusagebased.FeatureIDValidator(v); err != nil { return &ValidationError{Name: "feature_id", err: fmt.Errorf(`db: validator failed for field "ChargeUsageBased.feature_id": %w`, err)} @@ -1558,6 +1602,16 @@ func (_u *ChargeUsageBasedUpdateOne) sqlSave(ctx context.Context) (_node *Charge if _u.mutation.DiscountsCleared() { _spec.ClearField(chargeusagebased.FieldDiscounts, field.TypeString) } + if value, ok := _u.mutation.UnitConfig(); ok { + vv, err := chargeusagebased.ValueScanner.UnitConfig.Value(value) + if err != nil { + return nil, err + } + _spec.SetField(chargeusagebased.FieldUnitConfig, field.TypeString, vv) + } + if _u.mutation.UnitConfigCleared() { + _spec.ClearField(chargeusagebased.FieldUnitConfig, field.TypeString) + } if value, ok := _u.mutation.RatingEngine(); ok { _spec.SetField(chargeusagebased.FieldRatingEngine, field.TypeEnum, value) } diff --git a/openmeter/ent/db/migrate/schema.go b/openmeter/ent/db/migrate/schema.go index 95a65b89c2..fd1745d9d4 100644 --- a/openmeter/ent/db/migrate/schema.go +++ b/openmeter/ent/db/migrate/schema.go @@ -91,6 +91,7 @@ var ( {Name: "tax_config", Type: field.TypeString, Nullable: true, SchemaType: map[string]string{"postgres": "jsonb"}}, {Name: "billing_cadence", Type: field.TypeString, Nullable: true}, {Name: "price", Type: field.TypeString, Nullable: true, SchemaType: map[string]string{"postgres": "jsonb"}}, + {Name: "unit_config", Type: field.TypeString, Nullable: true, SchemaType: map[string]string{"postgres": "jsonb"}}, {Name: "discounts", Type: field.TypeString, Nullable: true, SchemaType: map[string]string{"postgres": "jsonb"}}, {Name: "addon_id", Type: field.TypeString, SchemaType: map[string]string{"postgres": "char(26)"}}, {Name: "feature_id", Type: field.TypeString, Nullable: true, SchemaType: map[string]string{"postgres": "char(26)"}}, @@ -104,19 +105,19 @@ var ( ForeignKeys: []*schema.ForeignKey{ { Symbol: "addon_rate_cards_addons_ratecards", - Columns: []*schema.Column{AddonRateCardsColumns[17]}, + Columns: []*schema.Column{AddonRateCardsColumns[18]}, RefColumns: []*schema.Column{AddonsColumns[0]}, OnDelete: schema.Cascade, }, { Symbol: "addon_rate_cards_features_addon_ratecard", - Columns: []*schema.Column{AddonRateCardsColumns[18]}, + Columns: []*schema.Column{AddonRateCardsColumns[19]}, RefColumns: []*schema.Column{FeaturesColumns[0]}, OnDelete: schema.SetNull, }, { Symbol: "addon_rate_cards_tax_codes_addon_rate_cards", - Columns: []*schema.Column{AddonRateCardsColumns[19]}, + Columns: []*schema.Column{AddonRateCardsColumns[20]}, RefColumns: []*schema.Column{TaxCodesColumns[0]}, OnDelete: schema.SetNull, }, @@ -145,12 +146,12 @@ var ( { Name: "addonratecard_tax_code_id", Unique: false, - Columns: []*schema.Column{AddonRateCardsColumns[19]}, + Columns: []*schema.Column{AddonRateCardsColumns[20]}, }, { Name: "addonratecard_addon_id_key", Unique: true, - Columns: []*schema.Column{AddonRateCardsColumns[17], AddonRateCardsColumns[8]}, + Columns: []*schema.Column{AddonRateCardsColumns[18], AddonRateCardsColumns[8]}, Annotation: &entsql.IndexAnnotation{ Where: "deleted_at IS NULL", }, @@ -158,7 +159,7 @@ var ( { Name: "addonratecard_addon_id_feature_key", Unique: true, - Columns: []*schema.Column{AddonRateCardsColumns[17], AddonRateCardsColumns[11]}, + Columns: []*schema.Column{AddonRateCardsColumns[18], AddonRateCardsColumns[11]}, Annotation: &entsql.IndexAnnotation{ Where: "deleted_at IS NULL", }, @@ -1175,6 +1176,8 @@ var ( {Name: "pre_line_period_quantity", Type: field.TypeOther, Nullable: true, SchemaType: map[string]string{"postgres": "numeric"}}, {Name: "metered_pre_line_period_quantity", Type: field.TypeOther, Nullable: true, SchemaType: map[string]string{"postgres": "numeric"}}, {Name: "metered_quantity", Type: field.TypeOther, Nullable: true, SchemaType: map[string]string{"postgres": "numeric"}}, + {Name: "converted_quantity", Type: field.TypeOther, Nullable: true, SchemaType: map[string]string{"postgres": "numeric"}}, + {Name: "applied_unit_config", Type: field.TypeString, Nullable: true, SchemaType: map[string]string{"postgres": "jsonb"}}, } // BillingInvoiceUsageBasedLineConfigsTable holds the schema information for the "billing_invoice_usage_based_line_configs" table. BillingInvoiceUsageBasedLineConfigsTable = &schema.Table{ @@ -2438,6 +2441,7 @@ var ( {Name: "invoice_at", Type: field.TypeTime}, {Name: "settlement_mode", Type: field.TypeEnum, Enums: []string{"credit_then_invoice", "credit_only"}}, {Name: "discounts", Type: field.TypeString, Nullable: true, SchemaType: map[string]string{"postgres": "jsonb"}}, + {Name: "unit_config", Type: field.TypeString, Nullable: true, SchemaType: map[string]string{"postgres": "jsonb"}}, {Name: "feature_key", Type: field.TypeString}, {Name: "rating_engine", Type: field.TypeEnum, Enums: []string{"delta", "period_preserving"}}, {Name: "price", Type: field.TypeString, SchemaType: map[string]string{"postgres": "jsonb"}}, @@ -2458,43 +2462,43 @@ var ( ForeignKeys: []*schema.ForeignKey{ { Symbol: "charge_usage_based_charge_usage_based_runs_current_run", - Columns: []*schema.Column{ChargeUsageBasedColumns[28]}, + Columns: []*schema.Column{ChargeUsageBasedColumns[29]}, RefColumns: []*schema.Column{ChargeUsageBasedRunsColumns[0]}, OnDelete: schema.SetNull, }, { Symbol: "charge_usage_based_customers_charges_usage_based", - Columns: []*schema.Column{ChargeUsageBasedColumns[29]}, + Columns: []*schema.Column{ChargeUsageBasedColumns[30]}, RefColumns: []*schema.Column{CustomersColumns[0]}, OnDelete: schema.NoAction, }, { Symbol: "charge_usage_based_features_usage_based_charges", - Columns: []*schema.Column{ChargeUsageBasedColumns[30]}, + Columns: []*schema.Column{ChargeUsageBasedColumns[31]}, RefColumns: []*schema.Column{FeaturesColumns[0]}, OnDelete: schema.NoAction, }, { Symbol: "charge_usage_based_subscriptions_charges_usage_based", - Columns: []*schema.Column{ChargeUsageBasedColumns[31]}, + Columns: []*schema.Column{ChargeUsageBasedColumns[32]}, RefColumns: []*schema.Column{SubscriptionsColumns[0]}, OnDelete: schema.SetNull, }, { Symbol: "charge_usage_based_subscription_items_charges_usage_based", - Columns: []*schema.Column{ChargeUsageBasedColumns[32]}, + Columns: []*schema.Column{ChargeUsageBasedColumns[33]}, RefColumns: []*schema.Column{SubscriptionItemsColumns[0]}, OnDelete: schema.SetNull, }, { Symbol: "charge_usage_based_subscription_phases_charges_usage_based", - Columns: []*schema.Column{ChargeUsageBasedColumns[33]}, + Columns: []*schema.Column{ChargeUsageBasedColumns[34]}, RefColumns: []*schema.Column{SubscriptionPhasesColumns[0]}, OnDelete: schema.SetNull, }, { Symbol: "charge_usage_based_tax_codes_charge_usage_based", - Columns: []*schema.Column{ChargeUsageBasedColumns[34]}, + Columns: []*schema.Column{ChargeUsageBasedColumns[35]}, RefColumns: []*schema.Column{TaxCodesColumns[0]}, OnDelete: schema.SetNull, }, @@ -2503,7 +2507,7 @@ var ( { Name: "chargeusagebased_namespace_customer_id_unique_reference_id", Unique: true, - Columns: []*schema.Column{ChargeUsageBasedColumns[14], ChargeUsageBasedColumns[29], ChargeUsageBasedColumns[8]}, + Columns: []*schema.Column{ChargeUsageBasedColumns[14], ChargeUsageBasedColumns[30], ChargeUsageBasedColumns[8]}, Annotation: &entsql.IndexAnnotation{ Where: "unique_reference_id IS NOT NULL AND deleted_at IS NULL", }, @@ -2536,7 +2540,7 @@ var ( { Name: "chargeusagebased_tax_code_id", Unique: false, - Columns: []*schema.Column{ChargeUsageBasedColumns[34]}, + Columns: []*schema.Column{ChargeUsageBasedColumns[35]}, }, }, } @@ -4446,6 +4450,7 @@ var ( {Name: "tax_config", Type: field.TypeString, Nullable: true, SchemaType: map[string]string{"postgres": "jsonb"}}, {Name: "billing_cadence", Type: field.TypeString, Nullable: true}, {Name: "price", Type: field.TypeString, Nullable: true, SchemaType: map[string]string{"postgres": "jsonb"}}, + {Name: "unit_config", Type: field.TypeString, Nullable: true, SchemaType: map[string]string{"postgres": "jsonb"}}, {Name: "discounts", Type: field.TypeString, Nullable: true, SchemaType: map[string]string{"postgres": "jsonb"}}, {Name: "feature_id", Type: field.TypeString, Nullable: true, SchemaType: map[string]string{"postgres": "char(26)"}}, {Name: "phase_id", Type: field.TypeString, SchemaType: map[string]string{"postgres": "char(26)"}}, @@ -4459,19 +4464,19 @@ var ( ForeignKeys: []*schema.ForeignKey{ { Symbol: "plan_rate_cards_features_ratecard", - Columns: []*schema.Column{PlanRateCardsColumns[17]}, + Columns: []*schema.Column{PlanRateCardsColumns[18]}, RefColumns: []*schema.Column{FeaturesColumns[0]}, OnDelete: schema.SetNull, }, { Symbol: "plan_rate_cards_plan_phases_ratecards", - Columns: []*schema.Column{PlanRateCardsColumns[18]}, + Columns: []*schema.Column{PlanRateCardsColumns[19]}, RefColumns: []*schema.Column{PlanPhasesColumns[0]}, OnDelete: schema.Cascade, }, { Symbol: "plan_rate_cards_tax_codes_plan_rate_cards", - Columns: []*schema.Column{PlanRateCardsColumns[19]}, + Columns: []*schema.Column{PlanRateCardsColumns[20]}, RefColumns: []*schema.Column{TaxCodesColumns[0]}, OnDelete: schema.SetNull, }, @@ -4500,12 +4505,12 @@ var ( { Name: "planratecard_tax_code_id", Unique: false, - Columns: []*schema.Column{PlanRateCardsColumns[19]}, + Columns: []*schema.Column{PlanRateCardsColumns[20]}, }, { Name: "planratecard_phase_id_key", Unique: true, - Columns: []*schema.Column{PlanRateCardsColumns[18], PlanRateCardsColumns[8]}, + Columns: []*schema.Column{PlanRateCardsColumns[19], PlanRateCardsColumns[8]}, Annotation: &entsql.IndexAnnotation{ Where: "deleted_at IS NULL", }, @@ -4513,7 +4518,7 @@ var ( { Name: "planratecard_phase_id_feature_key", Unique: true, - Columns: []*schema.Column{PlanRateCardsColumns[18], PlanRateCardsColumns[11]}, + Columns: []*schema.Column{PlanRateCardsColumns[19], PlanRateCardsColumns[11]}, Annotation: &entsql.IndexAnnotation{ Where: "deleted_at IS NULL", }, diff --git a/openmeter/ent/db/mutation.go b/openmeter/ent/db/mutation.go index 99603d786d..204426a094 100644 --- a/openmeter/ent/db/mutation.go +++ b/openmeter/ent/db/mutation.go @@ -1672,6 +1672,7 @@ type AddonRateCardMutation struct { tax_config **productcatalog.TaxConfig billing_cadence *datetime.ISODurationString price **productcatalog.Price + unit_config **productcatalog.UnitConfig discounts **productcatalog.Discounts clearedFields map[string]struct{} addon *string @@ -2495,6 +2496,55 @@ func (m *AddonRateCardMutation) ResetPrice() { delete(m.clearedFields, addonratecard.FieldPrice) } +// SetUnitConfig sets the "unit_config" field. +func (m *AddonRateCardMutation) SetUnitConfig(pc *productcatalog.UnitConfig) { + m.unit_config = &pc +} + +// UnitConfig returns the value of the "unit_config" field in the mutation. +func (m *AddonRateCardMutation) UnitConfig() (r *productcatalog.UnitConfig, exists bool) { + v := m.unit_config + if v == nil { + return + } + return *v, true +} + +// OldUnitConfig returns the old "unit_config" field's value of the AddonRateCard entity. +// If the AddonRateCard object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *AddonRateCardMutation) OldUnitConfig(ctx context.Context) (v *productcatalog.UnitConfig, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldUnitConfig is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldUnitConfig requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldUnitConfig: %w", err) + } + return oldValue.UnitConfig, nil +} + +// ClearUnitConfig clears the value of the "unit_config" field. +func (m *AddonRateCardMutation) ClearUnitConfig() { + m.unit_config = nil + m.clearedFields[addonratecard.FieldUnitConfig] = struct{}{} +} + +// UnitConfigCleared returns if the "unit_config" field was cleared in this mutation. +func (m *AddonRateCardMutation) UnitConfigCleared() bool { + _, ok := m.clearedFields[addonratecard.FieldUnitConfig] + return ok +} + +// ResetUnitConfig resets all changes to the "unit_config" field. +func (m *AddonRateCardMutation) ResetUnitConfig() { + m.unit_config = nil + delete(m.clearedFields, addonratecard.FieldUnitConfig) +} + // SetDiscounts sets the "discounts" field. func (m *AddonRateCardMutation) SetDiscounts(pr *productcatalog.Discounts) { m.discounts = &pr @@ -2757,7 +2807,7 @@ func (m *AddonRateCardMutation) Type() string { // order to get all numeric fields that were incremented/decremented, call // AddedFields(). func (m *AddonRateCardMutation) Fields() []string { - fields := make([]string, 0, 19) + fields := make([]string, 0, 20) if m.namespace != nil { fields = append(fields, addonratecard.FieldNamespace) } @@ -2806,6 +2856,9 @@ func (m *AddonRateCardMutation) Fields() []string { if m.price != nil { fields = append(fields, addonratecard.FieldPrice) } + if m.unit_config != nil { + fields = append(fields, addonratecard.FieldUnitConfig) + } if m.discounts != nil { fields = append(fields, addonratecard.FieldDiscounts) } @@ -2855,6 +2908,8 @@ func (m *AddonRateCardMutation) Field(name string) (ent.Value, bool) { return m.BillingCadence() case addonratecard.FieldPrice: return m.Price() + case addonratecard.FieldUnitConfig: + return m.UnitConfig() case addonratecard.FieldDiscounts: return m.Discounts() case addonratecard.FieldAddonID: @@ -2902,6 +2957,8 @@ func (m *AddonRateCardMutation) OldField(ctx context.Context, name string) (ent. return m.OldBillingCadence(ctx) case addonratecard.FieldPrice: return m.OldPrice(ctx) + case addonratecard.FieldUnitConfig: + return m.OldUnitConfig(ctx) case addonratecard.FieldDiscounts: return m.OldDiscounts(ctx) case addonratecard.FieldAddonID: @@ -3029,6 +3086,13 @@ func (m *AddonRateCardMutation) SetField(name string, value ent.Value) error { } m.SetPrice(v) return nil + case addonratecard.FieldUnitConfig: + v, ok := value.(*productcatalog.UnitConfig) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetUnitConfig(v) + return nil case addonratecard.FieldDiscounts: v, ok := value.(*productcatalog.Discounts) if !ok { @@ -3110,6 +3174,9 @@ func (m *AddonRateCardMutation) ClearedFields() []string { if m.FieldCleared(addonratecard.FieldPrice) { fields = append(fields, addonratecard.FieldPrice) } + if m.FieldCleared(addonratecard.FieldUnitConfig) { + fields = append(fields, addonratecard.FieldUnitConfig) + } if m.FieldCleared(addonratecard.FieldDiscounts) { fields = append(fields, addonratecard.FieldDiscounts) } @@ -3160,6 +3227,9 @@ func (m *AddonRateCardMutation) ClearField(name string) error { case addonratecard.FieldPrice: m.ClearPrice() return nil + case addonratecard.FieldUnitConfig: + m.ClearUnitConfig() + return nil case addonratecard.FieldDiscounts: m.ClearDiscounts() return nil @@ -3222,6 +3292,9 @@ func (m *AddonRateCardMutation) ResetField(name string) error { case addonratecard.FieldPrice: m.ResetPrice() return nil + case addonratecard.FieldUnitConfig: + m.ResetUnitConfig() + return nil case addonratecard.FieldDiscounts: m.ResetDiscounts() return nil @@ -26068,6 +26141,8 @@ type BillingInvoiceUsageBasedLineConfigMutation struct { pre_line_period_quantity *alpacadecimal.Decimal metered_pre_line_period_quantity *alpacadecimal.Decimal metered_quantity *alpacadecimal.Decimal + converted_quantity *alpacadecimal.Decimal + applied_unit_config **productcatalog.UnitConfig clearedFields map[string]struct{} done bool oldValue func(context.Context) (*BillingInvoiceUsageBasedLineConfig, error) @@ -26482,6 +26557,104 @@ func (m *BillingInvoiceUsageBasedLineConfigMutation) ResetMeteredQuantity() { delete(m.clearedFields, billinginvoiceusagebasedlineconfig.FieldMeteredQuantity) } +// SetConvertedQuantity sets the "converted_quantity" field. +func (m *BillingInvoiceUsageBasedLineConfigMutation) SetConvertedQuantity(a alpacadecimal.Decimal) { + m.converted_quantity = &a +} + +// ConvertedQuantity returns the value of the "converted_quantity" field in the mutation. +func (m *BillingInvoiceUsageBasedLineConfigMutation) ConvertedQuantity() (r alpacadecimal.Decimal, exists bool) { + v := m.converted_quantity + if v == nil { + return + } + return *v, true +} + +// OldConvertedQuantity returns the old "converted_quantity" field's value of the BillingInvoiceUsageBasedLineConfig entity. +// If the BillingInvoiceUsageBasedLineConfig object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *BillingInvoiceUsageBasedLineConfigMutation) OldConvertedQuantity(ctx context.Context) (v *alpacadecimal.Decimal, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldConvertedQuantity is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldConvertedQuantity requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldConvertedQuantity: %w", err) + } + return oldValue.ConvertedQuantity, nil +} + +// ClearConvertedQuantity clears the value of the "converted_quantity" field. +func (m *BillingInvoiceUsageBasedLineConfigMutation) ClearConvertedQuantity() { + m.converted_quantity = nil + m.clearedFields[billinginvoiceusagebasedlineconfig.FieldConvertedQuantity] = struct{}{} +} + +// ConvertedQuantityCleared returns if the "converted_quantity" field was cleared in this mutation. +func (m *BillingInvoiceUsageBasedLineConfigMutation) ConvertedQuantityCleared() bool { + _, ok := m.clearedFields[billinginvoiceusagebasedlineconfig.FieldConvertedQuantity] + return ok +} + +// ResetConvertedQuantity resets all changes to the "converted_quantity" field. +func (m *BillingInvoiceUsageBasedLineConfigMutation) ResetConvertedQuantity() { + m.converted_quantity = nil + delete(m.clearedFields, billinginvoiceusagebasedlineconfig.FieldConvertedQuantity) +} + +// SetAppliedUnitConfig sets the "applied_unit_config" field. +func (m *BillingInvoiceUsageBasedLineConfigMutation) SetAppliedUnitConfig(pc *productcatalog.UnitConfig) { + m.applied_unit_config = &pc +} + +// AppliedUnitConfig returns the value of the "applied_unit_config" field in the mutation. +func (m *BillingInvoiceUsageBasedLineConfigMutation) AppliedUnitConfig() (r *productcatalog.UnitConfig, exists bool) { + v := m.applied_unit_config + if v == nil { + return + } + return *v, true +} + +// OldAppliedUnitConfig returns the old "applied_unit_config" field's value of the BillingInvoiceUsageBasedLineConfig entity. +// If the BillingInvoiceUsageBasedLineConfig object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *BillingInvoiceUsageBasedLineConfigMutation) OldAppliedUnitConfig(ctx context.Context) (v *productcatalog.UnitConfig, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldAppliedUnitConfig is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldAppliedUnitConfig requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldAppliedUnitConfig: %w", err) + } + return oldValue.AppliedUnitConfig, nil +} + +// ClearAppliedUnitConfig clears the value of the "applied_unit_config" field. +func (m *BillingInvoiceUsageBasedLineConfigMutation) ClearAppliedUnitConfig() { + m.applied_unit_config = nil + m.clearedFields[billinginvoiceusagebasedlineconfig.FieldAppliedUnitConfig] = struct{}{} +} + +// AppliedUnitConfigCleared returns if the "applied_unit_config" field was cleared in this mutation. +func (m *BillingInvoiceUsageBasedLineConfigMutation) AppliedUnitConfigCleared() bool { + _, ok := m.clearedFields[billinginvoiceusagebasedlineconfig.FieldAppliedUnitConfig] + return ok +} + +// ResetAppliedUnitConfig resets all changes to the "applied_unit_config" field. +func (m *BillingInvoiceUsageBasedLineConfigMutation) ResetAppliedUnitConfig() { + m.applied_unit_config = nil + delete(m.clearedFields, billinginvoiceusagebasedlineconfig.FieldAppliedUnitConfig) +} + // Where appends a list predicates to the BillingInvoiceUsageBasedLineConfigMutation builder. func (m *BillingInvoiceUsageBasedLineConfigMutation) Where(ps ...predicate.BillingInvoiceUsageBasedLineConfig) { m.predicates = append(m.predicates, ps...) @@ -26516,7 +26689,7 @@ func (m *BillingInvoiceUsageBasedLineConfigMutation) Type() string { // order to get all numeric fields that were incremented/decremented, call // AddedFields(). func (m *BillingInvoiceUsageBasedLineConfigMutation) Fields() []string { - fields := make([]string, 0, 7) + fields := make([]string, 0, 9) if m.namespace != nil { fields = append(fields, billinginvoiceusagebasedlineconfig.FieldNamespace) } @@ -26538,6 +26711,12 @@ func (m *BillingInvoiceUsageBasedLineConfigMutation) Fields() []string { if m.metered_quantity != nil { fields = append(fields, billinginvoiceusagebasedlineconfig.FieldMeteredQuantity) } + if m.converted_quantity != nil { + fields = append(fields, billinginvoiceusagebasedlineconfig.FieldConvertedQuantity) + } + if m.applied_unit_config != nil { + fields = append(fields, billinginvoiceusagebasedlineconfig.FieldAppliedUnitConfig) + } return fields } @@ -26560,6 +26739,10 @@ func (m *BillingInvoiceUsageBasedLineConfigMutation) Field(name string) (ent.Val return m.MeteredPreLinePeriodQuantity() case billinginvoiceusagebasedlineconfig.FieldMeteredQuantity: return m.MeteredQuantity() + case billinginvoiceusagebasedlineconfig.FieldConvertedQuantity: + return m.ConvertedQuantity() + case billinginvoiceusagebasedlineconfig.FieldAppliedUnitConfig: + return m.AppliedUnitConfig() } return nil, false } @@ -26583,6 +26766,10 @@ func (m *BillingInvoiceUsageBasedLineConfigMutation) OldField(ctx context.Contex return m.OldMeteredPreLinePeriodQuantity(ctx) case billinginvoiceusagebasedlineconfig.FieldMeteredQuantity: return m.OldMeteredQuantity(ctx) + case billinginvoiceusagebasedlineconfig.FieldConvertedQuantity: + return m.OldConvertedQuantity(ctx) + case billinginvoiceusagebasedlineconfig.FieldAppliedUnitConfig: + return m.OldAppliedUnitConfig(ctx) } return nil, fmt.Errorf("unknown BillingInvoiceUsageBasedLineConfig field %s", name) } @@ -26641,6 +26828,20 @@ func (m *BillingInvoiceUsageBasedLineConfigMutation) SetField(name string, value } m.SetMeteredQuantity(v) return nil + case billinginvoiceusagebasedlineconfig.FieldConvertedQuantity: + v, ok := value.(alpacadecimal.Decimal) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetConvertedQuantity(v) + return nil + case billinginvoiceusagebasedlineconfig.FieldAppliedUnitConfig: + v, ok := value.(*productcatalog.UnitConfig) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetAppliedUnitConfig(v) + return nil } return fmt.Errorf("unknown BillingInvoiceUsageBasedLineConfig field %s", name) } @@ -26683,6 +26884,12 @@ func (m *BillingInvoiceUsageBasedLineConfigMutation) ClearedFields() []string { if m.FieldCleared(billinginvoiceusagebasedlineconfig.FieldMeteredQuantity) { fields = append(fields, billinginvoiceusagebasedlineconfig.FieldMeteredQuantity) } + if m.FieldCleared(billinginvoiceusagebasedlineconfig.FieldConvertedQuantity) { + fields = append(fields, billinginvoiceusagebasedlineconfig.FieldConvertedQuantity) + } + if m.FieldCleared(billinginvoiceusagebasedlineconfig.FieldAppliedUnitConfig) { + fields = append(fields, billinginvoiceusagebasedlineconfig.FieldAppliedUnitConfig) + } return fields } @@ -26709,6 +26916,12 @@ func (m *BillingInvoiceUsageBasedLineConfigMutation) ClearField(name string) err case billinginvoiceusagebasedlineconfig.FieldMeteredQuantity: m.ClearMeteredQuantity() return nil + case billinginvoiceusagebasedlineconfig.FieldConvertedQuantity: + m.ClearConvertedQuantity() + return nil + case billinginvoiceusagebasedlineconfig.FieldAppliedUnitConfig: + m.ClearAppliedUnitConfig() + return nil } return fmt.Errorf("unknown BillingInvoiceUsageBasedLineConfig nullable field %s", name) } @@ -26738,6 +26951,12 @@ func (m *BillingInvoiceUsageBasedLineConfigMutation) ResetField(name string) err case billinginvoiceusagebasedlineconfig.FieldMeteredQuantity: m.ResetMeteredQuantity() return nil + case billinginvoiceusagebasedlineconfig.FieldConvertedQuantity: + m.ResetConvertedQuantity() + return nil + case billinginvoiceusagebasedlineconfig.FieldAppliedUnitConfig: + m.ResetAppliedUnitConfig() + return nil } return fmt.Errorf("unknown BillingInvoiceUsageBasedLineConfig field %s", name) } @@ -54342,6 +54561,7 @@ type ChargeUsageBasedMutation struct { invoice_at *time.Time settlement_mode *productcatalog.SettlementMode discounts **productcatalog.Discounts + unit_config **productcatalog.UnitConfig feature_key *string rating_engine *usagebased.RatingEngine price **productcatalog.Price @@ -55642,6 +55862,55 @@ func (m *ChargeUsageBasedMutation) ResetDiscounts() { delete(m.clearedFields, chargeusagebased.FieldDiscounts) } +// SetUnitConfig sets the "unit_config" field. +func (m *ChargeUsageBasedMutation) SetUnitConfig(pc *productcatalog.UnitConfig) { + m.unit_config = &pc +} + +// UnitConfig returns the value of the "unit_config" field in the mutation. +func (m *ChargeUsageBasedMutation) UnitConfig() (r *productcatalog.UnitConfig, exists bool) { + v := m.unit_config + if v == nil { + return + } + return *v, true +} + +// OldUnitConfig returns the old "unit_config" field's value of the ChargeUsageBased entity. +// If the ChargeUsageBased object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *ChargeUsageBasedMutation) OldUnitConfig(ctx context.Context) (v *productcatalog.UnitConfig, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldUnitConfig is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldUnitConfig requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldUnitConfig: %w", err) + } + return oldValue.UnitConfig, nil +} + +// ClearUnitConfig clears the value of the "unit_config" field. +func (m *ChargeUsageBasedMutation) ClearUnitConfig() { + m.unit_config = nil + m.clearedFields[chargeusagebased.FieldUnitConfig] = struct{}{} +} + +// UnitConfigCleared returns if the "unit_config" field was cleared in this mutation. +func (m *ChargeUsageBasedMutation) UnitConfigCleared() bool { + _, ok := m.clearedFields[chargeusagebased.FieldUnitConfig] + return ok +} + +// ResetUnitConfig resets all changes to the "unit_config" field. +func (m *ChargeUsageBasedMutation) ResetUnitConfig() { + m.unit_config = nil + delete(m.clearedFields, chargeusagebased.FieldUnitConfig) +} + // SetFeatureKey sets the "feature_key" field. func (m *ChargeUsageBasedMutation) SetFeatureKey(s string) { m.feature_key = &s @@ -56254,7 +56523,7 @@ func (m *ChargeUsageBasedMutation) Type() string { // order to get all numeric fields that were incremented/decremented, call // AddedFields(). func (m *ChargeUsageBasedMutation) Fields() []string { - fields := make([]string, 0, 34) + fields := make([]string, 0, 35) if m.customer != nil { fields = append(fields, chargeusagebased.FieldCustomerID) } @@ -56339,6 +56608,9 @@ func (m *ChargeUsageBasedMutation) Fields() []string { if m.discounts != nil { fields = append(fields, chargeusagebased.FieldDiscounts) } + if m.unit_config != nil { + fields = append(fields, chargeusagebased.FieldUnitConfig) + } if m.feature_key != nil { fields = append(fields, chargeusagebased.FieldFeatureKey) } @@ -56421,6 +56693,8 @@ func (m *ChargeUsageBasedMutation) Field(name string) (ent.Value, bool) { return m.SettlementMode() case chargeusagebased.FieldDiscounts: return m.Discounts() + case chargeusagebased.FieldUnitConfig: + return m.UnitConfig() case chargeusagebased.FieldFeatureKey: return m.FeatureKey() case chargeusagebased.FieldFeatureID: @@ -56498,6 +56772,8 @@ func (m *ChargeUsageBasedMutation) OldField(ctx context.Context, name string) (e return m.OldSettlementMode(ctx) case chargeusagebased.FieldDiscounts: return m.OldDiscounts(ctx) + case chargeusagebased.FieldUnitConfig: + return m.OldUnitConfig(ctx) case chargeusagebased.FieldFeatureKey: return m.OldFeatureKey(ctx) case chargeusagebased.FieldFeatureID: @@ -56715,6 +56991,13 @@ func (m *ChargeUsageBasedMutation) SetField(name string, value ent.Value) error } m.SetDiscounts(v) return nil + case chargeusagebased.FieldUnitConfig: + v, ok := value.(*productcatalog.UnitConfig) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetUnitConfig(v) + return nil case chargeusagebased.FieldFeatureKey: v, ok := value.(string) if !ok { @@ -56823,6 +57106,9 @@ func (m *ChargeUsageBasedMutation) ClearedFields() []string { if m.FieldCleared(chargeusagebased.FieldDiscounts) { fields = append(fields, chargeusagebased.FieldDiscounts) } + if m.FieldCleared(chargeusagebased.FieldUnitConfig) { + fields = append(fields, chargeusagebased.FieldUnitConfig) + } if m.FieldCleared(chargeusagebased.FieldCurrentRealizationRunID) { fields = append(fields, chargeusagebased.FieldCurrentRealizationRunID) } @@ -56876,6 +57162,9 @@ func (m *ChargeUsageBasedMutation) ClearField(name string) error { case chargeusagebased.FieldDiscounts: m.ClearDiscounts() return nil + case chargeusagebased.FieldUnitConfig: + m.ClearUnitConfig() + return nil case chargeusagebased.FieldCurrentRealizationRunID: m.ClearCurrentRealizationRunID() return nil @@ -56971,6 +57260,9 @@ func (m *ChargeUsageBasedMutation) ResetField(name string) error { case chargeusagebased.FieldDiscounts: m.ResetDiscounts() return nil + case chargeusagebased.FieldUnitConfig: + m.ResetUnitConfig() + return nil case chargeusagebased.FieldFeatureKey: m.ResetFeatureKey() return nil @@ -94996,6 +95288,7 @@ type PlanRateCardMutation struct { tax_config **productcatalog.TaxConfig billing_cadence *datetime.ISODurationString price **productcatalog.Price + unit_config **productcatalog.UnitConfig discounts **productcatalog.Discounts clearedFields map[string]struct{} phase *string @@ -95819,6 +96112,55 @@ func (m *PlanRateCardMutation) ResetPrice() { delete(m.clearedFields, planratecard.FieldPrice) } +// SetUnitConfig sets the "unit_config" field. +func (m *PlanRateCardMutation) SetUnitConfig(pc *productcatalog.UnitConfig) { + m.unit_config = &pc +} + +// UnitConfig returns the value of the "unit_config" field in the mutation. +func (m *PlanRateCardMutation) UnitConfig() (r *productcatalog.UnitConfig, exists bool) { + v := m.unit_config + if v == nil { + return + } + return *v, true +} + +// OldUnitConfig returns the old "unit_config" field's value of the PlanRateCard entity. +// If the PlanRateCard object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *PlanRateCardMutation) OldUnitConfig(ctx context.Context) (v *productcatalog.UnitConfig, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldUnitConfig is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldUnitConfig requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldUnitConfig: %w", err) + } + return oldValue.UnitConfig, nil +} + +// ClearUnitConfig clears the value of the "unit_config" field. +func (m *PlanRateCardMutation) ClearUnitConfig() { + m.unit_config = nil + m.clearedFields[planratecard.FieldUnitConfig] = struct{}{} +} + +// UnitConfigCleared returns if the "unit_config" field was cleared in this mutation. +func (m *PlanRateCardMutation) UnitConfigCleared() bool { + _, ok := m.clearedFields[planratecard.FieldUnitConfig] + return ok +} + +// ResetUnitConfig resets all changes to the "unit_config" field. +func (m *PlanRateCardMutation) ResetUnitConfig() { + m.unit_config = nil + delete(m.clearedFields, planratecard.FieldUnitConfig) +} + // SetDiscounts sets the "discounts" field. func (m *PlanRateCardMutation) SetDiscounts(pr *productcatalog.Discounts) { m.discounts = &pr @@ -96081,7 +96423,7 @@ func (m *PlanRateCardMutation) Type() string { // order to get all numeric fields that were incremented/decremented, call // AddedFields(). func (m *PlanRateCardMutation) Fields() []string { - fields := make([]string, 0, 19) + fields := make([]string, 0, 20) if m.namespace != nil { fields = append(fields, planratecard.FieldNamespace) } @@ -96130,6 +96472,9 @@ func (m *PlanRateCardMutation) Fields() []string { if m.price != nil { fields = append(fields, planratecard.FieldPrice) } + if m.unit_config != nil { + fields = append(fields, planratecard.FieldUnitConfig) + } if m.discounts != nil { fields = append(fields, planratecard.FieldDiscounts) } @@ -96179,6 +96524,8 @@ func (m *PlanRateCardMutation) Field(name string) (ent.Value, bool) { return m.BillingCadence() case planratecard.FieldPrice: return m.Price() + case planratecard.FieldUnitConfig: + return m.UnitConfig() case planratecard.FieldDiscounts: return m.Discounts() case planratecard.FieldPhaseID: @@ -96226,6 +96573,8 @@ func (m *PlanRateCardMutation) OldField(ctx context.Context, name string) (ent.V return m.OldBillingCadence(ctx) case planratecard.FieldPrice: return m.OldPrice(ctx) + case planratecard.FieldUnitConfig: + return m.OldUnitConfig(ctx) case planratecard.FieldDiscounts: return m.OldDiscounts(ctx) case planratecard.FieldPhaseID: @@ -96353,6 +96702,13 @@ func (m *PlanRateCardMutation) SetField(name string, value ent.Value) error { } m.SetPrice(v) return nil + case planratecard.FieldUnitConfig: + v, ok := value.(*productcatalog.UnitConfig) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetUnitConfig(v) + return nil case planratecard.FieldDiscounts: v, ok := value.(*productcatalog.Discounts) if !ok { @@ -96434,6 +96790,9 @@ func (m *PlanRateCardMutation) ClearedFields() []string { if m.FieldCleared(planratecard.FieldPrice) { fields = append(fields, planratecard.FieldPrice) } + if m.FieldCleared(planratecard.FieldUnitConfig) { + fields = append(fields, planratecard.FieldUnitConfig) + } if m.FieldCleared(planratecard.FieldDiscounts) { fields = append(fields, planratecard.FieldDiscounts) } @@ -96484,6 +96843,9 @@ func (m *PlanRateCardMutation) ClearField(name string) error { case planratecard.FieldPrice: m.ClearPrice() return nil + case planratecard.FieldUnitConfig: + m.ClearUnitConfig() + return nil case planratecard.FieldDiscounts: m.ClearDiscounts() return nil @@ -96546,6 +96908,9 @@ func (m *PlanRateCardMutation) ResetField(name string) error { case planratecard.FieldPrice: m.ResetPrice() return nil + case planratecard.FieldUnitConfig: + m.ResetUnitConfig() + return nil case planratecard.FieldDiscounts: m.ResetDiscounts() return nil diff --git a/openmeter/ent/db/planratecard.go b/openmeter/ent/db/planratecard.go index 70e01b07d7..4ffb4dc6f8 100644 --- a/openmeter/ent/db/planratecard.go +++ b/openmeter/ent/db/planratecard.go @@ -55,6 +55,8 @@ type PlanRateCard struct { BillingCadence *datetime.ISODurationString `json:"billing_cadence,omitempty"` // Price holds the value of the "price" field. Price *productcatalog.Price `json:"price,omitempty"` + // UnitConfig holds the value of the "unit_config" field. + UnitConfig *productcatalog.UnitConfig `json:"unit_config,omitempty"` // Discounts holds the value of the "discounts" field. Discounts *productcatalog.Discounts `json:"discounts,omitempty"` // The phase identifier the ratecard is assigned to. @@ -130,6 +132,8 @@ func (*PlanRateCard) scanValues(columns []string) ([]any, error) { values[i] = planratecard.ValueScanner.TaxConfig.ScanValue() case planratecard.FieldPrice: values[i] = planratecard.ValueScanner.Price.ScanValue() + case planratecard.FieldUnitConfig: + values[i] = planratecard.ValueScanner.UnitConfig.ScanValue() case planratecard.FieldDiscounts: values[i] = planratecard.ValueScanner.Discounts.ScanValue() default: @@ -257,6 +261,12 @@ func (_m *PlanRateCard) assignValues(columns []string, values []any) error { } else { _m.Price = value } + case planratecard.FieldUnitConfig: + if value, err := planratecard.ValueScanner.UnitConfig.FromValue(values[i]); err != nil { + return err + } else { + _m.UnitConfig = value + } case planratecard.FieldDiscounts: if value, err := planratecard.ValueScanner.Discounts.FromValue(values[i]); err != nil { return err @@ -393,6 +403,11 @@ func (_m *PlanRateCard) String() string { builder.WriteString(fmt.Sprintf("%v", *v)) } builder.WriteString(", ") + if v := _m.UnitConfig; v != nil { + builder.WriteString("unit_config=") + builder.WriteString(fmt.Sprintf("%v", *v)) + } + builder.WriteString(", ") if v := _m.Discounts; v != nil { builder.WriteString("discounts=") builder.WriteString(fmt.Sprintf("%v", *v)) diff --git a/openmeter/ent/db/planratecard/planratecard.go b/openmeter/ent/db/planratecard/planratecard.go index 4365d033f3..66c3a5abd7 100644 --- a/openmeter/ent/db/planratecard/planratecard.go +++ b/openmeter/ent/db/planratecard/planratecard.go @@ -49,6 +49,8 @@ const ( FieldBillingCadence = "billing_cadence" // FieldPrice holds the string denoting the price field in the database. FieldPrice = "price" + // FieldUnitConfig holds the string denoting the unit_config field in the database. + FieldUnitConfig = "unit_config" // FieldDiscounts holds the string denoting the discounts field in the database. FieldDiscounts = "discounts" // FieldPhaseID holds the string denoting the phase_id field in the database. @@ -105,6 +107,7 @@ var Columns = []string{ FieldTaxConfig, FieldBillingCadence, FieldPrice, + FieldUnitConfig, FieldDiscounts, FieldPhaseID, FieldFeatureID, @@ -140,6 +143,7 @@ var ( EntitlementTemplate field.TypeValueScanner[*productcatalog.EntitlementTemplate] TaxConfig field.TypeValueScanner[*productcatalog.TaxConfig] Price field.TypeValueScanner[*productcatalog.Price] + UnitConfig field.TypeValueScanner[*productcatalog.UnitConfig] Discounts field.TypeValueScanner[*productcatalog.Discounts] } ) @@ -247,6 +251,11 @@ func ByPrice(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldPrice, opts...).ToFunc() } +// ByUnitConfig orders the results by the unit_config field. +func ByUnitConfig(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldUnitConfig, opts...).ToFunc() +} + // ByDiscounts orders the results by the discounts field. func ByDiscounts(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldDiscounts, opts...).ToFunc() diff --git a/openmeter/ent/db/planratecard/where.go b/openmeter/ent/db/planratecard/where.go index f7a308d4a5..9ba20b3650 100644 --- a/openmeter/ent/db/planratecard/where.go +++ b/openmeter/ent/db/planratecard/where.go @@ -882,6 +882,16 @@ func PriceNotNil() predicate.PlanRateCard { return predicate.PlanRateCard(sql.FieldNotNull(FieldPrice)) } +// UnitConfigIsNil applies the IsNil predicate on the "unit_config" field. +func UnitConfigIsNil() predicate.PlanRateCard { + return predicate.PlanRateCard(sql.FieldIsNull(FieldUnitConfig)) +} + +// UnitConfigNotNil applies the NotNil predicate on the "unit_config" field. +func UnitConfigNotNil() predicate.PlanRateCard { + return predicate.PlanRateCard(sql.FieldNotNull(FieldUnitConfig)) +} + // DiscountsIsNil applies the IsNil predicate on the "discounts" field. func DiscountsIsNil() predicate.PlanRateCard { return predicate.PlanRateCard(sql.FieldIsNull(FieldDiscounts)) diff --git a/openmeter/ent/db/planratecard_create.go b/openmeter/ent/db/planratecard_create.go index cac13b620a..0700dc2dce 100644 --- a/openmeter/ent/db/planratecard_create.go +++ b/openmeter/ent/db/planratecard_create.go @@ -188,6 +188,12 @@ func (_c *PlanRateCardCreate) SetPrice(v *productcatalog.Price) *PlanRateCardCre return _c } +// SetUnitConfig sets the "unit_config" field. +func (_c *PlanRateCardCreate) SetUnitConfig(v *productcatalog.UnitConfig) *PlanRateCardCreate { + _c.mutation.SetUnitConfig(v) + return _c +} + // SetDiscounts sets the "discounts" field. func (_c *PlanRateCardCreate) SetDiscounts(v *productcatalog.Discounts) *PlanRateCardCreate { _c.mutation.SetDiscounts(v) @@ -361,6 +367,11 @@ func (_c *PlanRateCardCreate) check() error { return &ValidationError{Name: "price", err: fmt.Errorf(`db: validator failed for field "PlanRateCard.price": %w`, err)} } } + if v, ok := _c.mutation.UnitConfig(); ok { + if err := v.Validate(); err != nil { + return &ValidationError{Name: "unit_config", err: fmt.Errorf(`db: validator failed for field "PlanRateCard.unit_config": %w`, err)} + } + } if v, ok := _c.mutation.Discounts(); ok { if err := v.Validate(); err != nil { return &ValidationError{Name: "discounts", err: fmt.Errorf(`db: validator failed for field "PlanRateCard.discounts": %w`, err)} @@ -488,6 +499,14 @@ func (_c *PlanRateCardCreate) createSpec() (*PlanRateCard, *sqlgraph.CreateSpec, _spec.SetField(planratecard.FieldPrice, field.TypeString, vv) _node.Price = value } + if value, ok := _c.mutation.UnitConfig(); ok { + vv, err := planratecard.ValueScanner.UnitConfig.Value(value) + if err != nil { + return nil, nil, err + } + _spec.SetField(planratecard.FieldUnitConfig, field.TypeString, vv) + _node.UnitConfig = value + } if value, ok := _c.mutation.Discounts(); ok { vv, err := planratecard.ValueScanner.Discounts.Value(value) if err != nil { @@ -803,6 +822,24 @@ func (u *PlanRateCardUpsert) ClearPrice() *PlanRateCardUpsert { return u } +// SetUnitConfig sets the "unit_config" field. +func (u *PlanRateCardUpsert) SetUnitConfig(v *productcatalog.UnitConfig) *PlanRateCardUpsert { + u.Set(planratecard.FieldUnitConfig, v) + return u +} + +// UpdateUnitConfig sets the "unit_config" field to the value that was provided on create. +func (u *PlanRateCardUpsert) UpdateUnitConfig() *PlanRateCardUpsert { + u.SetExcluded(planratecard.FieldUnitConfig) + return u +} + +// ClearUnitConfig clears the value of the "unit_config" field. +func (u *PlanRateCardUpsert) ClearUnitConfig() *PlanRateCardUpsert { + u.SetNull(planratecard.FieldUnitConfig) + return u +} + // SetDiscounts sets the "discounts" field. func (u *PlanRateCardUpsert) SetDiscounts(v *productcatalog.Discounts) *PlanRateCardUpsert { u.Set(planratecard.FieldDiscounts, v) @@ -1149,6 +1186,27 @@ func (u *PlanRateCardUpsertOne) ClearPrice() *PlanRateCardUpsertOne { }) } +// SetUnitConfig sets the "unit_config" field. +func (u *PlanRateCardUpsertOne) SetUnitConfig(v *productcatalog.UnitConfig) *PlanRateCardUpsertOne { + return u.Update(func(s *PlanRateCardUpsert) { + s.SetUnitConfig(v) + }) +} + +// UpdateUnitConfig sets the "unit_config" field to the value that was provided on create. +func (u *PlanRateCardUpsertOne) UpdateUnitConfig() *PlanRateCardUpsertOne { + return u.Update(func(s *PlanRateCardUpsert) { + s.UpdateUnitConfig() + }) +} + +// ClearUnitConfig clears the value of the "unit_config" field. +func (u *PlanRateCardUpsertOne) ClearUnitConfig() *PlanRateCardUpsertOne { + return u.Update(func(s *PlanRateCardUpsert) { + s.ClearUnitConfig() + }) +} + // SetDiscounts sets the "discounts" field. func (u *PlanRateCardUpsertOne) SetDiscounts(v *productcatalog.Discounts) *PlanRateCardUpsertOne { return u.Update(func(s *PlanRateCardUpsert) { @@ -1673,6 +1731,27 @@ func (u *PlanRateCardUpsertBulk) ClearPrice() *PlanRateCardUpsertBulk { }) } +// SetUnitConfig sets the "unit_config" field. +func (u *PlanRateCardUpsertBulk) SetUnitConfig(v *productcatalog.UnitConfig) *PlanRateCardUpsertBulk { + return u.Update(func(s *PlanRateCardUpsert) { + s.SetUnitConfig(v) + }) +} + +// UpdateUnitConfig sets the "unit_config" field to the value that was provided on create. +func (u *PlanRateCardUpsertBulk) UpdateUnitConfig() *PlanRateCardUpsertBulk { + return u.Update(func(s *PlanRateCardUpsert) { + s.UpdateUnitConfig() + }) +} + +// ClearUnitConfig clears the value of the "unit_config" field. +func (u *PlanRateCardUpsertBulk) ClearUnitConfig() *PlanRateCardUpsertBulk { + return u.Update(func(s *PlanRateCardUpsert) { + s.ClearUnitConfig() + }) +} + // SetDiscounts sets the "discounts" field. func (u *PlanRateCardUpsertBulk) SetDiscounts(v *productcatalog.Discounts) *PlanRateCardUpsertBulk { return u.Update(func(s *PlanRateCardUpsert) { diff --git a/openmeter/ent/db/planratecard_update.go b/openmeter/ent/db/planratecard_update.go index 98ead635c0..f4f4aefd97 100644 --- a/openmeter/ent/db/planratecard_update.go +++ b/openmeter/ent/db/planratecard_update.go @@ -221,6 +221,18 @@ func (_u *PlanRateCardUpdate) ClearPrice() *PlanRateCardUpdate { return _u } +// SetUnitConfig sets the "unit_config" field. +func (_u *PlanRateCardUpdate) SetUnitConfig(v *productcatalog.UnitConfig) *PlanRateCardUpdate { + _u.mutation.SetUnitConfig(v) + return _u +} + +// ClearUnitConfig clears the value of the "unit_config" field. +func (_u *PlanRateCardUpdate) ClearUnitConfig() *PlanRateCardUpdate { + _u.mutation.ClearUnitConfig() + return _u +} + // SetDiscounts sets the "discounts" field. func (_u *PlanRateCardUpdate) SetDiscounts(v *productcatalog.Discounts) *PlanRateCardUpdate { _u.mutation.SetDiscounts(v) @@ -377,6 +389,11 @@ func (_u *PlanRateCardUpdate) check() error { return &ValidationError{Name: "price", err: fmt.Errorf(`db: validator failed for field "PlanRateCard.price": %w`, err)} } } + if v, ok := _u.mutation.UnitConfig(); ok { + if err := v.Validate(); err != nil { + return &ValidationError{Name: "unit_config", err: fmt.Errorf(`db: validator failed for field "PlanRateCard.unit_config": %w`, err)} + } + } if v, ok := _u.mutation.Discounts(); ok { if err := v.Validate(); err != nil { return &ValidationError{Name: "discounts", err: fmt.Errorf(`db: validator failed for field "PlanRateCard.discounts": %w`, err)} @@ -477,6 +494,16 @@ func (_u *PlanRateCardUpdate) sqlSave(ctx context.Context) (_node int, err error if _u.mutation.PriceCleared() { _spec.ClearField(planratecard.FieldPrice, field.TypeString) } + if value, ok := _u.mutation.UnitConfig(); ok { + vv, err := planratecard.ValueScanner.UnitConfig.Value(value) + if err != nil { + return 0, err + } + _spec.SetField(planratecard.FieldUnitConfig, field.TypeString, vv) + } + if _u.mutation.UnitConfigCleared() { + _spec.ClearField(planratecard.FieldUnitConfig, field.TypeString) + } if value, ok := _u.mutation.Discounts(); ok { vv, err := planratecard.ValueScanner.Discounts.Value(value) if err != nil { @@ -782,6 +809,18 @@ func (_u *PlanRateCardUpdateOne) ClearPrice() *PlanRateCardUpdateOne { return _u } +// SetUnitConfig sets the "unit_config" field. +func (_u *PlanRateCardUpdateOne) SetUnitConfig(v *productcatalog.UnitConfig) *PlanRateCardUpdateOne { + _u.mutation.SetUnitConfig(v) + return _u +} + +// ClearUnitConfig clears the value of the "unit_config" field. +func (_u *PlanRateCardUpdateOne) ClearUnitConfig() *PlanRateCardUpdateOne { + _u.mutation.ClearUnitConfig() + return _u +} + // SetDiscounts sets the "discounts" field. func (_u *PlanRateCardUpdateOne) SetDiscounts(v *productcatalog.Discounts) *PlanRateCardUpdateOne { _u.mutation.SetDiscounts(v) @@ -951,6 +990,11 @@ func (_u *PlanRateCardUpdateOne) check() error { return &ValidationError{Name: "price", err: fmt.Errorf(`db: validator failed for field "PlanRateCard.price": %w`, err)} } } + if v, ok := _u.mutation.UnitConfig(); ok { + if err := v.Validate(); err != nil { + return &ValidationError{Name: "unit_config", err: fmt.Errorf(`db: validator failed for field "PlanRateCard.unit_config": %w`, err)} + } + } if v, ok := _u.mutation.Discounts(); ok { if err := v.Validate(); err != nil { return &ValidationError{Name: "discounts", err: fmt.Errorf(`db: validator failed for field "PlanRateCard.discounts": %w`, err)} @@ -1068,6 +1112,16 @@ func (_u *PlanRateCardUpdateOne) sqlSave(ctx context.Context) (_node *PlanRateCa if _u.mutation.PriceCleared() { _spec.ClearField(planratecard.FieldPrice, field.TypeString) } + if value, ok := _u.mutation.UnitConfig(); ok { + vv, err := planratecard.ValueScanner.UnitConfig.Value(value) + if err != nil { + return nil, err + } + _spec.SetField(planratecard.FieldUnitConfig, field.TypeString, vv) + } + if _u.mutation.UnitConfigCleared() { + _spec.ClearField(planratecard.FieldUnitConfig, field.TypeString) + } if value, ok := _u.mutation.Discounts(); ok { vv, err := planratecard.ValueScanner.Discounts.Value(value) if err != nil { diff --git a/openmeter/ent/db/runtime.go b/openmeter/ent/db/runtime.go index 9d5bc24cc6..2c008db5f9 100644 --- a/openmeter/ent/db/runtime.go +++ b/openmeter/ent/db/runtime.go @@ -174,11 +174,14 @@ func init() { // addonratecardDescPrice is the schema descriptor for price field. addonratecardDescPrice := addonratecardFields[5].Descriptor() addonratecard.ValueScanner.Price = addonratecardDescPrice.ValueScanner.(field.TypeValueScanner[*productcatalog.Price]) + // addonratecardDescUnitConfig is the schema descriptor for unit_config field. + addonratecardDescUnitConfig := addonratecardFields[6].Descriptor() + addonratecard.ValueScanner.UnitConfig = addonratecardDescUnitConfig.ValueScanner.(field.TypeValueScanner[*productcatalog.UnitConfig]) // addonratecardDescDiscounts is the schema descriptor for discounts field. - addonratecardDescDiscounts := addonratecardFields[6].Descriptor() + addonratecardDescDiscounts := addonratecardFields[7].Descriptor() addonratecard.ValueScanner.Discounts = addonratecardDescDiscounts.ValueScanner.(field.TypeValueScanner[*productcatalog.Discounts]) // addonratecardDescAddonID is the schema descriptor for addon_id field. - addonratecardDescAddonID := addonratecardFields[7].Descriptor() + addonratecardDescAddonID := addonratecardFields[8].Descriptor() // addonratecard.AddonIDValidator is a validator for the "addon_id" field. It is called by the builders before save. addonratecard.AddonIDValidator = addonratecardDescAddonID.Validators[0].(func(string) error) // addonratecardDescID is the schema descriptor for id field. @@ -690,6 +693,9 @@ func init() { // billinginvoiceusagebasedlineconfigDescPrice is the schema descriptor for price field. billinginvoiceusagebasedlineconfigDescPrice := billinginvoiceusagebasedlineconfigFields[2].Descriptor() billinginvoiceusagebasedlineconfig.ValueScanner.Price = billinginvoiceusagebasedlineconfigDescPrice.ValueScanner.(field.TypeValueScanner[*productcatalog.Price]) + // billinginvoiceusagebasedlineconfigDescAppliedUnitConfig is the schema descriptor for applied_unit_config field. + billinginvoiceusagebasedlineconfigDescAppliedUnitConfig := billinginvoiceusagebasedlineconfigFields[7].Descriptor() + billinginvoiceusagebasedlineconfig.ValueScanner.AppliedUnitConfig = billinginvoiceusagebasedlineconfigDescAppliedUnitConfig.ValueScanner.(field.TypeValueScanner[*productcatalog.UnitConfig]) // billinginvoiceusagebasedlineconfigDescID is the schema descriptor for id field. billinginvoiceusagebasedlineconfigDescID := billinginvoiceusagebasedlineconfigMixinFields1[0].Descriptor() // billinginvoiceusagebasedlineconfig.DefaultID holds the default value on creation for the id field. @@ -1298,16 +1304,19 @@ func init() { // chargeusagebasedDescDiscounts is the schema descriptor for discounts field. chargeusagebasedDescDiscounts := chargeusagebasedFields[2].Descriptor() chargeusagebased.ValueScanner.Discounts = chargeusagebasedDescDiscounts.ValueScanner.(field.TypeValueScanner[*productcatalog.Discounts]) + // chargeusagebasedDescUnitConfig is the schema descriptor for unit_config field. + chargeusagebasedDescUnitConfig := chargeusagebasedFields[3].Descriptor() + chargeusagebased.ValueScanner.UnitConfig = chargeusagebasedDescUnitConfig.ValueScanner.(field.TypeValueScanner[*productcatalog.UnitConfig]) // chargeusagebasedDescFeatureKey is the schema descriptor for feature_key field. - chargeusagebasedDescFeatureKey := chargeusagebasedFields[3].Descriptor() + chargeusagebasedDescFeatureKey := chargeusagebasedFields[4].Descriptor() // chargeusagebased.FeatureKeyValidator is a validator for the "feature_key" field. It is called by the builders before save. chargeusagebased.FeatureKeyValidator = chargeusagebasedDescFeatureKey.Validators[0].(func(string) error) // chargeusagebasedDescFeatureID is the schema descriptor for feature_id field. - chargeusagebasedDescFeatureID := chargeusagebasedFields[4].Descriptor() + chargeusagebasedDescFeatureID := chargeusagebasedFields[5].Descriptor() // chargeusagebased.FeatureIDValidator is a validator for the "feature_id" field. It is called by the builders before save. chargeusagebased.FeatureIDValidator = chargeusagebasedDescFeatureID.Validators[0].(func(string) error) // chargeusagebasedDescPrice is the schema descriptor for price field. - chargeusagebasedDescPrice := chargeusagebasedFields[6].Descriptor() + chargeusagebasedDescPrice := chargeusagebasedFields[7].Descriptor() chargeusagebased.ValueScanner.Price = chargeusagebasedDescPrice.ValueScanner.(field.TypeValueScanner[*productcatalog.Price]) // chargeusagebasedDescID is the schema descriptor for id field. chargeusagebasedDescID := chargeusagebasedMixinFields0[18].Descriptor() @@ -2426,11 +2435,14 @@ func init() { // planratecardDescPrice is the schema descriptor for price field. planratecardDescPrice := planratecardFields[5].Descriptor() planratecard.ValueScanner.Price = planratecardDescPrice.ValueScanner.(field.TypeValueScanner[*productcatalog.Price]) + // planratecardDescUnitConfig is the schema descriptor for unit_config field. + planratecardDescUnitConfig := planratecardFields[6].Descriptor() + planratecard.ValueScanner.UnitConfig = planratecardDescUnitConfig.ValueScanner.(field.TypeValueScanner[*productcatalog.UnitConfig]) // planratecardDescDiscounts is the schema descriptor for discounts field. - planratecardDescDiscounts := planratecardFields[6].Descriptor() + planratecardDescDiscounts := planratecardFields[7].Descriptor() planratecard.ValueScanner.Discounts = planratecardDescDiscounts.ValueScanner.(field.TypeValueScanner[*productcatalog.Discounts]) // planratecardDescPhaseID is the schema descriptor for phase_id field. - planratecardDescPhaseID := planratecardFields[7].Descriptor() + planratecardDescPhaseID := planratecardFields[8].Descriptor() // planratecard.PhaseIDValidator is a validator for the "phase_id" field. It is called by the builders before save. planratecard.PhaseIDValidator = planratecardDescPhaseID.Validators[0].(func(string) error) // planratecardDescID is the schema descriptor for id field. diff --git a/openmeter/ent/db/setorclear.go b/openmeter/ent/db/setorclear.go index 85f6b7f759..e22087cfe8 100644 --- a/openmeter/ent/db/setorclear.go +++ b/openmeter/ent/db/setorclear.go @@ -243,6 +243,20 @@ func (u *AddonRateCardUpdateOne) SetOrClearPrice(value **productcatalog.Price) * return u.SetPrice(*value) } +func (u *AddonRateCardUpdate) SetOrClearUnitConfig(value **productcatalog.UnitConfig) *AddonRateCardUpdate { + if value == nil { + return u.ClearUnitConfig() + } + return u.SetUnitConfig(*value) +} + +func (u *AddonRateCardUpdateOne) SetOrClearUnitConfig(value **productcatalog.UnitConfig) *AddonRateCardUpdateOne { + if value == nil { + return u.ClearUnitConfig() + } + return u.SetUnitConfig(*value) +} + func (u *AddonRateCardUpdate) SetOrClearDiscounts(value **productcatalog.Discounts) *AddonRateCardUpdate { if value == nil { return u.ClearDiscounts() @@ -1797,6 +1811,34 @@ func (u *BillingInvoiceUsageBasedLineConfigUpdateOne) SetOrClearMeteredQuantity( return u.SetMeteredQuantity(*value) } +func (u *BillingInvoiceUsageBasedLineConfigUpdate) SetOrClearConvertedQuantity(value *alpacadecimal.Decimal) *BillingInvoiceUsageBasedLineConfigUpdate { + if value == nil { + return u.ClearConvertedQuantity() + } + return u.SetConvertedQuantity(*value) +} + +func (u *BillingInvoiceUsageBasedLineConfigUpdateOne) SetOrClearConvertedQuantity(value *alpacadecimal.Decimal) *BillingInvoiceUsageBasedLineConfigUpdateOne { + if value == nil { + return u.ClearConvertedQuantity() + } + return u.SetConvertedQuantity(*value) +} + +func (u *BillingInvoiceUsageBasedLineConfigUpdate) SetOrClearAppliedUnitConfig(value **productcatalog.UnitConfig) *BillingInvoiceUsageBasedLineConfigUpdate { + if value == nil { + return u.ClearAppliedUnitConfig() + } + return u.SetAppliedUnitConfig(*value) +} + +func (u *BillingInvoiceUsageBasedLineConfigUpdateOne) SetOrClearAppliedUnitConfig(value **productcatalog.UnitConfig) *BillingInvoiceUsageBasedLineConfigUpdateOne { + if value == nil { + return u.ClearAppliedUnitConfig() + } + return u.SetAppliedUnitConfig(*value) +} + func (u *BillingInvoiceValidationIssueUpdate) SetOrClearDeletedAt(value *time.Time) *BillingInvoiceValidationIssueUpdate { if value == nil { return u.ClearDeletedAt() @@ -3211,6 +3253,20 @@ func (u *ChargeUsageBasedUpdateOne) SetOrClearDiscounts(value **productcatalog.D return u.SetDiscounts(*value) } +func (u *ChargeUsageBasedUpdate) SetOrClearUnitConfig(value **productcatalog.UnitConfig) *ChargeUsageBasedUpdate { + if value == nil { + return u.ClearUnitConfig() + } + return u.SetUnitConfig(*value) +} + +func (u *ChargeUsageBasedUpdateOne) SetOrClearUnitConfig(value **productcatalog.UnitConfig) *ChargeUsageBasedUpdateOne { + if value == nil { + return u.ClearUnitConfig() + } + return u.SetUnitConfig(*value) +} + func (u *ChargeUsageBasedUpdate) SetOrClearCurrentRealizationRunID(value *string) *ChargeUsageBasedUpdate { if value == nil { return u.ClearCurrentRealizationRunID() @@ -5101,6 +5157,20 @@ func (u *PlanRateCardUpdateOne) SetOrClearPrice(value **productcatalog.Price) *P return u.SetPrice(*value) } +func (u *PlanRateCardUpdate) SetOrClearUnitConfig(value **productcatalog.UnitConfig) *PlanRateCardUpdate { + if value == nil { + return u.ClearUnitConfig() + } + return u.SetUnitConfig(*value) +} + +func (u *PlanRateCardUpdateOne) SetOrClearUnitConfig(value **productcatalog.UnitConfig) *PlanRateCardUpdateOne { + if value == nil { + return u.ClearUnitConfig() + } + return u.SetUnitConfig(*value) +} + func (u *PlanRateCardUpdate) SetOrClearDiscounts(value **productcatalog.Discounts) *PlanRateCardUpdate { if value == nil { return u.ClearDiscounts() diff --git a/openmeter/ent/schema/billing.go b/openmeter/ent/schema/billing.go index 91e0912a38..fc6a48a9a4 100644 --- a/openmeter/ent/schema/billing.go +++ b/openmeter/ent/schema/billing.go @@ -581,6 +581,20 @@ func (BillingInvoiceUsageBasedLineConfig) Fields() []ent.Field { SchemaType(map[string]string{ dialect.Postgres: "numeric", }), + field.Other("converted_quantity", alpacadecimal.Decimal{}). + Optional(). + Nillable(). + SchemaType(map[string]string{ + dialect.Postgres: "numeric", + }), + field.String("applied_unit_config"). + GoType(&productcatalog.UnitConfig{}). + ValueScanner(UnitConfigValueScanner). + SchemaType(map[string]string{ + dialect.Postgres: "jsonb", + }). + Optional(). + Nillable(), } } diff --git a/openmeter/ent/schema/chargesusagebased.go b/openmeter/ent/schema/chargesusagebased.go index 8516d35a88..026e98486b 100644 --- a/openmeter/ent/schema/chargesusagebased.go +++ b/openmeter/ent/schema/chargesusagebased.go @@ -48,6 +48,15 @@ func (ChargeUsageBased) Fields() []ent.Field { Optional(). Nillable(), + field.String("unit_config"). + GoType(&productcatalog.UnitConfig{}). + ValueScanner(UnitConfigValueScanner). + SchemaType(map[string]string{ + dialect.Postgres: "jsonb", + }). + Optional(). + Nillable(), + field.String("feature_key"). NotEmpty(). Immutable(), diff --git a/openmeter/ent/schema/productcatalog.go b/openmeter/ent/schema/productcatalog.go index 05c90d4f94..b7e10587cd 100644 --- a/openmeter/ent/schema/productcatalog.go +++ b/openmeter/ent/schema/productcatalog.go @@ -202,6 +202,7 @@ var ( EntitlementTemplateValueScanner = entutils.JSONStringValueScanner[*productcatalog.EntitlementTemplate]() TaxConfigValueScanner = entutils.JSONStringValueScanner[*productcatalog.TaxConfig]() PriceValueScanner = entutils.JSONStringValueScanner[*productcatalog.Price]() + UnitConfigValueScanner = entutils.JSONStringValueScanner[*productcatalog.UnitConfig]() DiscountsValueScanner = entutils.JSONStringValueScanner[*productcatalog.Discounts]() ProRatingConfigValueScanner = entutils.JSONStringValueScanner[productcatalog.ProRatingConfig]() ) diff --git a/openmeter/ent/schema/ratecard.go b/openmeter/ent/schema/ratecard.go index 84e590da42..ffe104c80f 100644 --- a/openmeter/ent/schema/ratecard.go +++ b/openmeter/ent/schema/ratecard.go @@ -54,6 +54,14 @@ func (RateCard) Fields() []ent.Field { }). Optional(). Nillable(), + field.String("unit_config"). + GoType(&productcatalog.UnitConfig{}). + ValueScanner(UnitConfigValueScanner). + SchemaType(map[string]string{ + dialect.Postgres: "jsonb", + }). + Optional(). + Nillable(), field.String("discounts"). GoType(&productcatalog.Discounts{}). ValueScanner(DiscountsValueScanner). diff --git a/openmeter/productcatalog/addon/adapter/addon.go b/openmeter/productcatalog/addon/adapter/addon.go index f8c8e0fea5..aab601f507 100644 --- a/openmeter/productcatalog/addon/adapter/addon.go +++ b/openmeter/productcatalog/addon/adapter/addon.go @@ -250,6 +250,10 @@ func rateCardBulkCreate(c *entdb.AddonRateCardClient, rateCards productcatalog.R q.SetPrice(rateCardEntity.Price) } + if rateCardEntity.UnitConfig != nil { + q.SetUnitConfig(rateCardEntity.UnitConfig) + } + bulk = append(bulk, q) } diff --git a/openmeter/productcatalog/addon/adapter/mapping.go b/openmeter/productcatalog/addon/adapter/mapping.go index dd0a36a0ad..3b1f496e75 100644 --- a/openmeter/productcatalog/addon/adapter/mapping.go +++ b/openmeter/productcatalog/addon/adapter/mapping.go @@ -95,6 +95,7 @@ func FromAddonRateCardRow(r entdb.AddonRateCard) (*addon.RateCard, error) { FeatureID: r.FeatureID, TaxConfig: r.TaxConfig, Price: r.Price, + UnitConfig: r.UnitConfig, Discounts: lo.FromPtr(r.Discounts), } @@ -306,6 +307,7 @@ func FromPlanRateCardRow(r entdb.PlanRateCard) (productcatalog.RateCard, error) EntitlementTemplate: r.EntitlementTemplate, TaxConfig: r.TaxConfig, Price: r.Price, + UnitConfig: r.UnitConfig, Discounts: lo.FromPtr(r.Discounts), } @@ -363,6 +365,7 @@ func asAddonRateCardRow(r productcatalog.RateCard) (entdb.AddonRateCard, error) FeatureKey: meta.FeatureKey, FeatureID: meta.FeatureID, Price: meta.Price, + UnitConfig: meta.UnitConfig, Type: r.Type(), Discounts: lo.EmptyableToPtr(meta.Discounts), } diff --git a/openmeter/productcatalog/errors.go b/openmeter/productcatalog/errors.go index d3a5fdb0af..d39ee97514 100644 --- a/openmeter/productcatalog/errors.go +++ b/openmeter/productcatalog/errors.go @@ -376,6 +376,16 @@ var ErrRateCardUsageBasedPriceWithNoFeature = models.NewValidationIssue( commonhttp.WithHTTPStatusCodeAttribute(http.StatusBadRequest), ) +const ErrCodeRateCardUnitConfigRequiresUsageBasedPrice models.ErrorCode = "unit_config_requires_usage_based_price" + +var ErrRateCardUnitConfigRequiresUsageBasedPrice = models.NewValidationIssue( + ErrCodeRateCardUnitConfigRequiresUsageBasedPrice, + "unit config is only valid with unit, graduated, or volume prices", + models.WithFieldString("unitConfig"), + models.WithCriticalSeverity(), + commonhttp.WithHTTPStatusCodeAttribute(http.StatusBadRequest), +) + // Addon errors const ErrCodeAddonKeyEmpty models.ErrorCode = "addon_key_empty" diff --git a/openmeter/productcatalog/plan/adapter/adapter_test.go b/openmeter/productcatalog/plan/adapter/adapter_test.go index a4738e6fe0..df55c8b19a 100644 --- a/openmeter/productcatalog/plan/adapter/adapter_test.go +++ b/openmeter/productcatalog/plan/adapter/adapter_test.go @@ -143,6 +143,12 @@ func TestPostgresAdapter(t *testing.T) { MaximumAmount: nil, }, }), + UnitConfig: &productcatalog.UnitConfig{ + Operation: productcatalog.UnitConfigOperationDivide, + ConversionFactor: decimal.NewFromInt(1000), + Rounding: lo.ToPtr(productcatalog.UnitConfigRoundingModeCeiling), + DisplayUnit: lo.ToPtr("GB"), + }, }, BillingCadence: pctestutils.MonthPeriod, }, diff --git a/openmeter/productcatalog/plan/adapter/mapping.go b/openmeter/productcatalog/plan/adapter/mapping.go index 3f6e9d0139..7811191d11 100644 --- a/openmeter/productcatalog/plan/adapter/mapping.go +++ b/openmeter/productcatalog/plan/adapter/mapping.go @@ -182,6 +182,7 @@ func FromAddonRateCardRow(r entdb.AddonRateCard) (productcatalog.RateCard, error FeatureID: r.FeatureID, TaxConfig: r.TaxConfig, Price: r.Price, + UnitConfig: r.UnitConfig, Discounts: lo.FromPtr(r.Discounts), } @@ -296,6 +297,7 @@ func fromPlanRateCardRow(r entdb.PlanRateCard) (productcatalog.RateCard, error) EntitlementTemplate: r.EntitlementTemplate, TaxConfig: r.TaxConfig, Price: r.Price, + UnitConfig: r.UnitConfig, Discounts: lo.FromPtr(r.Discounts), } @@ -382,6 +384,7 @@ func asPlanRateCardRow(r productcatalog.RateCard) (entdb.PlanRateCard, error) { EntitlementTemplate: meta.EntitlementTemplate, TaxConfig: meta.TaxConfig, Price: meta.Price, + UnitConfig: meta.UnitConfig, Type: r.Type(), Discounts: lo.EmptyableToPtr(meta.Discounts), } diff --git a/openmeter/productcatalog/plan/adapter/phase.go b/openmeter/productcatalog/plan/adapter/phase.go index fc6cf7f4a6..badc7f75d7 100644 --- a/openmeter/productcatalog/plan/adapter/phase.go +++ b/openmeter/productcatalog/plan/adapter/phase.go @@ -135,6 +135,10 @@ func rateCardBulkCreate(c *entdb.PlanRateCardClient, rateCards productcatalog.Ra q.SetPrice(rateCardEntity.Price) } + if rateCardEntity.UnitConfig != nil { + q.SetUnitConfig(rateCardEntity.UnitConfig) + } + bulk = append(bulk, q) } diff --git a/openmeter/productcatalog/plan/assert.go b/openmeter/productcatalog/plan/assert.go index 92f8179026..3d24feb3ae 100644 --- a/openmeter/productcatalog/plan/assert.go +++ b/openmeter/productcatalog/plan/assert.go @@ -198,6 +198,8 @@ func AssertRateCardEqual(t *testing.T, r1, r2 productcatalog.RateCard) { assert.Truef(t, m1.TaxConfig.Equal(m2.TaxConfig), "tax config mismatch") + assert.Truef(t, m1.UnitConfig.Equal(m2.UnitConfig), "unit config mismatch") + assert.Truef(t, m1.Price.Equal(m2.Price), "price mismatch") billingCadence1 := r1.GetBillingCadence().ISOStringPtrOrNil() diff --git a/openmeter/productcatalog/ratecard.go b/openmeter/productcatalog/ratecard.go index 64c540f042..17f9639207 100644 --- a/openmeter/productcatalog/ratecard.go +++ b/openmeter/productcatalog/ratecard.go @@ -88,6 +88,10 @@ type RateCardMeta struct { // Price defines the price for the RateCard Price *Price `json:"price"` + // UnitConfig optionally transforms metered quantities into billing units + // before pricing. Persisted but inert until Phase 3 wires it into rating. + UnitConfig *UnitConfig `json:"unitConfig,omitempty"` + // Discounts defines a list of discounts for the RateCard Discounts Discounts `json:"discounts,omitempty"` } @@ -140,6 +144,11 @@ func (r RateCardMeta) Clone() RateCardMeta { clone.Price = &p } + if r.UnitConfig != nil { + uc := r.UnitConfig.Clone() + clone.UnitConfig = &uc + } + clone.Discounts = r.Discounts.Clone() // TaxCode is an eagerly loaded reference, shallow copy is sufficient @@ -182,6 +191,10 @@ func (r RateCardMeta) Equal(v RateCardMeta) bool { return false } + if !r.UnitConfig.Equal(v.UnitConfig) { + return false + } + if !r.Discounts.Equal(v.Discounts) { return false } @@ -245,6 +258,27 @@ func (r RateCardMeta) Validate() error { } } + if r.UnitConfig != nil { + if err := r.UnitConfig.Validate(); err != nil { + errs = append(errs, fmt.Errorf("invalid unit config: %w", + models.ErrorWithFieldPrefix( + models.NewFieldSelectorGroup(models.NewFieldSelector("unitConfig")), + err), + )) + } + + if r.Price == nil { + errs = append(errs, ErrRateCardUnitConfigRequiresUsageBasedPrice) + } else { + switch r.Price.Type() { + case UnitPriceType, TieredPriceType: + // allowed + default: + errs = append(errs, ErrRateCardUnitConfigRequiresUsageBasedPrice) + } + } + } + if err := r.Discounts.ValidateForPrice(r.Price); err != nil { errs = append(errs, err) } diff --git a/openmeter/productcatalog/ratecard_test.go b/openmeter/productcatalog/ratecard_test.go index c571849e48..9e00baadb8 100644 --- a/openmeter/productcatalog/ratecard_test.go +++ b/openmeter/productcatalog/ratecard_test.go @@ -141,6 +141,26 @@ func TestFlatFeeRateCard(t *testing.T) { }, ExpectedError: true, }, + { + Name: "invalid, unit config rejected with flat price", + RateCard: FlatFeeRateCard{ + RateCardMeta: RateCardMeta{ + Key: "feat-1", + Name: "Flat 1", + Description: lo.ToPtr("Flat 1"), + Price: NewPriceFrom(FlatPrice{ + Amount: decimal.NewFromInt(1000), + PaymentTerm: InArrearsPaymentTerm, + }), + UnitConfig: &UnitConfig{ + Operation: UnitConfigOperationMultiply, + ConversionFactor: decimal.NewFromFloat(1.2), + }, + }, + BillingCadence: lo.ToPtr(datetime.MustParseDuration(t, "P1M")), + }, + ExpectedError: true, + }, } for _, test := range tests { @@ -336,6 +356,61 @@ func TestUsageBasedRateCard(t *testing.T) { }, ExpectedError: true, }, + { + Name: "valid, unit config with unit price", + RateCard: UsageBasedRateCard{ + RateCardMeta: RateCardMeta{ + Key: "feat-1", + Name: "Usage 1", + FeatureKey: lo.ToPtr(feat1.Key), + FeatureID: lo.ToPtr(feat1.ID), + Price: NewPriceFrom(UnitPrice{ + Amount: decimal.NewFromInt(1), + }), + UnitConfig: &UnitConfig{ + Operation: UnitConfigOperationMultiply, + ConversionFactor: decimal.NewFromFloat(1.5), + }, + }, + BillingCadence: datetime.MustParseDuration(t, "P1M"), + }, + ExpectedError: false, + }, + { + Name: "valid, unit config with tiered price", + RateCard: UsageBasedRateCard{ + RateCardMeta: RateCardMeta{ + Key: "feat-1", + Name: "Usage 1", + FeatureKey: lo.ToPtr(feat1.Key), + FeatureID: lo.ToPtr(feat1.ID), + Price: NewPriceFrom(TieredPrice{ + Mode: VolumeTieredPrice, + Tiers: []PriceTier{ + { + UpToAmount: lo.ToPtr(decimal.NewFromInt(100)), + UnitPrice: &PriceTierUnitPrice{ + Amount: decimal.NewFromInt(5), + }, + }, + { + UpToAmount: nil, + UnitPrice: &PriceTierUnitPrice{ + Amount: decimal.NewFromInt(1), + }, + }, + }, + }), + UnitConfig: &UnitConfig{ + Operation: UnitConfigOperationDivide, + ConversionFactor: decimal.NewFromInt(1000), + Rounding: lo.ToPtr(UnitConfigRoundingModeCeiling), + }, + }, + BillingCadence: datetime.MustParseDuration(t, "P1M"), + }, + ExpectedError: false, + }, } for _, test := range tests { diff --git a/openmeter/productcatalog/unitconfig.go b/openmeter/productcatalog/unitconfig.go new file mode 100644 index 0000000000..516a202719 --- /dev/null +++ b/openmeter/productcatalog/unitconfig.go @@ -0,0 +1,220 @@ +package productcatalog + +import ( + "errors" + "fmt" + "slices" + + decimal "github.com/alpacahq/alpacadecimal" + "github.com/samber/lo" + + "github.com/openmeterio/openmeter/pkg/models" +) + +const ( + UnitConfigOperationMultiply UnitConfigOperation = "multiply" + UnitConfigOperationDivide UnitConfigOperation = "divide" +) + +type UnitConfigOperation string + +func (o UnitConfigOperation) Values() []string { + return []string{ + string(UnitConfigOperationMultiply), + string(UnitConfigOperationDivide), + } +} + +func (o UnitConfigOperation) Validate() error { + if !slices.Contains(o.Values(), string(o)) { + return fmt.Errorf("invalid unit config operation: %s", o) + } + + return nil +} + +const ( + UnitConfigRoundingModeNone UnitConfigRoundingMode = "none" + UnitConfigRoundingModeCeiling UnitConfigRoundingMode = "ceiling" + UnitConfigRoundingModeFloor UnitConfigRoundingMode = "floor" + UnitConfigRoundingModeHalfUp UnitConfigRoundingMode = "half_up" +) + +type UnitConfigRoundingMode string + +func (r UnitConfigRoundingMode) Values() []string { + return []string{ + string(UnitConfigRoundingModeNone), + string(UnitConfigRoundingModeCeiling), + string(UnitConfigRoundingModeFloor), + string(UnitConfigRoundingModeHalfUp), + } +} + +func (r UnitConfigRoundingMode) Validate() error { + if !slices.Contains(r.Values(), string(r)) { + return fmt.Errorf("invalid unit config rounding mode: %s", r) + } + + return nil +} + +// UnitConfig transforms a raw metered quantity into a billing quantity before +// pricing and entitlement evaluation. See unitconfig.md / UnitConfig.md for the +// conceptual model. The rating engine does not yet apply UnitConfig (Phase 3 +// of the roadmap); persisted values currently round-trip but are inert. +type UnitConfig struct { + // Operation is the arithmetic conversion to apply: multiply or divide. + Operation UnitConfigOperation `json:"operation"` + + // ConversionFactor is the positive non-zero factor used by Operation. + ConversionFactor decimal.Decimal `json:"conversion_factor"` + + // Rounding controls how the converted quantity is rounded for invoicing. + // Entitlement balance checks always use the unrounded value. + Rounding *UnitConfigRoundingMode `json:"rounding,omitempty"` + + // Precision is the decimal places retained after rounding. Only meaningful + // when Rounding is set and not "none". Nil means round to whole numbers. + Precision *int `json:"precision,omitempty"` + + // DisplayUnit is a human-readable label for the converted unit shown on + // invoices and the customer portal (e.g. "GB", "hours"). + DisplayUnit *string `json:"display_unit,omitempty"` +} + +func (c *UnitConfig) Validate() error { + if c == nil { + return nil + } + + var errs []error + + if err := c.Operation.Validate(); err != nil { + errs = append(errs, err) + } + + if c.ConversionFactor.IsNegative() { + errs = append(errs, errors.New("conversion_factor must not be negative")) + } + + if c.ConversionFactor.IsZero() { + errs = append(errs, errors.New("conversion_factor must not be zero")) + } + + if c.Rounding != nil { + if err := c.Rounding.Validate(); err != nil { + errs = append(errs, err) + } + } + + if c.Precision != nil && *c.Precision < 0 { + errs = append(errs, errors.New("precision must not be negative")) + } + + return models.NewNillableGenericValidationError(errors.Join(errs...)) +} + +func (c *UnitConfig) Equal(v *UnitConfig) bool { + if c == nil && v == nil { + return true + } + + if c == nil || v == nil { + return false + } + + if c.Operation != v.Operation { + return false + } + + if !c.ConversionFactor.Equal(v.ConversionFactor) { + return false + } + + if lo.FromPtr(c.Rounding) != lo.FromPtr(v.Rounding) { + return false + } + + if lo.FromPtr(c.Precision) != lo.FromPtr(v.Precision) { + return false + } + + if lo.FromPtr(c.DisplayUnit) != lo.FromPtr(v.DisplayUnit) { + return false + } + + return true +} + +// Apply transforms a raw metered quantity into the converted (precise) and +// invoiced (rounded) billing quantities. +// +// - converted: raw with the operation × conversion_factor applied. Used for +// entitlement balance checks, which always see the precise value. +// - invoiced: converted with the configured rounding/precision applied. Used +// as the line quantity at billing time. Equal to converted when no +// rounding is set or rounding is "none". +// +// When c is nil, both return values equal raw. Callers must have already run +// Validate so the operation and rounding enums are known values; an +// unrecognized operation falls back to identity rather than panicking +// mid-billing. +func (c *UnitConfig) Apply(raw decimal.Decimal) (converted, invoiced decimal.Decimal) { + if c == nil { + return raw, raw + } + + switch c.Operation { + case UnitConfigOperationMultiply: + converted = raw.Mul(c.ConversionFactor) + case UnitConfigOperationDivide: + converted = raw.Div(c.ConversionFactor) + default: + return raw, raw + } + + invoiced = converted + + if c.Rounding == nil { + return converted, invoiced + } + + places := int32(0) + if c.Precision != nil { + places = int32(*c.Precision) + } + + switch *c.Rounding { + case UnitConfigRoundingModeCeiling: + invoiced = converted.RoundCeil(places) + case UnitConfigRoundingModeFloor: + invoiced = converted.RoundFloor(places) + case UnitConfigRoundingModeHalfUp: + invoiced = converted.Round(places) + case UnitConfigRoundingModeNone: + } + + return converted, invoiced +} + +func (c UnitConfig) Clone() UnitConfig { + out := UnitConfig{ + Operation: c.Operation, + ConversionFactor: c.ConversionFactor.Copy(), + } + + if c.Rounding != nil { + out.Rounding = lo.ToPtr(*c.Rounding) + } + + if c.Precision != nil { + out.Precision = lo.ToPtr(*c.Precision) + } + + if c.DisplayUnit != nil { + out.DisplayUnit = lo.ToPtr(*c.DisplayUnit) + } + + return out +} diff --git a/openmeter/productcatalog/unitconfig_test.go b/openmeter/productcatalog/unitconfig_test.go new file mode 100644 index 0000000000..bab1cc1cfe --- /dev/null +++ b/openmeter/productcatalog/unitconfig_test.go @@ -0,0 +1,137 @@ +package productcatalog + +import ( + "testing" + + decimal "github.com/alpacahq/alpacadecimal" + "github.com/samber/lo" + "github.com/stretchr/testify/assert" +) + +func TestUnitConfigApply(t *testing.T) { + t.Run("nil receiver is identity", func(t *testing.T) { + var c *UnitConfig + raw := decimal.NewFromInt(1247) + + converted, invoiced := c.Apply(raw) + assert.True(t, converted.Equal(raw)) + assert.True(t, invoiced.Equal(raw)) + }) + + t.Run("multiply without rounding produces precise converted", func(t *testing.T) { + c := &UnitConfig{ + Operation: UnitConfigOperationMultiply, + ConversionFactor: decimal.NewFromFloat(1.5), + } + raw := decimal.NewFromFloat(4.2) + + converted, invoiced := c.Apply(raw) + assert.True(t, converted.Equal(decimal.NewFromFloat(6.3)), "converted: %s", converted.String()) + assert.True(t, invoiced.Equal(converted), "no rounding → invoiced equals converted") + }) + + t.Run("divide without rounding produces precise converted", func(t *testing.T) { + c := &UnitConfig{ + Operation: UnitConfigOperationDivide, + ConversionFactor: decimal.NewFromInt(1000), + } + raw := decimal.NewFromInt(1247) + + converted, invoiced := c.Apply(raw) + assert.True(t, converted.Equal(decimal.NewFromFloat(1.247)), "converted: %s", converted.String()) + assert.True(t, invoiced.Equal(converted)) + }) + + t.Run("divide with ceiling rounding to whole numbers (package-style)", func(t *testing.T) { + c := &UnitConfig{ + Operation: UnitConfigOperationDivide, + ConversionFactor: decimal.NewFromInt(1000), + Rounding: lo.ToPtr(UnitConfigRoundingModeCeiling), + } + raw := decimal.NewFromInt(1247) + + converted, invoiced := c.Apply(raw) + assert.True(t, converted.Equal(decimal.NewFromFloat(1.247)), "converted stays precise: %s", converted.String()) + assert.True(t, invoiced.Equal(decimal.NewFromInt(2)), "invoiced rounds up: %s", invoiced.String()) + }) + + t.Run("divide with floor rounding", func(t *testing.T) { + c := &UnitConfig{ + Operation: UnitConfigOperationDivide, + ConversionFactor: decimal.NewFromInt(1000), + Rounding: lo.ToPtr(UnitConfigRoundingModeFloor), + } + raw := decimal.NewFromInt(1999) + + _, invoiced := c.Apply(raw) + assert.True(t, invoiced.Equal(decimal.NewFromInt(1)), "invoiced floors: %s", invoiced.String()) + }) + + t.Run("divide with half_up rounding", func(t *testing.T) { + c := &UnitConfig{ + Operation: UnitConfigOperationDivide, + ConversionFactor: decimal.NewFromInt(1000), + Rounding: lo.ToPtr(UnitConfigRoundingModeHalfUp), + } + + _, invoicedUp := c.Apply(decimal.NewFromInt(1500)) + assert.True(t, invoicedUp.Equal(decimal.NewFromInt(2)), "1.500 rounds up: %s", invoicedUp.String()) + + _, invoicedDown := c.Apply(decimal.NewFromInt(1499)) + assert.True(t, invoicedDown.Equal(decimal.NewFromInt(1)), "1.499 rounds down: %s", invoicedDown.String()) + }) + + t.Run("explicit precision retains decimal places", func(t *testing.T) { + c := &UnitConfig{ + Operation: UnitConfigOperationDivide, + ConversionFactor: decimal.NewFromInt(1000), + Rounding: lo.ToPtr(UnitConfigRoundingModeCeiling), + Precision: lo.ToPtr(2), + } + raw := decimal.NewFromInt(1247) + + _, invoiced := c.Apply(raw) + assert.True(t, invoiced.Equal(decimal.NewFromFloat(1.25)), "1.247 ceil to 2dp: %s", invoiced.String()) + }) + + t.Run("rounding mode none leaves converted intact", func(t *testing.T) { + c := &UnitConfig{ + Operation: UnitConfigOperationDivide, + ConversionFactor: decimal.NewFromInt(1000), + Rounding: lo.ToPtr(UnitConfigRoundingModeNone), + } + raw := decimal.NewFromInt(1247) + + converted, invoiced := c.Apply(raw) + assert.True(t, invoiced.Equal(converted)) + }) + + t.Run("v1 dynamic equivalence: multiply", func(t *testing.T) { + c := &UnitConfig{ + Operation: UnitConfigOperationMultiply, + ConversionFactor: decimal.NewFromFloat(1.5), + } + raw := decimal.NewFromFloat(4.20) + + _, invoiced := c.Apply(raw) + expected := raw.Mul(decimal.NewFromFloat(1.5)) + assert.True(t, invoiced.Equal(expected), "Dynamic{1.5} parity: %s vs %s", invoiced.String(), expected.String()) + }) + + t.Run("v1 package equivalence: divide + ceiling", func(t *testing.T) { + c := &UnitConfig{ + Operation: UnitConfigOperationDivide, + ConversionFactor: decimal.NewFromInt(1000), + Rounding: lo.ToPtr(UnitConfigRoundingModeCeiling), + } + + _, exact := c.Apply(decimal.NewFromInt(1000)) + assert.True(t, exact.Equal(decimal.NewFromInt(1)), "exact boundary: %s", exact.String()) + + _, partial := c.Apply(decimal.NewFromInt(1247)) + assert.True(t, partial.Equal(decimal.NewFromInt(2)), "partial: %s", partial.String()) + + _, none := c.Apply(decimal.NewFromInt(0)) + assert.True(t, none.Equal(decimal.NewFromInt(0)), "zero: %s", none.String()) + }) +} diff --git a/prorating-vs-progressive-billing-eli5.md b/prorating-vs-progressive-billing-eli5.md new file mode 100644 index 0000000000..01b398c165 --- /dev/null +++ b/prorating-vs-progressive-billing-eli5.md @@ -0,0 +1,20 @@ +# Prorating vs progressive billing + +Close, but no — they're different mechanisms that both deal with "partial periods" in some sense. + +| | Prorating | Progressive billing | +|---|---|---| +| **What it does** | Scales a fee by a *time fraction* of the period | Splits a usage line into multiple invoice lines *within* the same period | +| **Typical target** | Flat fees / subscription fees | Usage-based prices | +| **Trigger** | Customer joins, upgrades, or cancels mid-period | Billing window inside a longer service period (e.g. weekly billing on a monthly plan) | +| **Math** | `fee × days_active / period_days` | Same rating math, but the rater is told "X units of this period were already billed in a prior split" | + +**Prorating example.** $100/month subscription, customer joins on day 16 of a 30-day month → first invoice is `$100 × 15/30 = $50`. The fee is computed once, scaled by time. + +**Progressive billing example.** Customer is on a monthly metered plan but you invoice them weekly. The month is one service period, but it's split into ~4 billing windows. At week 1, you bill the usage observed so far; at week 2, you bill the *new* usage on top, and the graduated tiers need to know "we already consumed 80 units in the prior window so tier 1 has 20 left." That tracker is `PreLinePeriodQuantity` in the L4 doc. + +**Why volume tiered can't progressive-bill.** It's not about time scaling — it's about the splitting mechanism. Volume's price depends on the *final* quantity at period close to decide which tier the entire usage lands in. Mid-window, you don't know yet. If a customer is at 80 units at week 1, they might end the month at 90 (tier 1) or 200 (tier 2) — and tier 2 reprices *all* 200 units, including the 80 from week 1. So you'd have to either (a) guess and reconcile later (messy), or (b) refuse to split, which is what the code does — it errors on `SplitLineGroup` for volume-tiered lines. + +Graduated has no such problem because each band's price is fixed the moment those units are consumed. Tier 1's price applies to units 0–100 regardless of where you end up; once unit 101 happens, it's priced at tier 2's rate forever, no retroactive change. + +**Prorating doesn't have this issue** because it only scales flat fees, which don't have usage-dependent tiers. diff --git a/tiered-pricing-eli5.md b/tiered-pricing-eli5.md new file mode 100644 index 0000000000..9d6a10daf8 --- /dev/null +++ b/tiered-pricing-eli5.md @@ -0,0 +1,24 @@ +# Tiered pricing — graduated vs volume + +Totally fair — "tiered" without a modifier is genuinely ambiguous and the L4 rating-engine note even calls it out as a frequent source of confusion. The cleanest way to internalize it is one example, run through both modes: + +**Setup:** tiers are `[0–100 @ $10/unit, 101+ @ $15/unit]`. Customer at **150 units**. + +| Mode | Mental model | Math | Bill | +|---|---|---|---| +| **Graduated** | Tax brackets. Each tier's price applies *only to units in that band*. | (100 × $10) + (50 × $15) = $1,000 + $750 | **$1,750** | +| **Volume** | "Final quantity picks the rate." The total falls in one tier; that price applies to *all* units. | 150 × $15 (because 150 is in tier 2) | **$2,250** | + +Same input, $500 apart. Different rule for which units pay what. + +**Why both exist (the intent behind each):** + +- **Graduated** = customer benefits *as they grow*. Used for "the more you use, the cheaper the marginal unit." Common for usage-discount plans where the customer's effective rate naturally improves with scale. +- **Volume** = customer benefits *all-or-nothing once they cross*. Used for "buy more than X and we'll re-rate the whole thing at the better price." Common for commitment-style pricing where crossing a threshold reprices everything retroactively. + +**One operational gotcha** (from the L4 doc) that makes the distinction real beyond pricing semantics: + +- **Volume can't be progressive-billed.** Its price depends on the *final* quantity at period close. Mid-period invoicing doesn't know which tier you'll land in, so the code explicitly errors on a `SplitLineGroup` for a volume-tiered line. +- **Graduated can be progressive-billed** (with `PreLinePeriodQuantity` tracking) because each band's price is settled the moment that band's units are consumed. + +So if you ever see "tiered" in a conversation or code, the first question to ask is *which mode* — the math, the split behavior, and the customer-facing intent are all different. diff --git a/tools/migrate/migrations/20260514145018_add_unit_config_to_ratecards.down.sql b/tools/migrate/migrations/20260514145018_add_unit_config_to_ratecards.down.sql new file mode 100644 index 0000000000..1edb3e7f50 --- /dev/null +++ b/tools/migrate/migrations/20260514145018_add_unit_config_to_ratecards.down.sql @@ -0,0 +1,4 @@ +-- reverse: modify "plan_rate_cards" table +ALTER TABLE "plan_rate_cards" DROP COLUMN "unit_config"; +-- reverse: modify "addon_rate_cards" table +ALTER TABLE "addon_rate_cards" DROP COLUMN "unit_config"; diff --git a/tools/migrate/migrations/20260514145018_add_unit_config_to_ratecards.up.sql b/tools/migrate/migrations/20260514145018_add_unit_config_to_ratecards.up.sql new file mode 100644 index 0000000000..f1239608ef --- /dev/null +++ b/tools/migrate/migrations/20260514145018_add_unit_config_to_ratecards.up.sql @@ -0,0 +1,4 @@ +-- modify "addon_rate_cards" table +ALTER TABLE "addon_rate_cards" ADD COLUMN "unit_config" jsonb NULL; +-- modify "plan_rate_cards" table +ALTER TABLE "plan_rate_cards" ADD COLUMN "unit_config" jsonb NULL; diff --git a/tools/migrate/migrations/20260518160440_add_unit_config_to_invoice_usage_based_line_config.down.sql b/tools/migrate/migrations/20260518160440_add_unit_config_to_invoice_usage_based_line_config.down.sql new file mode 100644 index 0000000000..e75da9f2d8 --- /dev/null +++ b/tools/migrate/migrations/20260518160440_add_unit_config_to_invoice_usage_based_line_config.down.sql @@ -0,0 +1,2 @@ +-- reverse: modify "billing_invoice_usage_based_line_configs" table +ALTER TABLE "billing_invoice_usage_based_line_configs" DROP COLUMN "applied_unit_config", DROP COLUMN "converted_quantity"; diff --git a/tools/migrate/migrations/20260518160440_add_unit_config_to_invoice_usage_based_line_config.up.sql b/tools/migrate/migrations/20260518160440_add_unit_config_to_invoice_usage_based_line_config.up.sql new file mode 100644 index 0000000000..3f4eb22bfa --- /dev/null +++ b/tools/migrate/migrations/20260518160440_add_unit_config_to_invoice_usage_based_line_config.up.sql @@ -0,0 +1,2 @@ +-- modify "billing_invoice_usage_based_line_configs" table +ALTER TABLE "billing_invoice_usage_based_line_configs" ADD COLUMN "converted_quantity" numeric NULL, ADD COLUMN "applied_unit_config" jsonb NULL; diff --git a/tools/migrate/migrations/20260518163515_add_unit_config_to_charge_usage_based.down.sql b/tools/migrate/migrations/20260518163515_add_unit_config_to_charge_usage_based.down.sql new file mode 100644 index 0000000000..69c1dc0054 --- /dev/null +++ b/tools/migrate/migrations/20260518163515_add_unit_config_to_charge_usage_based.down.sql @@ -0,0 +1,2 @@ +-- reverse: modify "charge_usage_based" table +ALTER TABLE "charge_usage_based" DROP COLUMN "unit_config"; diff --git a/tools/migrate/migrations/20260518163515_add_unit_config_to_charge_usage_based.up.sql b/tools/migrate/migrations/20260518163515_add_unit_config_to_charge_usage_based.up.sql new file mode 100644 index 0000000000..92dab4d84b --- /dev/null +++ b/tools/migrate/migrations/20260518163515_add_unit_config_to_charge_usage_based.up.sql @@ -0,0 +1,2 @@ +-- modify "charge_usage_based" table +ALTER TABLE "charge_usage_based" ADD COLUMN "unit_config" jsonb NULL; diff --git a/tools/migrate/migrations/atlas.sum b/tools/migrate/migrations/atlas.sum index 56ceb6d3cb..e2de2a6b6b 100644 --- a/tools/migrate/migrations/atlas.sum +++ b/tools/migrate/migrations/atlas.sum @@ -1,4 +1,4 @@ -h1:WTxy/UBLguSNWcnzymmD8y2u5y7y4Nqng2u2P5ud9ro= +h1:ugyEyL9iTTaTN4cwloG8XFcesC2Y2dpq/vlqF/KAo/s= 20240826120919_init.up.sql h1:tc1V91/smlmaeJGQ8h+MzTEeFjjnrrFDbDAjOYJK91o= 20240903155435_entitlement-expired-index.up.sql h1:Hp8u5uckmLXc1cRvWU0AtVnnK8ShlpzZNp8pbiJLhac= 20240917172257_billing-entities.up.sql h1:Q1dAMo0Vjiit76OybClNfYPGC5nmvov2/M2W1ioi4Kw= @@ -200,3 +200,6 @@ h1:WTxy/UBLguSNWcnzymmD8y2u5y7y4Nqng2u2P5ud9ro= 20260513072457_ledger_transaction_template_codes.up.sql h1:qbiFeJwp78WmAiUZjBne6RZwsij5lMRdkJn24yVkPTE= 20260513083018_add_flat_fee_run_immutable.up.sql h1:Wbl6jpSXveNQ8s1IJQ72AKsULD1pFffVTyPOypXYsY0= 20260513120726_add_app_custom_invoicing_customers_partial_unique_indexes.up.sql h1:8CAyzs63mv7p2EXSZ7IxvLvPvemN+9kTMn8iiY/jXm4= +20260514145018_add_unit_config_to_ratecards.up.sql h1:F4kx9rBCSwifNdJ6JiWJQHur7Nvt93v+Ay2P4848KIE= +20260518160440_add_unit_config_to_invoice_usage_based_line_config.up.sql h1:Xg3i5e/hRrDh1hIJ91vyo97RMlCYgM8ghl56xW0slvo= +20260518163515_add_unit_config_to_charge_usage_based.up.sql h1:6sJQ5MqGYcwWn+ENs95E8SnXtAIhmiPEk/einuwuPFk= diff --git a/unitconfig-eli5.md b/unitconfig-eli5.md new file mode 100644 index 0000000000..afe53f03f6 --- /dev/null +++ b/unitconfig-eli5.md @@ -0,0 +1,106 @@ +# UnitConfig — Pricing pipeline & scenarios + +> 10,000m view. How does a metered number become money? What pricing shapes exist, and where does UnitConfig fit? + +## The pipeline (when each step runs) + +Every billable event travels this path: + +``` +[Customer does a thing] + ↓ event ("user X called API at T, used 1,204 tokens") +[[Meter]] aggregates events → raw number + ↓ +[[Entitlement]] checks allowance (precise, unrounded) + ↓ +UnitConfig (optional) → converts raw → billable quantity + ↓ +Price turns billable quantity → money + ↓ +[[Invoice]] line +``` + +Two handoffs do the real work: + +- The **meter** picks what the raw number means (calls, bytes, tokens, dollars-of-cost…). +- **UnitConfig**, if present, sits between the meter and the price — same number, different unit. The price then acts on whatever it receives: raw if no UnitConfig, converted if one is set. + +## Price shapes + +| Shape | What it means | Math | +|---|---|---| +| **Flat** | A fixed fee, not usage-based | `amount` | +| **Unit** | $X per unit consumed | `quantity × amount` | +| **Tiered (graduated)** | Different price per band, like tax brackets | sum of (units in tier × tier price) | +| **Tiered (volume)** | Total picks one tier; that price applies to *all* units | `quantity × tier_price` | +| **Dynamic** *(v1 only)* | Multiplier on cost passed in via the event | `quantity × multiplier` | +| **Package** *(v1 only)* | $X per bundle of N units, round up | `⌈quantity ÷ N⌉ × amount` | +| **UnitConfig** | Not a price — a *conversion layer* before the price | `raw × factor` (multiply) or `raw ÷ factor` (divide), optionally rounded | + +UnitConfig has three knobs: an **operation** (multiply or divide), a **conversion_factor** (number), and optional **rounding** + **display_unit**. + +--- + +## Scenario A — Plain per-unit ("$0.01 per API call") + +Customer makes **1,247 calls**. + +| | Configuration | Math | Bill | +|---|---|---|---| +| v1 | `UnitPrice{$0.01}` | 1,247 × $0.01 | **$12.47** | +| v3 | `UnitPrice{$0.01}` (no UnitConfig) | 1,247 × $0.01 | **$12.47** | + +Meter output is already the billable unit. Nothing to convert. + +## Scenario B — Per-package ("$10 per 1,000 calls, round up") + +Customer makes **1,247 calls**. + +| | Configuration | Math | Bill | +|---|---|---|---| +| v1 | `PackagePrice{$10, per: 1000}` | ⌈1,247 ÷ 1,000⌉ × $10 = 2 × $10 | **$20** | +| v3 | `UnitPrice{$10}` + `UnitConfig{divide, 1000, ceiling}` | ⌈1,247 ÷ 1,000⌉ × $10 = 2 × $10 | **$20** | + +Same answer. v3 makes the divide-and-round step **explicit** instead of baked into the price type. + +## Scenario C — Cost-plus markup ("Resell tokens at 1.5×") + +Meter sums a `provider_cost` field per event. Customer accumulates **$4.20** of cost. + +| | Configuration | Math | Bill | +|---|---|---|---| +| v1 | `DynamicPrice{multiplier: 1.5}` | $4.20 × 1.5 | **$6.30** | +| v3 | `UnitPrice{$1}` + `UnitConfig{multiply, 1.5}` | $4.20 × 1.5 × $1 | **$6.30** | + +Same answer. v3 separates "margin" (UnitConfig) from "per-unit price" (UnitPrice) — two knobs you can turn independently. + +--- + +## What v3 unlocks (v1 cannot do this in one rate card) + +### Scenario D — Tiered by GB + +Meter sums bytes. Plan: **first 100 GB free, next 1 TB at $0.05/GB, above 1.1 TB at $0.03/GB.** Customer at **1.5 TB** raw. + +| | Boundaries authored in… | Failure mode | +|---|---|---| +| v1 | bytes — `up_to_amount: 100_000_000_000`, `1_100_000_000_000` | If the meter ever changes unit (bytes → KB), the whole plan must be rewritten. | +| v3 | GB — `up_to_amount: 100, 1100` after `UnitConfig{divide, 1e9, ceiling, "GB"}` | Boundaries match the human unit; meter changes don't touch the plan. | + +Math is the same: `0 + 1,000 × $0.05 + 400 × $0.03 = $62`. The unlock is **readability + stability**, and the invoice can display "GB" alongside the bill. + +### Scenario E — Markup composed with tiers + +"Resell tokens at 1.5× **and** give a volume discount: $0.001/token for the first 1M, $0.0005 after." Customer at **5M provider tokens**. + +- **v1: not expressible.** `DynamicPrice` is flat (no tiers). `TieredPrice` has no multiplier. +- **v3:** `UnitConfig{multiply, 1.5}` (margin first) → `TieredPrice{graduated, [1M @ $0.001, rest @ $0.0005]}` on the converted 7.5M. +- Math: 1M × $0.001 + 6.5M × $0.0005 = **$4,250**. + +Margin and tier shape become **separable decisions** on the same rate card. + +--- + +## One quirk worth flagging + +**Entitlement and invoicing use different precisions when rounding is set.** Invoice sees the rounded value (5 packages, 1,500 GB); the [[Entitlement]] check sees the unrounded one (1.247 packages, 1,499.8 GB). A customer can hit their cap on a fractional unit while the invoice still bills in whole units. By design — gating wants precision, invoices want clean lines. diff --git a/unitconfig_plan.md b/unitconfig_plan.md new file mode 100644 index 0000000000..c14e3eb2c0 --- /dev/null +++ b/unitconfig_plan.md @@ -0,0 +1,375 @@ +# UnitConfig roadmap + +Step 0 — v3 read-side translation of v1 dynamic and package prices into +`UnitPrice + UnitConfig` — has shipped. Each phase below is independently +shippable and assumes the previous ones are landed. + +## Phase 1 — Domain + persistence (no behavior change) + +**Goal:** UnitConfig becomes a first-class thing in the codebase without +changing what users see. + +- `openmeter/productcatalog/unitconfig.go`: `UnitConfig`, + `UnitConfigOperation`, `UnitConfigRoundingMode`, `Validate()`, `Clone()`, + `Equal()`. +- Add `UnitConfig *UnitConfig` to `RateCardMeta`. +- Ent schema: store as a JSON column on the rate-card row (1:1, simple, + matches API shape). +- Migration via `atlas migrate --env local diff add_unit_config_to_ratecards`. +- Keep v3 read translation from step 0 intact — it still synthesizes + UnitConfig from v1 Dynamic/Package on the fly. Empty `UnitConfig` column + for those rows. + +**Gate:** domain type round-trips through DB; nothing else changes. + +**Status — shipped on `feat/unitconfig-poc`.** Domain type added with +`Validate`/`Clone`/`Equal`; `UnitConfig *UnitConfig` threaded through +`RateCardMeta` (Clone/Equal/Validate updated); `unit_config` JSON column +on the shared `RateCard` Ent mixin → present on both `plan_rate_cards` +and `addon_rate_cards`; adapter mappings updated for read + write in +`plan/adapter/mapping.go` and `addon/adapter/mapping.go`; bulk-create +builders extended in `plan/adapter/phase.go` and `addon/adapter/addon.go`. +Migration `20260514145018_add_unit_config_to_ratecards`. Round-trip gated +by `TestPostgresAdapter/Plan/Create` (fixture now includes a non-nil +UnitConfig). v3 read translation untouched; reject-on-write check stays. + +**Gotcha for future phases.** The bulk-create builder is a `q.Set*` chain +that's separate from the entity-struct construction. Adding a new +RateCard-mixin field requires updating BOTH places — the entity struct +in `mapping.go` AND the `Set*` chain in the bulk-create builder. +Otherwise the field silently drops on write. The duplicative pattern is +load-bearing, not vestigial. + +## Phase 2 — v3 authoring + +**Goal:** v3 clients can create/update plans with `UnitConfig`. + +- Flip visibility on `unit_config` from `Lifecycle.Read` to full + read/create/update in `ratecard.tsp`. +- `FromAPIBillingRateCard` parses it. +- Validation: `UnitConfig` is only valid when the price is unit / graduated + / volume. Reject on flat/free, and reject on dynamic/package (the v1-only + types). Decide whether `conversion_factor > 0` is enforced server-side. +- Equivalence rule: a v3-authored `UnitPrice + UnitConfig{multiply, m}` is + *not* equal to a v1-authored `DynamicPrice{m}` for editing/diff purposes + — the read translation produces lookalike output but they're different + rows in storage. Decision needed: do we collapse them at write time or + keep them distinct? + +**Gate:** v3 round-trip works; v1 plans untouched; charges/billing still +rate on the underlying price (UnitConfig still inert). + +**Status — shipped on `feat/unitconfig-poc`.** TypeSpec `unit_config` +flipped to full read/create/update (`ratecard.tsp:66`); regenerated +`api/v3/api.gen.go` + `api/v3/openapi.yaml`. `FromAPIBillingUnitConfig` +/ `ToAPIBillingUnitConfig` helpers added in +`api/v3/handlers/plans/convert.go`; `FromAPIBillingRateCard` parses +UnitConfig into `meta.UnitConfig` instead of rejecting. Read path +prefers persisted `meta.UnitConfig` over v1 synthesis; +`ToAPIBillingRateCardUnitConfig` remains as the v1 Dynamic/Package +synthesis fallback. Semantic validation added in `RateCardMeta.Validate()` +(`ratecard.go:261`): UnitConfig is only accepted with `UnitPriceType` / +`TieredPriceType`; rejected on flat / free / dynamic / package via new +sentinel `ErrRateCardUnitConfigRequiresUsageBasedPrice` (`errors.go`). +Tests cover parse (multiply, divide+rounding+display, invalid decimal), +domain validate (unit+UC and tiered+UC accept, flat+UC reject), and +read-path verbatim (persisted wins over synthesis). + +**Deferred.** Step 5 (v1 list refusal for v3-only shapes — tiered + +UnitConfig) is not implemented. Once a v3-authored tiered+UnitConfig +plan is persisted, v1 list still serializes the underlying tiered price +without the UnitConfig context. Decision was to ship Phase 2 without it; +revisit before any production use that mixes v1 list consumers with v3 +authoring. See Decision #1. + +**Equivalence policy as enforced.** Decision #1 ("replace, not collapse") +is enforced by construction: the domain validator forbids +`Dynamic + UnitConfig` and `Package + UnitConfig`, so the only way to +persist a UnitConfig is alongside a Unit or Tiered price. There is no +write-time collapse logic and no defensive read fallback — trust +validation. Gotcha for Phase 3: when the rating engine starts consuming +UnitConfig, the v1 Dynamic/Package synthesis path in +`ToAPIBillingRateCardUnitConfig` is read-only display — the *stored* +price is still v1 Dynamic/Package, not Unit+UnitConfig, so rating must +keep handling those types natively or back-project them at the rating +layer (see Phase 3 bullet 4). + +## Phase 3 — Charges + rating engine + +**Goal:** UnitConfig actually transforms metered quantities at billing time. + +- Charges: when realizing usage, apply `operation × conversion_factor`, + then rounding/precision, before producing the billable quantity. +- Rating: line amount uses converted quantity × unit price. +- Invoice line: emit `InvoiceUsageQuantityDetail` snapshot (raw / + converted / invoiced / display_unit / applied_unit_config) per + `unitconfig.tsp:105`. +- Backfill the v1 read-path equivalence at the rating layer too: a stored + `DynamicPrice` should rate identically whether read as v1 or as the + synthesized v3 form. + +**Gate:** invoices for plans with UnitConfig produce the same totals as +the equivalent v1 dynamic/package plan. + +**Status — partially shipped on `feat/unitconfig-poc`.** Sub-steps 3a–3c +landed; 3d (API surface) and 3e (end-to-end equivalence gate test) are +remaining. + +**Step 3a — UnitConfig carried on the charge intent (shipped).** +`usagebased.Intent` (`openmeter/billing/charges/usagebased/charge.go`) +carries `UnitConfig *productcatalog.UnitConfig`; `Intent.Validate()` +runs `UnitConfig.Validate()` inline. The subscription-sync charge +constructor `newUsageBasedChargeIntent` +(`openmeter/billing/worker/subscriptionsync/service/reconciler/patchchargeusagebased.go`) +sets it from `rateCardMeta.UnitConfig`. New JSONB column `unit_config` +on the `charge_usage_based` Ent schema plus migration +`20260518163515_add_unit_config_to_charge_usage_based.{up,down}.sql`; +adapter `Create`/`UpdateOne` paths and the DB→domain mapper updated. +No behavior change yet — UnitConfig is carried but nothing reads it. + +**Step 3b — UnitConfig applied at the rating-input path (shipped).** +The two callsites that build `usagebased.RateableIntent` from a snapshot +quantity now apply `intent.UnitConfig.Apply(rawCumulative)` and pass the +*invoiced* cumulative quantity as `MeterValue`: +`openmeter/billing/charges/usagebased/service/rating/totals.go` +(`GetTotalsForUsage`) and `.../delta/engine.go` (`Engine.Rate`). The +domain helper `UnitConfig.Apply(raw) (converted, invoiced Decimal)` is +the single conversion entry point. Tests under +`.../delta/unitconfig_test.go` cover multiply equivalence, divide+ceiling +equivalence, and the load-bearing no-double-billing property when raw +usage moves within a package boundary. + +**Step 3c — InvoiceUsageQuantityDetail snapshot persisted (shipped).** +`billing.UsageBasedLine` (`openmeter/billing/stdinvoiceline.go`) gained +`ConvertedQuantity *Decimal` (precise line-period after conversion) and +`AppliedUnitConfig *UnitConfig` (snapshot in effect at billing time). +Linemapper `populateUsageBasedStandardLineFromRun` +(`openmeter/billing/charges/usagebased/service/linemapper.go`) now +accepts the charge intent and writes both fields when UnitConfig is set; +both callers in `lineengine.go` pass `charge.Intent`. The line-period +converted quantity uses **cumulative-then-diff** (`Apply(cumulative_current).converted - +Apply(cumulative_prior).converted`) so ceiling/floor rounding stays +correct across runs. Adapter read (`stdinvoicelinemapper.go`) and write +(`stdinvoicelines.go:upsertUsageBasedConfig` via +`SetNillableConvertedQuantity` / `SetAppliedUnitConfig`) updated. +Existing `Quantity` / `PreLinePeriodQuantity` semantics on the +UsageBasedLine were intentionally left unchanged — they remain +discount-aware, raw-units values. The new fields are the audit-trail +half only. + +**Decisions made during Phase 3 implementation:** + +1. **Conversion lives at the charge layer, not the rating engine.** + Rating stays UnitConfig-unaware; it sees whatever billable quantity + the charge layer hands it. This is what made the v1↔v3 equivalence + tests fall out cleanly and makes Phase 4 entitlement (precise + converted, no rounding) easy to fork off the same Apply. +2. **Cumulative-then-diff is load-bearing for ceiling/floor rounding.** + Applying UnitConfig to per-run diffs would double-bill customers who + added raw usage inside an existing package boundary. The delta engine + test `TestUnitConfigDivideCeilingCumulativeNoDoubleBilling` locks + this in. Any refactor that tries to apply UnitConfig to a delta + instead of cumulative will break it. +3. **UnitConfig persists on both the charge and the invoice line.** On + the charge so re-rates read the right config after persistence; on + the line as a snapshot for invoice display history. The two are + redundant in normal flow but the line snapshot is the immutable + audit record. +4. **`Quantity`/`PreLinePeriodQuantity` semantics unchanged in 3c.** + These remain the discount-aware raw-unit values. If invoice display + needs them in converted units, that's a follow-on; the + `InvoiceUsageQuantityDetail` surface (3d) carries the explicit + raw/converted/invoiced trio for that purpose. +5. **Ent "silent drop" pattern hit twice in Phase 3.** Both + `BillingInvoiceUsageBasedLineConfig` and `ChargeUsageBased` have + hand-written `Create()/Update*().Set*()` chains that needed explicit + `Set` calls for the new columns. Memory note + `project-ratecard-ent-builder-pattern` was generalized to cover both + sites. + +**Remaining work — Step 3d (API surface).** Map the persisted line +fields to the v3 `BillingInvoiceUsageQuantityDetail` model on invoice +line responses. TypeSpec already defines the model +(`api/spec/packages/aip/src/productcatalog/unitconfig.tsp:120`); the v3 +invoice-line response shape needs a field referencing it, and the +`To...` handler needs to assemble `{raw, converted, invoiced, +display_unit, applied_unit_config}` from +`UsageBasedLine.{MeteredQuantity, ConvertedQuantity, Quantity, +AppliedUnitConfig}`. Confirm where this mapping should live in the v3 +invoice handler before editing. + +**Remaining work — Step 3e (end-to-end gate test).** The Phase 3 gate +in the plan says: "invoices for plans with UnitConfig produce the same +totals as the equivalent v1 dynamic/package plan." The delta tests +cover this at the rating layer. Step 3e is the integration-level +version: drive two full plan flows (v1 `DynamicPrice` and v3 +`UnitPrice + UnitConfig{multiply}`) through subscription sync → charge +realization → invoice line and assert identical money totals. Same for +package vs divide+ceiling. Likely belongs in +`test/billing/` using `SubscriptionMixin`. + +**Skipped for Phase 3 by explicit decision.** The plan's fourth Phase 3 +bullet ("Backfill the v1 read-path equivalence at the rating layer too: +a stored DynamicPrice should rate identically whether read as v1 or as +the synthesized v3 form") was deliberately NOT unified into a single +code path — v1 Dynamic and Package keep their existing rate functions, +and equivalence is enforced by tests rather than by code unification. +This matches Phase 2 Decision #1 ("replace, not collapse"). See the +delta `unitconfig_test.go` tests. + +## Phase 4 — Entitlements + +**Goal:** entitlement balance checks see *precise* (unrounded) converted +quantities. + +- Apply `operation × conversion_factor` only — skip rounding. Per + `unitconfig.tsp:31`. +- Touch points: balance worker, entitlement reset/recurrence, and any + place that compares raw meter output against entitlement quotas. + +**Gate:** a customer on a `divide` UnitConfig hits their entitlement cap +on the converted axis, not the raw meter axis. + +## Phase 5 — Tiered price semantics + +**Open design question, not deferrable past this point.** The TypeSpec +docs (`price.tsp:137,165,191`) state that `up_to_amount` on tier +boundaries is expressed in *converted* (billing) units when UnitConfig is +present. That's a meaningful semantic shift. + +- Decide: do we apply UnitConfig *before* tier matching (tiers are in + billing units) or *after* (tiers are in raw units)? The doc says before. + But it changes how plan authors think about tiers. +- Document and enforce uniformly across rating, entitlement balance + display, and invoice rendering. + +**Gate:** consistent tier behavior under UnitConfig, with a clear contract. + +## Phase 6 — Subscription propagation + +**Goal:** active subscriptions carry UnitConfig forward. + +- Subscription view / sync: when a subscription is attached to a plan + version, the subscription's per-rate-card snapshot includes the + UnitConfig. +- Plan version bump: pinning vs migration policy. Existing behavior is to + pin to the version attached at; same applies here unless we explicitly + migrate. +- Subscription editing API: accept UnitConfig on rate-card overrides. + +**Gate:** a subscription on a UnitConfig-bearing plan rates correctly +through plan version changes and edits. + +## Phase 7 — Migration & deprecation policy + +**Decision, not implementation.** Two options: + +- **Coexist forever.** Keep v1 dynamic/package authoring; v3 read + translation is the bridge. Pro: zero risk to existing customers. Con: + two ways to do the same thing in storage forever. +- **Backfill + deprecate.** One-off migration converts stored + `Dynamic` → `UnitConfig{multiply}`, `Package` → `UnitConfig{divide, ceiling}`. + Stop accepting new Dynamic/Package via v1. Pro: single source of truth. + Con: real migration risk and a v1 SDK breaking change. + +Recommend deferring this decision until Phase 3 is shipped — by then we'll +know whether the equivalence is truly lossless in production data. + +--- + +## Cross-cutting open questions to resolve before Phase 2 + +1. **Equivalence policy** between v1 Dynamic/Package and v3 + UnitConfig+UnitPrice forms (Phase 2 can't ship without an answer). +2. **Where validation lives** — TypeSpec constraints, domain `Validate()`, + or service layer? Suggested split: shape constraints in TypeSpec, + semantic constraints (which prices accept UnitConfig) in domain. +3. **`conversion_factor` precision** — what's the max practical scale for + decimal? Multiplier-style configs likely want more than money-style 2dp. +4. **Display semantics** — does the `display_unit` show on customer-portal + invoices today? Where else does it surface (PDF, hosted page, webhooks)? + +## Decisions (to revisit when implementing) + +These are working answers to the four questions above. None are committed +code yet; revisit when actually implementing Phase 2+. + +1. **Equivalence: replace, not collapse.** When v3 authoring lands, a + write with `UnitPrice + UnitConfig` replaces a stored + `DynamicPrice`/`PackagePrice` row. The v1 list endpoint + back-projects the simple cases (`UnitPrice + UnitConfig{multiply}` → + `DynamicPrice`, etc.) and refuses to list v3-only shapes (e.g. tiered + + UnitConfig). Reason: equivalence detection on every edit is fragile; + replace gives one canonical storage shape; the v1-read-side surprise + ("plan no longer listable") is the honest failure mode for plans v1 + can't express. +2. **Validation layering:** TypeSpec for shape (enum membership, + nullability, decimal range if supported by validator); domain + `Validate()` for cross-field semantics ("UnitConfig only valid with + Unit / Tiered"); handler-layer checks only for transitional rules + ("not yet accepted") that will be deleted in Phase 2. The current + reject-on-write in `convert.go` is in the transitional bucket. +3. **`conversion_factor` precision:** single field-level cap of ~18 dp + storage, applied uniformly to multiply and divide. The interesting + precision/rounding decisions belong in the rating pipeline (Phase 3), + not on the storage field. +4. **Display:** persist `InvoiceUsageQuantityDetail` (raw / converted / + invoiced / applied UnitConfig) on the invoice line in Phase 3. Defer + per-surface rendering (portal, PDF, webhook, email) — each surface + makes its own call once the underlying data is available. + +--- + +## Resume prompt — pick up at Step 3d + +Copy this into a fresh session on `feat/unitconfig-poc` to continue +where the last session left off: + +> Continuing UnitConfig POC on branch `feat/unitconfig-poc` in this +> repo. Phases 1, 2, and Phase 3 sub-steps 3a/3b/3c have shipped. Pick +> up at Step 3d (API surface) and then 3e (end-to-end equivalence gate +> test). +> +> Read these to orient — they have everything you need: +> +> 1. `unitconfig_plan.md` — phased roadmap. The Phase 3 "Status — +> partially shipped" block has shipped sub-steps with file pointers, +> the load-bearing design decisions (cumulative-then-diff, +> charge-layer conversion, etc.), and the scope of the remaining 3d +> + 3e work. +> 2. `unitconfig-eli5.md`, `tiered-pricing-eli5.md`, +> `prorating-vs-progressive-billing-eli5.md` — conceptual primers. +> Skim only if needed. +> 3. Internal docs vault at `/Users/roland.spekker/repos/indernal-docs` +> (the typo "indernal" is intentional — that's the actual path). Key +> notes: `Primitives/UnitConfig.md`, `level-4-rating-engine.md`. The +> `obsidian` CLI is on PATH; run commands from inside that directory +> so the vault is detected. +> 4. Auto-memory entry `project-ratecard-ent-builder-pattern` — the Ent +> "silent drop" pattern. Now generalized to cover RateCard mixin, +> `BillingInvoiceUsageBasedLineConfig`, and `ChargeUsageBased` sites. +> If 3d/3e adds any new Ent fields, this applies again. +> 5. Auto-memory entry `feedback-noisy-commands-subagent` — delegate +> `make generate` / `gen-api` / `test` / `build` to a subagent or +> hand me a copyable command; don't run them inline in the main +> session. +> +> **Step 3d scope (from the plan):** map the persisted +> `UsageBasedLine.{MeteredQuantity, ConvertedQuantity, Quantity, +> AppliedUnitConfig}` to the v3 `BillingInvoiceUsageQuantityDetail` +> model on invoice-line responses. TypeSpec model already exists at +> `api/spec/packages/aip/src/productcatalog/unitconfig.tsp:120`. Surface +> only emits when `AppliedUnitConfig != nil`. Identify the v3 invoice +> handler / convert function and confirm where the mapping lives before +> editing. +> +> **Step 3e scope:** end-to-end gate test under `test/billing/` using +> `SubscriptionMixin`. Drive two plan flows (v1 `DynamicPrice{m}` and +> v3 `UnitPrice(1) + UnitConfig{multiply, m}`) through subscription +> sync → charge realization → invoice line; assert identical money +> totals. Same for `PackagePrice{amount, qty}` vs `UnitPrice(amount) + +> UnitConfig{divide, qty, ceiling}`. This is the plan's stated Phase 3 +> gate. +> +> Before any code, confirm 3d scope with me. Start with recon, propose +> a sequencing plan before writing code.