| package subscriptionaddon_test |
|
|
| import ( |
| "fmt" |
| "testing" |
|
|
| "github.com/alpacahq/alpacadecimal" |
| "github.com/samber/lo" |
| "github.com/stretchr/testify/require" |
|
|
| "github.com/openmeterio/openmeter/openmeter/ent/db/entitlement" |
| "github.com/openmeterio/openmeter/openmeter/productcatalog" |
| "github.com/openmeterio/openmeter/openmeter/productcatalog/addon" |
| "github.com/openmeterio/openmeter/openmeter/subscription" |
| subscriptionaddon "github.com/openmeterio/openmeter/openmeter/subscription/addon" |
| subscriptiontestutils "github.com/openmeterio/openmeter/openmeter/subscription/testutils" |
| "github.com/openmeterio/openmeter/pkg/datetime" |
| "github.com/openmeterio/openmeter/pkg/models" |
| ) |
|
|
| func TestValidations(t *testing.T) { |
| someMeta := productcatalog.RateCardMeta{ |
| Name: "Test Addon Rate Card 4", |
| Description: lo.ToPtr("Test Addon Rate Card 4 Description"), |
| Key: subscriptiontestutils.ExampleFeatureKey, |
| FeatureKey: lo.ToPtr(subscriptiontestutils.ExampleFeatureKey), |
| Price: productcatalog.NewPriceFrom(productcatalog.FlatPrice{ |
| Amount: alpacadecimal.NewFromInt(100), |
| PaymentTerm: productcatalog.InAdvancePaymentTerm, |
| }), |
| } |
|
|
| t.Run("Should error if provided RateCard is nil", func(t *testing.T) { |
| rc := getTestAddonRateCard(&productcatalog.FlatFeeRateCard{ |
| RateCardMeta: someMeta.Clone(), |
| }) |
|
|
| t.Run("Apply", func(t *testing.T) { |
| err := rc.Apply(nil, nil) |
| require.Error(t, err) |
| require.ErrorContains(t, err, "target must not be nil") |
| }) |
|
|
| t.Run("Restore", func(t *testing.T) { |
| err := rc.Restore(nil, nil, productcatalog.AddonInstanceTypeSingle) |
| require.Error(t, err) |
| require.ErrorContains(t, err, "target must not be nil") |
| }) |
| }) |
|
|
| t.Run("Should error if provided RateCard is not a pointer", func(t *testing.T) { |
| rc := getTestAddonRateCard(&productcatalog.FlatFeeRateCard{ |
| RateCardMeta: someMeta.Clone(), |
| }) |
|
|
| t.Run("Apply", func(t *testing.T) { |
| err := rc.Apply(nonPointerRateCard{}, nil) |
| require.Error(t, err) |
| require.ErrorContains(t, err, "target must be a pointer") |
| }) |
|
|
| t.Run("Restore", func(t *testing.T) { |
| err := rc.Restore(nonPointerRateCard{}, nil, productcatalog.AddonInstanceTypeSingle) |
| require.Error(t, err) |
| require.ErrorContains(t, err, "target must be a pointer") |
| }) |
| }) |
|
|
| t.Run("Should error if provided annotations are nil", func(t *testing.T) { |
| rc := getTestAddonRateCard(&productcatalog.FlatFeeRateCard{ |
| RateCardMeta: someMeta.Clone(), |
| }) |
|
|
| t.Run("Apply", func(t *testing.T) { |
| err := rc.Apply(&productcatalog.FlatFeeRateCard{ |
| RateCardMeta: someMeta.Clone(), |
| }, nil) |
| require.Error(t, err) |
| require.ErrorContains(t, err, "annotations must not be nil") |
| }) |
|
|
| t.Run("Restore", func(t *testing.T) { |
| err := rc.Restore(&productcatalog.FlatFeeRateCard{ |
| RateCardMeta: someMeta.Clone(), |
| }, nil, productcatalog.AddonInstanceTypeSingle) |
| require.Error(t, err) |
| require.ErrorContains(t, err, "annotations must not be nil") |
| }) |
| }) |
|
|
| t.Run("Should error if target RateCard has different price type", func(t *testing.T) { |
| rc := getTestAddonRateCard(&productcatalog.FlatFeeRateCard{ |
| RateCardMeta: someMeta.Clone(), |
| }) |
|
|
| meta := someMeta.Clone() |
| meta.Price = productcatalog.NewPriceFrom(productcatalog.UnitPrice{ |
| Amount: alpacadecimal.NewFromInt(100), |
| }) |
|
|
| t.Run("Apply", func(t *testing.T) { |
| err := rc.Apply(&productcatalog.UsageBasedRateCard{ |
| RateCardMeta: meta, |
| }, models.Annotations{}) |
| require.Error(t, err) |
| require.ErrorContains(t, err, "price type must match") |
| }) |
|
|
| t.Run("Restore", func(t *testing.T) { |
| err := rc.Restore(&productcatalog.UsageBasedRateCard{ |
| RateCardMeta: meta, |
| BillingCadence: datetime.MustParseDuration(t, "P1M"), |
| }, models.Annotations{}, productcatalog.AddonInstanceTypeSingle) |
| require.NoError(t, err) |
| }) |
| }) |
|
|
| t.Run("Should error if target RateCard has different entitlement type", func(t *testing.T) { |
| meta := someMeta.Clone() |
| meta.EntitlementTemplate = productcatalog.NewEntitlementTemplateFrom(productcatalog.BooleanEntitlementTemplate{}) |
|
|
| rc := getTestAddonRateCard(&productcatalog.FlatFeeRateCard{ |
| RateCardMeta: meta, |
| }) |
|
|
| meta = someMeta.Clone() |
| meta.EntitlementTemplate = productcatalog.NewEntitlementTemplateFrom(productcatalog.StaticEntitlementTemplate{}) |
|
|
| t.Run("Apply", func(t *testing.T) { |
| err := rc.Apply(&productcatalog.UsageBasedRateCard{ |
| RateCardMeta: meta, |
| }, models.Annotations{}) |
| require.Error(t, err) |
| require.ErrorContains(t, err, "entitlement template type must match") |
| }) |
|
|
| t.Run("Restore", func(t *testing.T) { |
| err := rc.Restore(&productcatalog.UsageBasedRateCard{ |
| RateCardMeta: meta, |
| }, models.Annotations{}, productcatalog.AddonInstanceTypeSingle) |
| require.Error(t, err) |
| require.ErrorContains(t, err, "entitlement template type must match") |
| }) |
| }) |
|
|
| t.Run("Should error if target RateCard has a different key", func(t *testing.T) { |
| rc := getTestAddonRateCard(&productcatalog.FlatFeeRateCard{ |
| RateCardMeta: someMeta.Clone(), |
| }) |
|
|
| meta := someMeta.Clone() |
| meta.Key = "different-key" |
|
|
| t.Run("Apply", func(t *testing.T) { |
| err := rc.Apply(&productcatalog.FlatFeeRateCard{ |
| RateCardMeta: meta, |
| }, models.Annotations{}) |
| require.Error(t, err) |
| require.ErrorContains(t, err, "key must match") |
| }) |
|
|
| t.Run("Restore", func(t *testing.T) { |
| err := rc.Restore(&productcatalog.FlatFeeRateCard{ |
| RateCardMeta: meta, |
| }, models.Annotations{}, productcatalog.AddonInstanceTypeSingle) |
| require.Error(t, err) |
| require.ErrorContains(t, err, "key must match") |
| }) |
| }) |
|
|
| t.Run("Should error if target Price has different payment term", func(t *testing.T) { |
| meta := someMeta.Clone() |
| meta.Price = productcatalog.NewPriceFrom(productcatalog.FlatPrice{ |
| Amount: alpacadecimal.NewFromInt(100), |
| PaymentTerm: productcatalog.InAdvancePaymentTerm, |
| }) |
|
|
| rc := getTestAddonRateCard(&productcatalog.FlatFeeRateCard{ |
| RateCardMeta: meta.Clone(), |
| }) |
|
|
| meta.Price = productcatalog.NewPriceFrom(productcatalog.FlatPrice{ |
| Amount: alpacadecimal.NewFromInt(50), |
| PaymentTerm: productcatalog.InArrearsPaymentTerm, |
| }) |
|
|
| target := &productcatalog.FlatFeeRateCard{ |
| RateCardMeta: meta.Clone(), |
| } |
|
|
| t.Run("Apply", func(t *testing.T) { |
| err := rc.Apply(target, models.Annotations{}) |
|
|
| require.Error(t, err) |
| require.ErrorContains(t, err, "price payment term must match") |
| }) |
|
|
| t.Run("Restore", func(t *testing.T) { |
| err := rc.Restore(target, models.Annotations{}, productcatalog.AddonInstanceTypeSingle) |
|
|
| require.Error(t, err) |
| require.ErrorContains(t, err, "flat price would yield a negative amount") |
| }) |
| }) |
|
|
| t.Run("Should error on UsageBasedRateCards with different BillingCadence", func(t *testing.T) { |
| meta := someMeta.Clone() |
| meta.Price = nil |
|
|
| |
| rc := getTestAddonRateCard(&productcatalog.UsageBasedRateCard{ |
| RateCardMeta: meta.Clone(), |
| BillingCadence: datetime.MustParseDuration(t, "P2M"), |
| }) |
|
|
| target := &productcatalog.UsageBasedRateCard{ |
| RateCardMeta: meta.Clone(), |
| BillingCadence: datetime.MustParseDuration(t, "P1M"), |
| } |
|
|
| err := rc.Apply(target, models.Annotations{}) |
| require.Error(t, err) |
| require.ErrorContains(t, err, "billing cadence must match") |
| }) |
|
|
| t.Run("Should error on UsageBasedRateCards with divergent UnitConfig", func(t *testing.T) { |
| meta := someMeta.Clone() |
| meta.Price = nil |
|
|
| addonMeta := meta.Clone() |
| addonMeta.UnitConfig = &productcatalog.UnitConfig{ |
| Operation: productcatalog.UnitConfigOperationDivide, |
| ConversionFactor: alpacadecimal.NewFromInt(500), |
| } |
| rc := getTestAddonRateCard(&productcatalog.UsageBasedRateCard{ |
| RateCardMeta: addonMeta, |
| BillingCadence: datetime.MustParseDuration(t, "P1M"), |
| }) |
|
|
| targetMeta := meta.Clone() |
| targetMeta.UnitConfig = &productcatalog.UnitConfig{ |
| Operation: productcatalog.UnitConfigOperationDivide, |
| ConversionFactor: alpacadecimal.NewFromInt(1000), |
| } |
| target := &productcatalog.UsageBasedRateCard{ |
| RateCardMeta: targetMeta, |
| BillingCadence: datetime.MustParseDuration(t, "P1M"), |
| } |
|
|
| err := rc.Apply(target, models.Annotations{}) |
| require.Error(t, err) |
| require.ErrorContains(t, err, "unit config must match") |
| }) |
| } |
|
|
| func TestExtendApply(t *testing.T) { |
| someMeta := productcatalog.RateCardMeta{ |
| Name: "Test Addon Rate Card 4", |
| Description: lo.ToPtr("Test Addon Rate Card 4 Description"), |
| Key: subscriptiontestutils.ExampleFeatureKey, |
| FeatureKey: lo.ToPtr(subscriptiontestutils.ExampleFeatureKey), |
| Price: productcatalog.NewPriceFrom(productcatalog.FlatPrice{ |
| Amount: alpacadecimal.NewFromInt(100), |
| PaymentTerm: productcatalog.InAdvancePaymentTerm, |
| }), |
| } |
|
|
| t.Run("Should not change target RateCard if addon RateCard has no entitlement & no price", func(t *testing.T) { |
| meta := someMeta.Clone() |
| meta.EntitlementTemplate = nil |
| meta.Price = nil |
|
|
| rc := getTestAddonRateCard(&productcatalog.FlatFeeRateCard{ |
| RateCardMeta: meta, |
| }) |
|
|
| target := &productcatalog.FlatFeeRateCard{ |
| RateCardMeta: someMeta.Clone(), |
| } |
|
|
| targetClone := target.Clone() |
|
|
| err := rc.Apply(target, models.Annotations{}) |
|
|
| require.NoError(t, err) |
| require.Equal(t, targetClone, target) |
| }) |
|
|
| t.Run("Should keep FlatPrice of target if addon has no price", func(t *testing.T) { |
| meta := someMeta.Clone() |
| meta.Price = nil |
|
|
| rc := getTestAddonRateCard(&productcatalog.FlatFeeRateCard{ |
| RateCardMeta: meta.Clone(), |
| }) |
|
|
| meta.Price = productcatalog.NewPriceFrom(productcatalog.FlatPrice{ |
| Amount: alpacadecimal.NewFromInt(100), |
| PaymentTerm: productcatalog.InAdvancePaymentTerm, |
| }) |
|
|
| target := &productcatalog.FlatFeeRateCard{ |
| RateCardMeta: meta.Clone(), |
| } |
|
|
| err := rc.Apply(target, models.Annotations{}) |
|
|
| require.NoError(t, err) |
| fp, err := target.AsMeta().Price.AsFlat() |
| require.NoError(t, err) |
| require.Equal(t, alpacadecimal.NewFromInt(100), fp.Amount) |
| }) |
|
|
| t.Run("Should update target Price when FlatPrice", func(t *testing.T) { |
| meta := someMeta.Clone() |
| meta.Price = productcatalog.NewPriceFrom(productcatalog.FlatPrice{ |
| Amount: alpacadecimal.NewFromInt(100), |
| PaymentTerm: productcatalog.InAdvancePaymentTerm, |
| }) |
|
|
| rc := getTestAddonRateCard(&productcatalog.FlatFeeRateCard{ |
| RateCardMeta: meta.Clone(), |
| }) |
|
|
| meta.Price = productcatalog.NewPriceFrom(productcatalog.FlatPrice{ |
| Amount: alpacadecimal.NewFromInt(50), |
| PaymentTerm: productcatalog.InAdvancePaymentTerm, |
| }) |
|
|
| target := &productcatalog.FlatFeeRateCard{ |
| RateCardMeta: meta.Clone(), |
| } |
|
|
| err := rc.Apply(target, models.Annotations{}) |
|
|
| require.NoError(t, err) |
| fp, err := target.AsMeta().Price.AsFlat() |
| require.NoError(t, err) |
| require.Equal(t, alpacadecimal.NewFromInt(150), fp.Amount) |
| }) |
|
|
| t.Run("Should add addon price to target", func(t *testing.T) { |
| meta := someMeta.Clone() |
| meta.Price = productcatalog.NewPriceFrom(productcatalog.FlatPrice{ |
| Amount: alpacadecimal.NewFromInt(100), |
| PaymentTerm: productcatalog.InAdvancePaymentTerm, |
| }) |
|
|
| rc := getTestAddonRateCard(&productcatalog.FlatFeeRateCard{ |
| RateCardMeta: meta.Clone(), |
| }) |
|
|
| meta.Price = nil |
|
|
| target := &productcatalog.FlatFeeRateCard{ |
| RateCardMeta: meta.Clone(), |
| } |
|
|
| err := rc.Apply(target, models.Annotations{}) |
|
|
| require.NoError(t, err) |
| fp, err := target.AsMeta().Price.AsFlat() |
| require.NoError(t, err) |
| require.Equal(t, fp.Amount, alpacadecimal.NewFromInt(100)) |
| }) |
|
|
| t.Run("Should add addon entitlement to target", func(t *testing.T) { |
| meta := someMeta.Clone() |
| meta.EntitlementTemplate = productcatalog.NewEntitlementTemplateFrom(productcatalog.BooleanEntitlementTemplate{}) |
|
|
| rc := getTestAddonRateCard(&productcatalog.FlatFeeRateCard{ |
| RateCardMeta: meta.Clone(), |
| }) |
|
|
| meta.EntitlementTemplate = nil |
|
|
| target := &productcatalog.FlatFeeRateCard{ |
| RateCardMeta: meta.Clone(), |
| } |
|
|
| err := rc.Apply(target, models.Annotations{}) |
|
|
| require.NoError(t, err) |
| require.Equal(t, string(entitlement.EntitlementTypeBoolean), string(target.AsMeta().EntitlementTemplate.Type())) |
| }) |
|
|
| t.Run("Should keep BooleanEntitlement of target if addon has no entitlement", func(t *testing.T) { |
| meta := someMeta.Clone() |
| meta.EntitlementTemplate = nil |
|
|
| rc := getTestAddonRateCard(&productcatalog.FlatFeeRateCard{ |
| RateCardMeta: meta.Clone(), |
| }) |
|
|
| meta.EntitlementTemplate = productcatalog.NewEntitlementTemplateFrom(productcatalog.BooleanEntitlementTemplate{}) |
|
|
| target := &productcatalog.FlatFeeRateCard{ |
| RateCardMeta: meta.Clone(), |
| } |
|
|
| err := rc.Apply(target, models.Annotations{}) |
|
|
| require.NoError(t, err) |
| require.Equal(t, string(entitlement.EntitlementTypeBoolean), string(target.AsMeta().EntitlementTemplate.Type())) |
| }) |
|
|
| t.Run("Should set MeteredEntitlement of addon to target", func(t *testing.T) { |
| meta := someMeta.Clone() |
| meta.EntitlementTemplate = productcatalog.NewEntitlementTemplateFrom(productcatalog.MeteredEntitlementTemplate{ |
| IssueAfterReset: lo.ToPtr(100.0), |
| UsagePeriod: datetime.MustParseDuration(t, "P1M"), |
| }) |
|
|
| rc := getTestAddonRateCard(&productcatalog.FlatFeeRateCard{ |
| RateCardMeta: meta.Clone(), |
| }) |
|
|
| meta.EntitlementTemplate = nil |
| target := &productcatalog.FlatFeeRateCard{ |
| RateCardMeta: meta.Clone(), |
| } |
|
|
| err := rc.Apply(target, models.Annotations{}) |
|
|
| require.NoError(t, err) |
| me, err := target.AsMeta().EntitlementTemplate.AsMetered() |
| require.NoError(t, err) |
| require.NotNil(t, me.IsSoftLimit) |
| require.Equal(t, 100.0, *me.IssueAfterReset) |
| }) |
|
|
| t.Run("Should combine issueAfterReset for metered entitlements treating nil as 0", func(t *testing.T) { |
| meta := someMeta.Clone() |
| meta.EntitlementTemplate = productcatalog.NewEntitlementTemplateFrom(productcatalog.MeteredEntitlementTemplate{ |
| IssueAfterReset: lo.ToPtr(100.0), |
| UsagePeriod: datetime.MustParseDuration(t, "P1M"), |
| }) |
|
|
| rc := getTestAddonRateCard(&productcatalog.FlatFeeRateCard{ |
| RateCardMeta: meta.Clone(), |
| }) |
|
|
| meta.EntitlementTemplate = nil |
|
|
| target := &productcatalog.FlatFeeRateCard{ |
| RateCardMeta: meta.Clone(), |
| } |
|
|
| err := rc.Apply(target, models.Annotations{}) |
|
|
| require.NoError(t, err) |
| me, err := target.AsMeta().EntitlementTemplate.AsMetered() |
| require.NoError(t, err) |
| require.NotNil(t, me.IssueAfterReset) |
| require.Equal(t, 100.0, *me.IssueAfterReset) |
|
|
| err = rc.Apply(target, models.Annotations{}) |
|
|
| require.NoError(t, err) |
| me, err = target.AsMeta().EntitlementTemplate.AsMetered() |
| require.NoError(t, err) |
| require.NotNil(t, me.IssueAfterReset) |
| require.Equal(t, 200.0, *me.IssueAfterReset) |
| }) |
|
|
| t.Run("Should add Usage Discounts from UsageBasedRateCard to target with UnitPrice", func(t *testing.T) { |
| meta := someMeta.Clone() |
| meta.Price = nil |
| meta.Discounts = productcatalog.Discounts{ |
| Usage: &productcatalog.UsageDiscount{ |
| Quantity: alpacadecimal.NewFromInt(100), |
| }, |
| } |
|
|
| rc := getTestAddonRateCard(&productcatalog.UsageBasedRateCard{ |
| RateCardMeta: meta.Clone(), |
| BillingCadence: datetime.MustParseDuration(t, "P1M"), |
| }) |
|
|
| meta.Discounts = productcatalog.Discounts{} |
| meta.Price = productcatalog.NewPriceFrom(productcatalog.UnitPrice{ |
| Amount: alpacadecimal.NewFromInt(1), |
| }) |
|
|
| target := &productcatalog.UsageBasedRateCard{ |
| RateCardMeta: meta.Clone(), |
| BillingCadence: datetime.MustParseDuration(t, "P1M"), |
| } |
|
|
| err := rc.Apply(target, models.Annotations{}) |
|
|
| require.NoError(t, err) |
| require.Equal(t, alpacadecimal.NewFromInt(100).InexactFloat64(), target.AsMeta().Discounts.Usage.Quantity.InexactFloat64()) |
| }) |
| } |
|
|
| func TestExtendRestore(t *testing.T) { |
| someMeta := productcatalog.RateCardMeta{ |
| Name: "Test Addon Rate Card 4", |
| Description: lo.ToPtr("Test Addon Rate Card 4 Description"), |
| Key: subscriptiontestutils.ExampleFeatureKey, |
| FeatureKey: lo.ToPtr(subscriptiontestutils.ExampleFeatureKey), |
| Price: productcatalog.NewPriceFrom(productcatalog.FlatPrice{ |
| Amount: alpacadecimal.NewFromInt(100), |
| PaymentTerm: productcatalog.InAdvancePaymentTerm, |
| }), |
| } |
|
|
| t.Run("Should not change target RateCard if addon RateCard has no entitlement & no price", func(t *testing.T) { |
| meta := someMeta.Clone() |
| meta.EntitlementTemplate = nil |
| meta.Price = nil |
|
|
| rc := getTestAddonRateCard(&productcatalog.FlatFeeRateCard{ |
| RateCardMeta: meta, |
| }) |
|
|
| target := &productcatalog.FlatFeeRateCard{ |
| RateCardMeta: someMeta.Clone(), |
| } |
|
|
| targetClone := target.Clone() |
|
|
| err := rc.Restore(target, models.Annotations{}, productcatalog.AddonInstanceTypeSingle) |
|
|
| require.NoError(t, err) |
| require.Equal(t, targetClone, target) |
| }) |
|
|
| t.Run("Should error if target Price is nil but addon has a flat price", func(t *testing.T) { |
| meta := someMeta.Clone() |
| meta.EntitlementTemplate = nil |
|
|
| rc := getTestAddonRateCard(&productcatalog.FlatFeeRateCard{ |
| RateCardMeta: meta.Clone(), |
| }) |
|
|
| meta.Price = nil |
|
|
| target := &productcatalog.FlatFeeRateCard{ |
| RateCardMeta: meta.Clone(), |
| } |
|
|
| err := rc.Restore(target, models.Annotations{}, productcatalog.AddonInstanceTypeSingle) |
|
|
| require.Error(t, err) |
| require.ErrorContains(t, err, "target price is nil, cannot restore price without addon") |
| }) |
|
|
| t.Run("Should deduct flat price of addon from target", func(t *testing.T) { |
| meta := someMeta.Clone() |
| meta.EntitlementTemplate = nil |
| meta.Price = productcatalog.NewPriceFrom(productcatalog.FlatPrice{ |
| Amount: alpacadecimal.NewFromInt(10), |
| PaymentTerm: productcatalog.InAdvancePaymentTerm, |
| }) |
|
|
| rc := getTestAddonRateCard(&productcatalog.FlatFeeRateCard{ |
| RateCardMeta: meta.Clone(), |
| }) |
|
|
| target := &productcatalog.FlatFeeRateCard{ |
| RateCardMeta: someMeta.Clone(), |
| } |
|
|
| err := rc.Restore(target, models.Annotations{}, productcatalog.AddonInstanceTypeSingle) |
|
|
| require.NoError(t, err) |
| fp, err := target.AsMeta().Price.AsFlat() |
| require.NoError(t, err) |
| require.Equal(t, alpacadecimal.NewFromInt(90), fp.Amount) |
| }) |
|
|
| t.Run("Should allow 0 resulting flat price", func(t *testing.T) { |
| meta := someMeta.Clone() |
| meta.EntitlementTemplate = nil |
|
|
| rc := getTestAddonRateCard(&productcatalog.FlatFeeRateCard{ |
| RateCardMeta: meta.Clone(), |
| }) |
|
|
| target := &productcatalog.FlatFeeRateCard{ |
| RateCardMeta: meta.Clone(), |
| } |
|
|
| err := rc.Restore(target, models.Annotations{}, productcatalog.AddonInstanceTypeSingle) |
|
|
| require.NoError(t, err) |
| fp, err := target.AsMeta().Price.AsFlat() |
| require.NoError(t, err) |
| require.Equal(t, alpacadecimal.NewFromInt(0), fp.Amount) |
| }) |
|
|
| t.Run("Should error if target has no entitlement but addon has a metered entitlement", func(t *testing.T) { |
| meta := someMeta.Clone() |
| meta.EntitlementTemplate = productcatalog.NewEntitlementTemplateFrom(productcatalog.MeteredEntitlementTemplate{ |
| IssueAfterReset: lo.ToPtr(100.0), |
| UsagePeriod: datetime.MustParseDuration(t, "P1M"), |
| }) |
|
|
| rc := getTestAddonRateCard(&productcatalog.FlatFeeRateCard{ |
| RateCardMeta: meta.Clone(), |
| }) |
|
|
| meta.EntitlementTemplate = nil |
|
|
| target := &productcatalog.FlatFeeRateCard{ |
| RateCardMeta: meta.Clone(), |
| } |
|
|
| err := rc.Restore(target, models.Annotations{}, productcatalog.AddonInstanceTypeSingle) |
|
|
| require.Error(t, err) |
| require.ErrorContains(t, err, "target entitlement template is nil, cannot restore entitlement template without addon") |
| }) |
|
|
| t.Run("Should error if target has boolean entitlement but no annotation", func(t *testing.T) { |
| meta := someMeta.Clone() |
| meta.EntitlementTemplate = productcatalog.NewEntitlementTemplateFrom(productcatalog.BooleanEntitlementTemplate{}) |
|
|
| rc := getTestAddonRateCard(&productcatalog.FlatFeeRateCard{ |
| RateCardMeta: meta.Clone(), |
| }) |
|
|
| target := &productcatalog.FlatFeeRateCard{ |
| RateCardMeta: meta.Clone(), |
| } |
|
|
| err := rc.Restore(target, models.Annotations{}, productcatalog.AddonInstanceTypeSingle) |
|
|
| require.Error(t, err) |
| require.ErrorContains(t, err, "target doesn't have boolean entitlement count annotation while has a boolean entitlement template") |
| }) |
|
|
| t.Run("Should error if target has boolean entitlement but annotation is invalid (negative count)", func(t *testing.T) { |
| meta := someMeta.Clone() |
| meta.EntitlementTemplate = productcatalog.NewEntitlementTemplateFrom(productcatalog.BooleanEntitlementTemplate{}) |
|
|
| rc := getTestAddonRateCard(&productcatalog.FlatFeeRateCard{ |
| RateCardMeta: meta.Clone(), |
| }) |
|
|
| target := &productcatalog.FlatFeeRateCard{ |
| RateCardMeta: meta.Clone(), |
| } |
|
|
| ann := models.Annotations{} |
| if _, err := subscription.AnnotationParser.SetBooleanEntitlementCount(ann, -1); err != nil { |
| t.Fatalf("failed to set boolean entitlement count: %s", err) |
| } |
|
|
| err := rc.Restore(target, ann, productcatalog.AddonInstanceTypeSingle) |
|
|
| require.Error(t, err) |
| require.ErrorContains(t, err, "received invalid entitlement count annotation value: -1") |
| }) |
|
|
| t.Run("Should error if target has boolean entitlement with annotation value of 0", func(t *testing.T) { |
| meta := someMeta.Clone() |
| meta.EntitlementTemplate = productcatalog.NewEntitlementTemplateFrom(productcatalog.BooleanEntitlementTemplate{}) |
|
|
| rc := getTestAddonRateCard(&productcatalog.FlatFeeRateCard{ |
| RateCardMeta: meta.Clone(), |
| }) |
|
|
| target := &productcatalog.FlatFeeRateCard{ |
| RateCardMeta: meta.Clone(), |
| } |
|
|
| ann := models.Annotations{} |
| if _, err := subscription.AnnotationParser.SetBooleanEntitlementCount(ann, 0); err != nil { |
| t.Fatalf("failed to set boolean entitlement count: %s", err) |
| } |
|
|
| err := rc.Restore(target, ann, productcatalog.AddonInstanceTypeSingle) |
|
|
| require.Error(t, err) |
| require.ErrorContains(t, err, "target doesn't have boolean entitlement count annotation while has a boolean entitlement template") |
| }) |
|
|
| t.Run("Should remove boolean entitlement if annotation value falls to 0", func(t *testing.T) { |
| meta := someMeta.Clone() |
| meta.EntitlementTemplate = productcatalog.NewEntitlementTemplateFrom(productcatalog.BooleanEntitlementTemplate{}) |
|
|
| rc := getTestAddonRateCard(&productcatalog.FlatFeeRateCard{ |
| RateCardMeta: meta.Clone(), |
| }) |
|
|
| target := &productcatalog.FlatFeeRateCard{ |
| RateCardMeta: meta.Clone(), |
| } |
|
|
| ann := models.Annotations{} |
| if _, err := subscription.AnnotationParser.SetBooleanEntitlementCount(ann, 1); err != nil { |
| t.Fatalf("failed to set boolean entitlement count: %s", err) |
| } |
|
|
| err := rc.Restore(target, ann, productcatalog.AddonInstanceTypeSingle) |
|
|
| require.NoError(t, err) |
| require.Nil(t, target.EntitlementTemplate) |
| }) |
|
|
| t.Run("Should decrement boolean entitlement count", func(t *testing.T) { |
| meta := someMeta.Clone() |
| meta.EntitlementTemplate = productcatalog.NewEntitlementTemplateFrom(productcatalog.BooleanEntitlementTemplate{}) |
|
|
| rc := getTestAddonRateCard(&productcatalog.FlatFeeRateCard{ |
| RateCardMeta: meta.Clone(), |
| }) |
|
|
| target := &productcatalog.FlatFeeRateCard{ |
| RateCardMeta: meta.Clone(), |
| } |
|
|
| ann := models.Annotations{} |
| if _, err := subscription.AnnotationParser.SetBooleanEntitlementCount(ann, 2); err != nil { |
| t.Fatalf("failed to set boolean entitlement count: %s", err) |
| } |
|
|
| err := rc.Restore(target, ann, productcatalog.AddonInstanceTypeSingle) |
|
|
| require.NoError(t, err) |
| require.NotNil(t, target.EntitlementTemplate) |
| require.Equal(t, 1, subscription.AnnotationParser.GetBooleanEntitlementCount(ann)) |
| }) |
|
|
| t.Run("Should deduct issueAfterReset of addon from target", func(t *testing.T) { |
| meta := someMeta.Clone() |
| meta.EntitlementTemplate = productcatalog.NewEntitlementTemplateFrom(productcatalog.MeteredEntitlementTemplate{ |
| IssueAfterReset: lo.ToPtr(10.0), |
| UsagePeriod: datetime.MustParseDuration(t, "P1M"), |
| }) |
|
|
| rc := getTestAddonRateCard(&productcatalog.FlatFeeRateCard{ |
| RateCardMeta: meta.Clone(), |
| }) |
|
|
| meta.EntitlementTemplate = productcatalog.NewEntitlementTemplateFrom(productcatalog.MeteredEntitlementTemplate{ |
| IssueAfterReset: lo.ToPtr(100.0), |
| UsagePeriod: datetime.MustParseDuration(t, "P1M"), |
| }) |
|
|
| target := &productcatalog.FlatFeeRateCard{ |
| RateCardMeta: meta.Clone(), |
| } |
|
|
| err := rc.Restore(target, models.Annotations{}, productcatalog.AddonInstanceTypeSingle) |
|
|
| require.NoError(t, err) |
| me, err := target.AsMeta().EntitlementTemplate.AsMetered() |
| require.NoError(t, err) |
| require.Equal(t, 90.0, *me.IssueAfterReset) |
| }) |
|
|
| t.Run("Should not allow negative issueAfterReset", func(t *testing.T) { |
| meta := someMeta.Clone() |
| meta.EntitlementTemplate = productcatalog.NewEntitlementTemplateFrom(productcatalog.MeteredEntitlementTemplate{ |
| IssueAfterReset: lo.ToPtr(100.0), |
| UsagePeriod: datetime.MustParseDuration(t, "P1M"), |
| }) |
|
|
| rc := getTestAddonRateCard(&productcatalog.FlatFeeRateCard{ |
| RateCardMeta: meta.Clone(), |
| }) |
|
|
| meta.EntitlementTemplate = productcatalog.NewEntitlementTemplateFrom(productcatalog.MeteredEntitlementTemplate{ |
| IssueAfterReset: lo.ToPtr(50.0), |
| UsagePeriod: datetime.MustParseDuration(t, "P1M"), |
| }) |
|
|
| target := &productcatalog.FlatFeeRateCard{ |
| RateCardMeta: meta.Clone(), |
| } |
|
|
| err := rc.Restore(target, models.Annotations{}, productcatalog.AddonInstanceTypeSingle) |
|
|
| require.Error(t, err) |
| require.ErrorContains(t, err, "restoring entitlement template would yield a negative issue after reset: 50 - 100 = -50") |
| }) |
|
|
| t.Run("Should allow 0 resulting issueAfterReset", func(t *testing.T) { |
| meta := someMeta.Clone() |
| meta.EntitlementTemplate = productcatalog.NewEntitlementTemplateFrom(productcatalog.MeteredEntitlementTemplate{ |
| IssueAfterReset: lo.ToPtr(100.0), |
| UsagePeriod: datetime.MustParseDuration(t, "P1M"), |
| }) |
|
|
| rc := getTestAddonRateCard(&productcatalog.FlatFeeRateCard{ |
| RateCardMeta: meta.Clone(), |
| }) |
|
|
| target := &productcatalog.FlatFeeRateCard{ |
| RateCardMeta: meta.Clone(), |
| } |
|
|
| err := rc.Restore(target, models.Annotations{}, productcatalog.AddonInstanceTypeSingle) |
|
|
| require.NoError(t, err) |
| me, err := target.AsMeta().EntitlementTemplate.AsMetered() |
| require.NoError(t, err) |
| require.Equal(t, 0.0, *me.IssueAfterReset) |
| }) |
|
|
| t.Run("Should error if trying to restore Usage Discount when one is not present on target", func(t *testing.T) { |
| meta := someMeta.Clone() |
| meta.Price = nil |
| meta.Discounts = productcatalog.Discounts{ |
| Usage: &productcatalog.UsageDiscount{ |
| Quantity: alpacadecimal.NewFromInt(100), |
| }, |
| } |
|
|
| rc := getTestAddonRateCard(&productcatalog.UsageBasedRateCard{ |
| RateCardMeta: meta.Clone(), |
| BillingCadence: datetime.MustParseDuration(t, "P1M"), |
| }) |
|
|
| meta.Discounts = productcatalog.Discounts{} |
| meta.Price = productcatalog.NewPriceFrom(productcatalog.UnitPrice{ |
| Amount: alpacadecimal.NewFromInt(1), |
| }) |
|
|
| target := &productcatalog.UsageBasedRateCard{ |
| RateCardMeta: meta.Clone(), |
| BillingCadence: datetime.MustParseDuration(t, "P1M"), |
| } |
|
|
| err := rc.Restore(target, models.Annotations{}, productcatalog.AddonInstanceTypeSingle) |
|
|
| require.Error(t, err) |
| require.ErrorContains(t, err, "target doesn't have usage discount while addon has a usage discount") |
| }) |
|
|
| t.Run("Should error if trying to restore Usage Discount greater than target's discount", func(t *testing.T) { |
| meta := someMeta.Clone() |
| meta.Price = nil |
| meta.Discounts = productcatalog.Discounts{ |
| Usage: &productcatalog.UsageDiscount{ |
| Quantity: alpacadecimal.NewFromInt(100), |
| }, |
| } |
|
|
| rc := getTestAddonRateCard(&productcatalog.UsageBasedRateCard{ |
| RateCardMeta: meta.Clone(), |
| BillingCadence: datetime.MustParseDuration(t, "P1M"), |
| }) |
|
|
| meta.Discounts = productcatalog.Discounts{ |
| Usage: &productcatalog.UsageDiscount{ |
| Quantity: alpacadecimal.NewFromInt(50), |
| }, |
| } |
| meta.Price = productcatalog.NewPriceFrom(productcatalog.UnitPrice{ |
| Amount: alpacadecimal.NewFromInt(1), |
| }) |
|
|
| target := &productcatalog.UsageBasedRateCard{ |
| RateCardMeta: meta.Clone(), |
| BillingCadence: datetime.MustParseDuration(t, "P1M"), |
| } |
|
|
| err := rc.Restore(target, models.Annotations{}, productcatalog.AddonInstanceTypeSingle) |
|
|
| require.Error(t, err) |
| require.ErrorContains(t, err, fmt.Sprintf("target has %.0f usage discount which is less than addon's %.0f", 50.0, 100.0)) |
| }) |
|
|
| t.Run("Should restore Usage Discounts from UsageBasedRateCard to target with UnitPrice", func(t *testing.T) { |
| meta := someMeta.Clone() |
| meta.Price = nil |
| meta.Discounts = productcatalog.Discounts{ |
| Usage: &productcatalog.UsageDiscount{ |
| Quantity: alpacadecimal.NewFromInt(100), |
| }, |
| } |
|
|
| rc := getTestAddonRateCard(&productcatalog.UsageBasedRateCard{ |
| RateCardMeta: meta.Clone(), |
| BillingCadence: datetime.MustParseDuration(t, "P1M"), |
| }) |
|
|
| meta.Discounts = productcatalog.Discounts{ |
| Usage: &productcatalog.UsageDiscount{ |
| Quantity: alpacadecimal.NewFromInt(150), |
| }, |
| } |
| meta.Price = productcatalog.NewPriceFrom(productcatalog.UnitPrice{ |
| Amount: alpacadecimal.NewFromInt(1), |
| }) |
|
|
| target := &productcatalog.UsageBasedRateCard{ |
| RateCardMeta: meta.Clone(), |
| BillingCadence: datetime.MustParseDuration(t, "P1M"), |
| } |
|
|
| err := rc.Restore(target, models.Annotations{}, productcatalog.AddonInstanceTypeSingle) |
|
|
| require.NoError(t, err) |
| require.Equal(t, alpacadecimal.NewFromInt(50).InexactFloat64(), target.AsMeta().Discounts.Usage.Quantity.InexactFloat64()) |
| }) |
| } |
|
|
| var _ productcatalog.RateCard = nonPointerRateCard{} |
|
|
| type nonPointerRateCard struct{} |
|
|
| func (n nonPointerRateCard) HasFeature() bool { |
| return false |
| } |
|
|
| func (n nonPointerRateCard) GetFeatureID() *string { |
| return nil |
| } |
|
|
| func (n nonPointerRateCard) GetFeatureKey() *string { |
| return nil |
| } |
|
|
| func (n nonPointerRateCard) SetFeature(_, _ *string) { |
| } |
|
|
| func (n nonPointerRateCard) IsBillable() bool { |
| return true |
| } |
|
|
| func (n nonPointerRateCard) AsMeta() productcatalog.RateCardMeta { |
| return productcatalog.RateCardMeta{} |
| } |
|
|
| func (n nonPointerRateCard) ChangeMeta(func(m productcatalog.RateCardMeta) (productcatalog.RateCardMeta, error)) error { |
| return nil |
| } |
|
|
| func (n nonPointerRateCard) Clone() productcatalog.RateCard { |
| return n |
| } |
|
|
| func (n nonPointerRateCard) Compatible(productcatalog.RateCard) error { |
| return nil |
| } |
|
|
| func (n nonPointerRateCard) Equal(productcatalog.RateCard) bool { |
| return false |
| } |
|
|
| func (n nonPointerRateCard) Validate() error { |
| return nil |
| } |
|
|
| func (n nonPointerRateCard) GetBillingCadence() *datetime.ISODuration { |
| return nil |
| } |
|
|
| func (n nonPointerRateCard) Key() string { |
| return subscriptiontestutils.ExampleFeatureKey |
| } |
|
|
| func (n nonPointerRateCard) Merge(productcatalog.RateCard) error { |
| return nil |
| } |
|
|
| func (n nonPointerRateCard) Type() productcatalog.RateCardType { |
| return productcatalog.FlatFeeRateCardType |
| } |
|
|
| func getTestAddonRateCard(rc productcatalog.RateCard) subscriptionaddon.SubscriptionAddonRateCard { |
| return subscriptionaddon.SubscriptionAddonRateCard{ |
| AddonRateCard: addon.RateCard{ |
| RateCard: rc, |
| }, |
| } |
| } |
|
|