package lineengine import ( "testing" "time" "github.com/alpacahq/alpacadecimal" "github.com/samber/lo" "github.com/samber/mo" "github.com/stretchr/testify/require" "github.com/openmeterio/openmeter/openmeter/billing" "github.com/openmeterio/openmeter/openmeter/productcatalog" "github.com/openmeterio/openmeter/pkg/models" "github.com/openmeterio/openmeter/pkg/timeutil" ) func TestValidateLegacyLineOverrideRejectsSplitLinePeriodChange(t *testing.T) { period := lineEngineOverrideTestPeriod() line := standardLineForLineEngineOverrideTest(t, period) line.SplitLineGroupID = lo.ToPtr("split-line-group-id") err := validateLegacyLineOverride(billing.InvoiceLineOverride{ ExistingLine: line.AsGenericLine(), ChangesToApply: billing.ExistingLineOverride{ Period: mo.Some(timeutil.ClosedPeriod{ From: period.From, To: period.To.AddDate(0, 1, 0), }), }, }) require.ErrorIs(t, err, billing.ErrInvoiceLineNoPeriodChangeForSplitLine) } func TestValidateLegacyLineOverrideValidatesSplitLineUsageDiscountChanges(t *testing.T) { period := lineEngineOverrideTestPeriod() t.Run("unchanged usage discount succeeds", func(t *testing.T) { line := standardLineForLineEngineOverrideTest(t, period) line.SplitLineGroupID = lo.ToPtr("split-line-group-id") line.RateCardDiscounts = usageDiscountForLineEngineOverrideTest("10") err := validateLegacyLineOverride(billing.InvoiceLineOverride{ ExistingLine: line.AsGenericLine(), ChangesToApply: billing.ExistingLineOverride{ Discounts: mo.Some(usageDiscountForLineEngineOverrideTest("10")), }, }) require.NoError(t, err) }) t.Run("changed usage discount fails", func(t *testing.T) { line := standardLineForLineEngineOverrideTest(t, period) line.SplitLineGroupID = lo.ToPtr("split-line-group-id") line.RateCardDiscounts = usageDiscountForLineEngineOverrideTest("10") err := validateLegacyLineOverride(billing.InvoiceLineOverride{ ExistingLine: line.AsGenericLine(), ChangesToApply: billing.ExistingLineOverride{ Discounts: mo.Some(usageDiscountForLineEngineOverrideTest("11")), }, }) require.ErrorIs(t, err, billing.ErrInvoiceLineProgressiveBillingUsageDiscountUpdateForbidden) }) } func TestValidateLegacyLineOverrideValidatesSubscriptionManagedPeriodChange(t *testing.T) { period := lineEngineOverrideTestPeriod() subscription := &billing.SubscriptionReference{ SubscriptionID: "subscription-id", PhaseID: "phase-id", ItemID: "item-id", BillingPeriod: period, } t.Run("usage-based line period change fails", func(t *testing.T) { line := standardLineForLineEngineOverrideTest(t, period) line.Subscription = subscription err := validateLegacyLineOverride(billing.InvoiceLineOverride{ ExistingLine: line.AsGenericLine(), ChangesToApply: billing.ExistingLineOverride{ Period: mo.Some(timeutil.ClosedPeriod{ From: period.From, To: period.To.AddDate(0, 1, 0), }), }, }) require.ErrorIs(t, err, billing.ErrInvoiceLineNoPeriodChangeForSubscriptionManagedLine) }) t.Run("flat-fee line period change succeeds", func(t *testing.T) { line := standardLineForLineEngineOverrideTest(t, period) line.Subscription = subscription line.UsageBased.Price = productcatalog.NewPriceFrom(productcatalog.FlatPrice{ Amount: alpacadecimal.RequireFromString("1"), PaymentTerm: productcatalog.InAdvancePaymentTerm, }) err := validateLegacyLineOverride(billing.InvoiceLineOverride{ ExistingLine: line.AsGenericLine(), ChangesToApply: billing.ExistingLineOverride{ Period: mo.Some(timeutil.ClosedPeriod{ From: period.From, To: period.To.AddDate(0, 1, 0), }), }, }) require.NoError(t, err) }) t.Run("usage-based to flat-fee period change fails", func(t *testing.T) { line := standardLineForLineEngineOverrideTest(t, period) line.Subscription = subscription err := validateLegacyLineOverride(billing.InvoiceLineOverride{ ExistingLine: line.AsGenericLine(), ChangesToApply: billing.ExistingLineOverride{ Period: mo.Some(timeutil.ClosedPeriod{ From: period.From, To: period.To.AddDate(0, 1, 0), }), Price: mo.Some(productcatalog.NewPriceFrom(productcatalog.FlatPrice{ Amount: alpacadecimal.RequireFromString("1"), PaymentTerm: productcatalog.InAdvancePaymentTerm, })), }, }) require.ErrorIs(t, err, billing.ErrInvoiceLineNoPeriodChangeForSubscriptionManagedLine) }) } func lineEngineOverrideTestPeriod() timeutil.ClosedPeriod { return 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), } } func standardLineForLineEngineOverrideTest(t *testing.T, period timeutil.ClosedPeriod) *billing.StandardLine { t.Helper() return &billing.StandardLine{ StandardLineBase: billing.StandardLineBase{ ManagedResource: models.NewManagedResource(models.ManagedResourceInput{ Namespace: "ns", ID: "line-id", Name: "line", CreatedAt: period.From, UpdatedAt: period.From, }), ManagedBy: billing.ManuallyManagedLine, Engine: billing.LineEngineTypeInvoice, InvoiceID: "invoice-id", Currency: "USD", Period: period, InvoiceAt: period.To, }, UsageBased: &billing.UsageBasedLine{ Price: productcatalog.NewPriceFrom(productcatalog.UnitPrice{Amount: alpacadecimal.RequireFromString("1")}), FeatureKey: "feature-key", }, } } func usageDiscountForLineEngineOverrideTest(quantity string) billing.Discounts { return billing.Discounts{ Usage: &billing.UsageDiscount{ UsageDiscount: productcatalog.UsageDiscount{ Quantity: alpacadecimal.RequireFromString(quantity), }, }, } }