Leon4gr45's picture
Upload folder using huggingface_hub (part 7)
16cdcb7 verified
Raw
History Blame Contribute Delete
28.2 kB
package service_test
import (
"context"
"testing"
"time"
"github.com/alpacahq/alpacadecimal"
"github.com/invopop/gobl/currency"
"github.com/samber/lo"
"github.com/stretchr/testify/require"
"github.com/openmeterio/openmeter/openmeter/customer"
"github.com/openmeterio/openmeter/openmeter/productcatalog"
"github.com/openmeterio/openmeter/openmeter/productcatalog/plan"
"github.com/openmeterio/openmeter/openmeter/productcatalog/planaddon"
plansubscription "github.com/openmeterio/openmeter/openmeter/productcatalog/subscription"
"github.com/openmeterio/openmeter/openmeter/subscription"
subscriptionaddon "github.com/openmeterio/openmeter/openmeter/subscription/addon"
addondiff "github.com/openmeterio/openmeter/openmeter/subscription/addon/diff"
"github.com/openmeterio/openmeter/openmeter/subscription/patch"
subscriptiontestutils "github.com/openmeterio/openmeter/openmeter/subscription/testutils"
subscriptionworkflow "github.com/openmeterio/openmeter/openmeter/subscription/workflow"
"github.com/openmeterio/openmeter/openmeter/testutils"
"github.com/openmeterio/openmeter/pkg/clock"
"github.com/openmeterio/openmeter/pkg/currencyx"
"github.com/openmeterio/openmeter/pkg/datetime"
"github.com/openmeterio/openmeter/pkg/models"
)
func TestAddAddon(t *testing.T) {
now := testutils.GetRFC3339Time(t, "2025-04-01T00:00:00Z")
runWithDeps := func(fn func(t *testing.T, deps subscriptiontestutils.SubscriptionDependencies)) func(t *testing.T) {
return func(t *testing.T) {
// Freeze just past the effective boundary to avoid Postgres timestamp
// truncation turning "now" into a boundary value that reads as
// not-yet-active or already-past in follow-up comparisons.
clock.FreezeTime(now.Add(time.Millisecond))
defer func() {
clock.UnFreeze()
clock.ResetTime()
}()
dbDeps := subscriptiontestutils.SetupDBDeps(t)
defer dbDeps.Cleanup(t)
deps := subscriptiontestutils.NewService(t, dbDeps)
fn(t, deps)
}
}
t.Run("Should error on invalid input", runWithDeps(func(t *testing.T, deps subscriptiontestutils.SubscriptionDependencies) {
p, add := subscriptiontestutils.CreatePlanWithAddon(
t,
deps,
subscriptiontestutils.GetExamplePlanInput(t),
subscriptiontestutils.BuildAddonForTesting(t,
productcatalog.EffectivePeriod{
EffectiveFrom: &now,
EffectiveTo: nil,
},
productcatalog.AddonInstanceTypeSingle,
subscriptiontestutils.ExampleAddonRateCard2.Clone(),
subscriptiontestutils.ExampleAddonRateCard4.Clone(),
),
)
subView := subscriptiontestutils.CreateSubscriptionFromPlan(t, &deps, p, now)
addonInp := subscriptionworkflow.AddAddonWorkflowInput{
AddonID: add.ID,
InitialQuantity: 0,
Timing: subscription.Timing{
Custom: &now,
},
}
expectedErr := addonInp.Validate()
require.NotNil(t, expectedErr)
_, _, err := deps.WorkflowService.AddAddon(context.Background(), subView.Subscription.NamespacedID, addonInp)
require.Error(t, err)
require.True(t, models.IsGenericValidationError(err))
require.ErrorContains(t, err, expectedErr.Error())
}))
t.Run("Should error if the subscription is inactive or in wrong state", runWithDeps(func(t *testing.T, deps subscriptiontestutils.SubscriptionDependencies) {
p, add := subscriptiontestutils.CreatePlanWithAddon(
t,
deps,
subscriptiontestutils.GetExamplePlanInput(t),
subscriptiontestutils.BuildAddonForTesting(t,
productcatalog.EffectivePeriod{
EffectiveFrom: &now,
EffectiveTo: nil,
},
productcatalog.AddonInstanceTypeSingle,
subscriptiontestutils.ExampleAddonRateCard2.Clone(), // This will add a new item
subscriptiontestutils.ExampleAddonRateCard4.Clone(), // This will extend existing items
),
)
// Let's create a subscription from the plan
subView := subscriptiontestutils.CreateSubscriptionFromPlan(t, &deps, p, now)
// Let's cancel the subscription
_, err := deps.SubscriptionService.Cancel(context.Background(), subView.Subscription.NamespacedID, subscription.Timing{
Enum: lo.ToPtr(subscription.TimingNextBillingCycle),
})
require.NoError(t, err)
// Let's add an addon to the subscription
addonInp := subscriptionworkflow.AddAddonWorkflowInput{
AddonID: add.ID,
InitialQuantity: 1,
Timing: subscription.Timing{
Custom: &now,
},
}
_, _, err = deps.WorkflowService.AddAddon(context.Background(), subView.Subscription.NamespacedID, addonInp)
require.Error(t, err)
require.ErrorAs(t, err, lo.ToPtr(&models.GenericForbiddenError{}))
require.True(t, models.IsGenericForbiddenError(err))
require.ErrorContains(t, err, "state canceled not allowed")
}))
t.Run("Should add a new addon to a subscription that already has a different addon", runWithDeps(func(t *testing.T, deps subscriptiontestutils.SubscriptionDependencies) {
_ = deps.FeatureConnector.CreateExampleFeatures(t, deps.ExampleMeterID)
// Let's create a plan
p, err := deps.PlanService.CreatePlan(context.Background(), subscriptiontestutils.BuildTestPlanInput(t).
AddPhase(nil, subscriptiontestutils.ExampleRateCard1.Clone()).
Build())
require.Nil(t, err)
require.NotNil(t, p)
// Let's create two addons that are compatible
addonInp := subscriptiontestutils.BuildAddonForTesting(t,
productcatalog.EffectivePeriod{
EffectiveFrom: &now,
EffectiveTo: nil,
},
productcatalog.AddonInstanceTypeSingle,
subscriptiontestutils.ExampleAddonRateCard5.Clone(),
)
add1 := deps.AddonService.CreateTestAddon(t, addonInp)
addonInp.Key = "some-new-key"
add2 := deps.AddonService.CreateTestAddon(t, addonInp)
// Let's link both addons to the plan
_, err = deps.PlanAddonService.CreatePlanAddon(context.Background(), planaddon.CreatePlanAddonInput{
NamespacedModel: models.NamespacedModel{
Namespace: subscriptiontestutils.ExampleNamespace,
},
PlanID: p.ID,
AddonID: add1.ID,
FromPlanPhase: p.Phases[0].Key,
})
require.Nil(t, err, "received error: %s", err)
_, err = deps.PlanAddonService.CreatePlanAddon(context.Background(), planaddon.CreatePlanAddonInput{
NamespacedModel: models.NamespacedModel{
Namespace: subscriptiontestutils.ExampleNamespace,
},
PlanID: p.ID,
AddonID: add2.ID,
FromPlanPhase: p.Phases[0].Key,
})
require.Nil(t, err, "received error: %s", err)
// Let's publish the plan
p, err = deps.PlanService.PublishPlan(context.Background(), plan.PublishPlanInput{
NamespacedID: p.NamespacedID,
EffectivePeriod: productcatalog.EffectivePeriod{
EffectiveFrom: lo.ToPtr(clock.Now()),
EffectiveTo: lo.ToPtr(testutils.GetRFC3339Time(t, "2099-01-01T00:00:00Z")),
},
})
require.Nil(t, err, "received error: %s", err)
// Let's create a subscription from the plan
subView := subscriptiontestutils.CreateSubscriptionFromPlan(t, &deps, &plansubscription.Plan{
Plan: p.AsProductCatalogPlan(),
Ref: &p.NamespacedID,
}, now)
// Let's add the first addon
_, _, err = deps.WorkflowService.AddAddon(context.Background(), subView.Subscription.NamespacedID, subscriptionworkflow.AddAddonWorkflowInput{
AddonID: add1.ID,
InitialQuantity: 1,
Timing: subscription.Timing{
Custom: &now,
},
})
require.NoError(t, err)
// Now let's add the second addon
subView, _, err = deps.WorkflowService.AddAddon(context.Background(), subView.Subscription.NamespacedID, subscriptionworkflow.AddAddonWorkflowInput{
AddonID: add2.ID,
InitialQuantity: 1,
Timing: subscription.Timing{
Custom: &now,
},
})
require.NoError(t, err)
// And lets validate that both of them updated the entitlement limit
item := subView.Phases[0].ItemsByKey[subscriptiontestutils.ExampleRateCard1.Key()][0]
require.Equal(t, 200.0, *item.Entitlement.Entitlement.IssueAfterReset)
}))
t.Run("Should sync subscription with new addons contents", runWithDeps(func(t *testing.T, deps subscriptiontestutils.SubscriptionDependencies) {
p, add := subscriptiontestutils.CreatePlanWithAddon(
t,
deps,
subscriptiontestutils.GetExamplePlanInput(t),
subscriptiontestutils.BuildAddonForTesting(t,
productcatalog.EffectivePeriod{
EffectiveFrom: &now,
EffectiveTo: nil,
},
productcatalog.AddonInstanceTypeSingle,
subscriptiontestutils.ExampleAddonRateCard2.Clone(), // This will add a new item
subscriptiontestutils.ExampleAddonRateCard4.Clone(), // This will extend existing items
),
)
// Let's create a subscription from the plan
subView := subscriptiontestutils.CreateSubscriptionFromPlan(t, &deps, p, now)
ogView := subView
require.NotNil(t, ogView)
spec := subView.AsSpec()
addonInp := subscriptionworkflow.AddAddonWorkflowInput{
AddonID: add.ID,
InitialQuantity: 1,
Timing: subscription.Timing{
Custom: &now,
},
}
subView, subAdd, err := deps.WorkflowService.AddAddon(context.Background(), subView.Subscription.NamespacedID, addonInp)
require.NoError(t, err)
// Let's figure out what the expected spec should be
{
diff, err := addondiff.GetDiffableFromAddon(subView, subAdd)
require.NoError(t, err)
require.NoError(t, spec.Apply(diff.GetApplies(), subscription.ApplyContext{
CurrentTime: now,
}))
}
newSpec := subView.AsSpec()
// Due to not knowing the FeatureIDs before the subscription is updated, we cannot use subscriptiontestutils.SpecsEqual properly
// We'll strip all FeatureIDs from the comparison, which OPENS UP silent errors but this is the best we can do for now
stripFeatureIDs(&spec)
stripFeatureIDs(&newSpec)
subscriptiontestutils.SpecsEqual(t, newSpec, spec)
}))
t.Run("Should return conflict error if subscription already has that addon purchased", runWithDeps(func(t *testing.T, deps subscriptiontestutils.SubscriptionDependencies) {
p, add := subscriptiontestutils.CreatePlanWithAddon(
t,
deps,
subscriptiontestutils.GetExamplePlanInput(t),
subscriptiontestutils.BuildAddonForTesting(t,
productcatalog.EffectivePeriod{
EffectiveFrom: &now,
EffectiveTo: nil,
},
productcatalog.AddonInstanceTypeSingle,
subscriptiontestutils.ExampleAddonRateCard2.Clone(), // This will add a new item
subscriptiontestutils.ExampleAddonRateCard4.Clone(), // This will extend existing items
),
)
subView := subscriptiontestutils.CreateSubscriptionFromPlan(t, &deps, p, now)
addonInp := subscriptionworkflow.AddAddonWorkflowInput{
AddonID: add.ID,
InitialQuantity: 1,
Timing: subscription.Timing{
Custom: &now,
},
}
_, _, err := deps.WorkflowService.AddAddon(context.Background(), subView.Subscription.NamespacedID, addonInp)
require.NoError(t, err)
_, _, err = deps.WorkflowService.AddAddon(context.Background(), subView.Subscription.NamespacedID, addonInp)
require.Error(t, err)
require.ErrorAs(t, err, lo.ToPtr(&models.GenericConflictError{}))
require.True(t, models.IsGenericConflictError(err))
}))
}
func TestChangeAddonQuantity(t *testing.T) {
now := testutils.GetRFC3339Time(t, "2025-04-01T00:00:00Z")
runWithDeps := func(fn func(t *testing.T, deps subscriptiontestutils.SubscriptionDependencies)) func(t *testing.T) {
return func(t *testing.T) {
// Freeze just past the effective boundary to avoid Postgres timestamp
// truncation turning "now" into a boundary value that reads as
// not-yet-active or already-past in follow-up comparisons.
clock.FreezeTime(now.Add(time.Millisecond))
defer func() {
clock.UnFreeze()
clock.ResetTime()
}()
dbDeps := subscriptiontestutils.SetupDBDeps(t)
defer dbDeps.Cleanup(t)
deps := subscriptiontestutils.NewService(t, dbDeps)
fn(t, deps)
}
}
t.Run("Should error if SubscriptionAddon and Subscription are in different namespaces", runWithDeps(func(t *testing.T, deps subscriptiontestutils.SubscriptionDependencies) {
p, add := subscriptiontestutils.CreatePlanWithAddon(
t,
deps,
subscriptiontestutils.GetExamplePlanInput(t),
subscriptiontestutils.BuildAddonForTesting(t,
productcatalog.EffectivePeriod{
EffectiveFrom: &now,
EffectiveTo: nil,
},
productcatalog.AddonInstanceTypeSingle,
subscriptiontestutils.ExampleAddonRateCard2.Clone(), // This will add a new item
subscriptiontestutils.ExampleAddonRateCard4.Clone(), // This will extend existing items
),
)
subView := subscriptiontestutils.CreateSubscriptionFromPlan(t, &deps, p, now)
addonInp := subscriptionworkflow.AddAddonWorkflowInput{
AddonID: add.ID,
InitialQuantity: 1,
Timing: subscription.Timing{
Custom: &now,
},
}
subView, subAdd, err := deps.WorkflowService.AddAddon(context.Background(), subView.Subscription.NamespacedID, addonInp)
require.NoError(t, err)
changeTime := now.AddDate(0, 0, 1)
changeInp := subscriptionworkflow.ChangeAddonQuantityWorkflowInput{
SubscriptionAddonID: models.NamespacedID{
ID: subAdd.ID,
Namespace: "some-other-namespace",
},
Quantity: 0,
Timing: subscription.Timing{
Custom: &changeTime,
},
}
_, _, err = deps.WorkflowService.ChangeAddonQuantity(context.Background(), subView.Subscription.NamespacedID, changeInp)
require.Error(t, err)
require.ErrorAs(t, err, lo.ToPtr(&models.GenericValidationError{}))
require.True(t, models.IsGenericValidationError(err))
}))
t.Run("Should error if SubscriptionAddon doesn't belong to Subscription", runWithDeps(func(t *testing.T, deps subscriptiontestutils.SubscriptionDependencies) {
p, add := subscriptiontestutils.CreatePlanWithAddon(
t,
deps,
subscriptiontestutils.GetExamplePlanInput(t),
subscriptiontestutils.BuildAddonForTesting(t,
productcatalog.EffectivePeriod{
EffectiveFrom: &now,
EffectiveTo: nil,
},
productcatalog.AddonInstanceTypeSingle,
subscriptiontestutils.ExampleAddonRateCard2.Clone(), // This will add a new item
subscriptiontestutils.ExampleAddonRateCard4.Clone(), // This will extend existing items
),
)
subView := subscriptiontestutils.CreateSubscriptionFromPlan(t, &deps, p, now)
addonInp := subscriptionworkflow.AddAddonWorkflowInput{
AddonID: add.ID,
InitialQuantity: 1,
Timing: subscription.Timing{
Custom: &now,
},
}
cust2, err := deps.CustomerService.CreateCustomer(context.Background(), customer.CreateCustomerInput{
Namespace: subscriptiontestutils.ExampleNamespace,
CustomerMutate: customer.CustomerMutate{
Name: "another",
PrimaryEmail: lo.ToPtr("mail@me.uk"),
Currency: lo.ToPtr(currencyx.Code("USD")),
UsageAttribution: &customer.CustomerUsageAttribution{
SubjectKeys: []string{"another"},
},
},
})
require.NoError(t, err)
subView2, err := deps.WorkflowService.CreateFromPlan(context.Background(), subscriptionworkflow.CreateSubscriptionWorkflowInput{
Namespace: cust2.Namespace,
CustomerID: cust2.ID,
ChangeSubscriptionWorkflowInput: subscriptionworkflow.ChangeSubscriptionWorkflowInput{
Name: "test",
Timing: subscription.Timing{
Custom: &now,
},
},
}, p)
require.NoError(t, err)
subView, _, err = deps.WorkflowService.AddAddon(context.Background(), subView.Subscription.NamespacedID, addonInp)
require.NoError(t, err)
subView2, subAdd2, err := deps.WorkflowService.AddAddon(context.Background(), subView2.Subscription.NamespacedID, addonInp)
require.NoError(t, err)
changeTime := now.AddDate(0, 0, 1)
// Now let's reference a SubsAddon from sub2 while providing sub1 below
changeInp := subscriptionworkflow.ChangeAddonQuantityWorkflowInput{
SubscriptionAddonID: subAdd2.NamespacedID,
Quantity: 0,
Timing: subscription.Timing{
Custom: &changeTime,
},
}
_, _, err = deps.WorkflowService.ChangeAddonQuantity(context.Background(), subView.Subscription.NamespacedID, changeInp)
require.Error(t, err)
require.ErrorAs(t, err, lo.ToPtr(&models.GenericValidationError{}))
require.True(t, models.IsGenericValidationError(err))
}))
t.Run("Should error if the subscription is inactive or in wrong state", runWithDeps(func(t *testing.T, deps subscriptiontestutils.SubscriptionDependencies) {
p, add := subscriptiontestutils.CreatePlanWithAddon(
t,
deps,
subscriptiontestutils.GetExamplePlanInput(t),
subscriptiontestutils.BuildAddonForTesting(t,
productcatalog.EffectivePeriod{
EffectiveFrom: &now,
EffectiveTo: nil,
},
productcatalog.AddonInstanceTypeMultiple,
subscriptiontestutils.ExampleAddonRateCard2.Clone(), // This will add a new item
subscriptiontestutils.ExampleAddonRateCard4.Clone(), // This will extend existing items
),
)
// Let's create a subscription from the plan
subView := subscriptiontestutils.CreateSubscriptionFromPlan(t, &deps, p, now)
// Let's add an addon to the subscription
addonInp := subscriptionworkflow.AddAddonWorkflowInput{
AddonID: add.ID,
InitialQuantity: 1,
Timing: subscription.Timing{
Custom: &now,
},
}
_, subAdd, err := deps.WorkflowService.AddAddon(context.Background(), subView.Subscription.NamespacedID, addonInp)
require.NoError(t, err)
// Let's cancel the subscription
_, err = deps.SubscriptionService.Cancel(context.Background(), subView.Subscription.NamespacedID, subscription.Timing{
Enum: lo.ToPtr(subscription.TimingNextBillingCycle),
})
require.NoError(t, err)
// Now let's try to change the quantity of the addon
changeInp := subscriptionworkflow.ChangeAddonQuantityWorkflowInput{
SubscriptionAddonID: subAdd.NamespacedID,
Quantity: 2,
Timing: subscription.Timing{
Custom: lo.ToPtr(now.AddDate(0, 0, 1)),
},
}
_, _, err = deps.WorkflowService.ChangeAddonQuantity(context.Background(), subView.Subscription.NamespacedID, changeInp)
require.Error(t, err)
require.ErrorAs(t, err, lo.ToPtr(&models.GenericForbiddenError{}))
require.True(t, models.IsGenericForbiddenError(err))
require.ErrorContains(t, err, "state canceled not allowed")
}))
t.Run("Should update the quantity of the addon", runWithDeps(func(t *testing.T, deps subscriptiontestutils.SubscriptionDependencies) {
p, add := subscriptiontestutils.CreatePlanWithAddon(
t,
deps,
subscriptiontestutils.BuildTestPlanInput(t).
AddPhase(nil, subscriptiontestutils.ExampleRateCard1.Clone()).
Build(),
subscriptiontestutils.BuildAddonForTesting(t,
productcatalog.EffectivePeriod{
EffectiveFrom: &now,
EffectiveTo: nil,
},
productcatalog.AddonInstanceTypeMultiple,
subscriptiontestutils.ExampleAddonRateCard5.Clone(),
),
)
subView := subscriptiontestutils.CreateSubscriptionFromPlan(t, &deps, p, now)
addonInp := subscriptionworkflow.AddAddonWorkflowInput{
AddonID: add.ID,
InitialQuantity: 1,
Timing: subscription.Timing{
Custom: &now,
},
}
subView, subAdd, err := deps.WorkflowService.AddAddon(context.Background(), subView.Subscription.NamespacedID, addonInp)
require.NoError(t, err)
changeTime := now.AddDate(0, 0, 1)
changeInp := subscriptionworkflow.ChangeAddonQuantityWorkflowInput{
SubscriptionAddonID: subAdd.NamespacedID,
Quantity: 0,
Timing: subscription.Timing{
Custom: &changeTime,
},
}
subView, subAdd, err = deps.WorkflowService.ChangeAddonQuantity(context.Background(), subView.Subscription.NamespacedID, changeInp)
require.NoError(t, err)
require.Len(t, subAdd.Quantities.GetTimes(), 2)
require.Equal(t, subAdd.Quantities.GetAt(0).GetValue().Quantity, 1)
require.Equal(t, subAdd.Quantities.GetAt(1).GetValue().Quantity, 0)
require.Len(t, subView.Phases[0].ItemsByKey[subscriptiontestutils.ExampleRateCard1.Key()], 2)
}))
t.Run("Should not combine two subsequent identical items (present due to an edit) when changing the quantity after an addon is already purchased", runWithDeps(func(t *testing.T, deps subscriptiontestutils.SubscriptionDependencies) {
twoMonths := datetime.MustParseDuration(t, "P2M")
twoMonthsFromNow, _ := twoMonths.AddTo(now)
p, add := subscriptiontestutils.CreatePlanWithAddon(
t,
deps,
subscriptiontestutils.BuildTestPlanInput(t).
SetMeta(productcatalog.PlanMeta{
Name: "Test Plan",
Key: "test_plan",
Version: 1,
Currency: currency.USD,
BillingCadence: datetime.MustParseDuration(t, "P1M"),
ProRatingConfig: productcatalog.ProRatingConfig{
Enabled: true,
Mode: productcatalog.ProRatingModeProratePrices,
},
SettlementMode: productcatalog.CreditThenInvoiceSettlementMode,
}).
AddPhase(nil, subscriptiontestutils.ExampleRateCard1.Clone()).
Build(),
subscriptiontestutils.BuildAddonForTesting(t,
productcatalog.EffectivePeriod{
EffectiveFrom: &now,
EffectiveTo: nil,
},
productcatalog.AddonInstanceTypeMultiple,
subscriptiontestutils.ExampleAddonRateCard5.Clone(),
),
)
subView := subscriptiontestutils.CreateSubscriptionFromPlan(t, &deps, p, now)
// Now let's edit the sub
ogItem := subView.Phases[0].ItemsByKey[subscriptiontestutils.ExampleRateCard1.Key()][0].Spec
timing := subscription.Timing{
Enum: lo.ToPtr(subscription.TimingNextBillingCycle),
}
subView, err := deps.WorkflowService.EditRunning(context.Background(), subView.Subscription.NamespacedID, []subscription.Patch{
patch.PatchRemoveItem{
PhaseKey: ogItem.PhaseKey,
ItemKey: ogItem.ItemKey,
},
patch.PatchAddItem{
PhaseKey: ogItem.PhaseKey,
ItemKey: ogItem.ItemKey,
CreateInput: ogItem, // This will be identical to the original but will still cause a split
},
}, timing)
require.NoError(t, err)
require.Len(t, subView.Phases[0].ItemsByKey[subscriptiontestutils.ExampleRateCard1.Key()], 2)
// Now let's add the addon
addonInp := subscriptionworkflow.AddAddonWorkflowInput{
AddonID: add.ID,
InitialQuantity: 1,
Timing: subscription.Timing{
Custom: &now,
},
}
subView, subAdd, err := deps.WorkflowService.AddAddon(context.Background(), subView.Subscription.NamespacedID, addonInp)
require.NoError(t, err)
// assert that its still two items
require.Len(t, subView.Phases[0].ItemsByKey[subscriptiontestutils.ExampleRateCard1.Key()], 2)
// Now let's change the quantity of the addon
changeInp := subscriptionworkflow.ChangeAddonQuantityWorkflowInput{
SubscriptionAddonID: subAdd.NamespacedID,
Quantity: 0,
Timing: subscription.Timing{
Custom: &twoMonthsFromNow,
},
}
subView, subAdd, err = deps.WorkflowService.ChangeAddonQuantity(context.Background(), subView.Subscription.NamespacedID, changeInp)
require.NoError(t, err)
// Now it should be three items: original two with addon included + 3rd without the addon after 3 months
require.Len(t, subView.Phases[0].ItemsByKey[subscriptiontestutils.ExampleRateCard1.Key()], 3)
}))
}
// Instead of stripping them, we could also populate them with the correct values
func stripFeatureIDs(spec *subscription.SubscriptionSpec) {
for _, phase := range spec.Phases {
for _, items := range phase.ItemsByKey {
for _, item := range items {
item.RateCard.SetFeature(nil, item.RateCard.GetFeatureKey())
}
}
}
}
func TestAddonCombinations(t *testing.T) {
now := testutils.GetRFC3339Time(t, "2025-04-01T00:00:00Z")
runWithDeps := func(fn func(t *testing.T, deps subscriptiontestutils.SubscriptionDependencies)) func(t *testing.T) {
return func(t *testing.T) {
// Freeze just past the effective boundary to avoid Postgres timestamp
// truncation turning "now" into a boundary value that reads as
// not-yet-active or already-past in follow-up comparisons.
clock.FreezeTime(now.Add(time.Millisecond))
defer func() {
clock.UnFreeze()
clock.ResetTime()
}()
dbDeps := subscriptiontestutils.SetupDBDeps(t)
defer dbDeps.Cleanup(t)
deps := subscriptiontestutils.NewService(t, dbDeps)
fn(t, deps)
}
}
t.Run("Should handle repeated add/remove of single instance addon with dynamic price", runWithDeps(func(t *testing.T, deps subscriptiontestutils.SubscriptionDependencies) {
// Create a plan with mixed rate cards
planInput := subscriptiontestutils.BuildTestPlanInput(t).
AddPhase(nil,
subscriptiontestutils.ExampleRateCard1.Clone(), // Flat price
&productcatalog.UsageBasedRateCard{ // Dynamic price
RateCardMeta: productcatalog.RateCardMeta{
Key: subscriptiontestutils.ExampleFeatureKey2,
Name: "Dynamic Rate Card 1",
FeatureKey: &subscriptiontestutils.ExampleFeatureKey2,
Price: productcatalog.NewPriceFrom(productcatalog.TieredPrice{
Mode: productcatalog.VolumeTieredPrice,
Tiers: []productcatalog.PriceTier{
{
UpToAmount: lo.ToPtr(alpacadecimal.NewFromInt(100)),
FlatPrice: &productcatalog.PriceTierFlatPrice{
Amount: alpacadecimal.NewFromInt(10),
},
},
{
// UpToAmount: nil for infinity
UnitPrice: &productcatalog.PriceTierUnitPrice{
Amount: alpacadecimal.NewFromInt(1),
},
},
},
}),
},
BillingCadence: subscriptiontestutils.ISOMonth,
},
).
Build()
p, add := subscriptiontestutils.CreatePlanWithAddon(
t,
deps,
planInput,
subscriptiontestutils.BuildAddonForTesting(t,
productcatalog.EffectivePeriod{
EffectiveFrom: &now,
EffectiveTo: nil,
},
productcatalog.AddonInstanceTypeSingle,
&productcatalog.UsageBasedRateCard{ // Dynamic price for addon
RateCardMeta: productcatalog.RateCardMeta{
Key: subscriptiontestutils.ExampleFeatureKey3,
Name: "Addon Dynamic Rate Card",
FeatureKey: &subscriptiontestutils.ExampleFeatureKey3,
Price: productcatalog.NewPriceFrom(productcatalog.TieredPrice{
Mode: productcatalog.VolumeTieredPrice,
Tiers: []productcatalog.PriceTier{
{
// UpToAmount: nil for infinity
UnitPrice: &productcatalog.PriceTierUnitPrice{
Amount: alpacadecimal.NewFromFloat(0.5),
},
},
},
}),
},
BillingCadence: subscriptiontestutils.ISOMonth,
},
),
)
// Create a subscription from the plan
subView := subscriptiontestutils.CreateSubscriptionFromPlan(t, &deps, p, now)
// Add the addon initially
addTime := now
var subAdd subscriptionaddon.SubscriptionAddon
var err error
subView, subAdd, err = deps.WorkflowService.AddAddon(context.Background(), subView.Subscription.NamespacedID, subscriptionworkflow.AddAddonWorkflowInput{
AddonID: add.ID,
InitialQuantity: 1,
Timing: subscription.Timing{
Custom: &addTime,
},
})
require.NoError(t, err, "failed to add addon initially")
require.NotNil(t, subAdd)
// Now, repeatedly change quantity to 0 and then back to 1
// Let's pass time
clock.FreezeTime(clock.Now().Add(time.Minute))
for i := 0; i < 3; i++ {
// Change quantity to 0 (remove)
changeInpZero := subscriptionworkflow.ChangeAddonQuantityWorkflowInput{
SubscriptionAddonID: subAdd.NamespacedID,
Quantity: 0,
Timing: subscription.Timing{
Custom: lo.ToPtr(clock.Now()),
},
}
subView, _, err = deps.WorkflowService.ChangeAddonQuantity(context.Background(), subView.Subscription.NamespacedID, changeInpZero)
require.NoError(t, err, "failed to change addon quantity to 0 on iteration %d", i)
// Let's pass time
clock.FreezeTime(clock.Now().Add(time.Minute))
// Change quantity back to 1 (re-add)
changeInpOne := subscriptionworkflow.ChangeAddonQuantityWorkflowInput{
SubscriptionAddonID: subAdd.NamespacedID,
Quantity: 1,
Timing: subscription.Timing{
Custom: lo.ToPtr(clock.Now()),
},
}
subView, subAdd, err = deps.WorkflowService.ChangeAddonQuantity(context.Background(), subView.Subscription.NamespacedID, changeInpOne)
require.NoError(t, err, "failed to change addon quantity to 1 on iteration %d", i)
// Let's pass time
clock.FreezeTime(clock.Now().Add(time.Minute))
}
}))
}