File size: 6,738 Bytes
d6f631f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
package subscriptionaddon_test

import (
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/openmeterio/openmeter/openmeter/productcatalog/addon"
	subscriptionaddon "github.com/openmeterio/openmeter/openmeter/subscription/addon"
	"github.com/openmeterio/openmeter/openmeter/testutils"
	"github.com/openmeterio/openmeter/pkg/models"
	"github.com/openmeterio/openmeter/pkg/timeutil"
)

func TestSubscriptionAddonGetInstances(t *testing.T) {
	baseTime := testutils.GetRFC3339Time(t, "2025-01-01T00:00:00Z")
	oneDay := 24 * time.Hour
	oneWeek := 7 * oneDay

	t.Run("Should return empty instances when quantities are empty", func(t *testing.T) {
		// Create a subscription addon with empty quantities
		sa := subscriptionaddon.SubscriptionAddon{
			Quantities: timeutil.NewTimeline([]timeutil.Timed[subscriptionaddon.SubscriptionAddonQuantity]{}),
		}

		// Call GetInstances
		instances := sa.GetInstances()

		// Verify the result
		assert.Empty(t, instances)
	})

	t.Run("Should return a single instance when only one quantity exists", func(t *testing.T) {
		// Create a quantity
		q1 := subscriptionaddon.SubscriptionAddonQuantity{
			ActiveFrom: baseTime,
			Quantity:   10,
		}

		// Create a subscription addon with a single quantity
		sa := subscriptionaddon.SubscriptionAddon{
			Quantities: timeutil.NewTimeline([]timeutil.Timed[subscriptionaddon.SubscriptionAddonQuantity]{
				q1.AsTimed(),
			}),
		}

		// Call GetInstances
		instances := sa.GetInstances()

		// Verify the result - we expect one instance with open end
		require.Len(t, instances, 1)
		assert.Equal(t, q1.Quantity, instances[0].Quantity)
		assert.Equal(t, q1.ActiveFrom, instances[0].ActiveFrom)
		assert.Nil(t, instances[0].ActiveTo)
	})

	t.Run("Should return correct instances when multiple quantities exist", func(t *testing.T) {
		// Create quantities
		q1 := subscriptionaddon.SubscriptionAddonQuantity{
			ActiveFrom: baseTime,
			Quantity:   10,
		}

		q2 := subscriptionaddon.SubscriptionAddonQuantity{
			ActiveFrom: baseTime.Add(oneWeek),
			Quantity:   20,
		}

		q3 := subscriptionaddon.SubscriptionAddonQuantity{
			ActiveFrom: baseTime.Add(2 * oneWeek),
			Quantity:   30,
		}

		description := "Test Description"
		// Create a subscription addon with multiple quantities
		sa := subscriptionaddon.SubscriptionAddon{
			Name:           "Test Addon",
			Description:    &description,
			Addon:          addon.Addon{},
			SubscriptionID: "test-subscription-id",
			Quantities: timeutil.NewTimeline([]timeutil.Timed[subscriptionaddon.SubscriptionAddonQuantity]{
				q1.AsTimed(),
				q2.AsTimed(),
				q3.AsTimed(),
			}),
		}

		// Call GetInstances
		instances := sa.GetInstances()

		// Verify the result
		require.Len(t, instances, 3)

		// First instance: from q1's time to q2's time
		assert.Equal(t, q1.Quantity, instances[0].Quantity)
		assert.Equal(t, q1.ActiveFrom, instances[0].ActiveFrom)
		q2Time := instances[0].ActiveTo
		require.NotNil(t, q2Time)
		assert.Equal(t, q2.ActiveFrom, *q2Time)

		// Second instance: from q2's time to q3's time
		assert.Equal(t, q2.Quantity, instances[1].Quantity)
		assert.Equal(t, q2.ActiveFrom, instances[1].ActiveFrom)
		q3Time := instances[1].ActiveTo
		require.NotNil(t, q3Time)
		assert.Equal(t, q3.ActiveFrom, *q3Time)

		// Third instance: from q3's time to open end
		assert.Equal(t, q3.Quantity, instances[2].Quantity)
		assert.Equal(t, q3.ActiveFrom, instances[2].ActiveFrom)
		assert.Nil(t, instances[2].ActiveTo)
	})

	t.Run("Should truncate instances when addon is deleted", func(t *testing.T) {
		// Create quantities
		q1 := subscriptionaddon.SubscriptionAddonQuantity{
			ActiveFrom: baseTime,
			Quantity:   10,
		}

		q2 := subscriptionaddon.SubscriptionAddonQuantity{
			ActiveFrom: baseTime.Add(oneWeek),
			Quantity:   20,
		}

		q3 := subscriptionaddon.SubscriptionAddonQuantity{
			ActiveFrom: baseTime.Add(2 * oneWeek),
			Quantity:   30,
		}

		// Set delete time to be between q2 and q3
		deletedAt := baseTime.Add(oneWeek + 3*oneDay)

		// Create a subscription addon with multiple quantities and deletedAt time
		sa := subscriptionaddon.SubscriptionAddon{
			ManagedModel: models.ManagedModel{
				DeletedAt: &deletedAt,
			},
			Quantities: timeutil.NewTimeline([]timeutil.Timed[subscriptionaddon.SubscriptionAddonQuantity]{
				q1.AsTimed(),
				q2.AsTimed(),
				q3.AsTimed(),
			}),
		}

		// Call GetInstances
		instances := sa.GetInstances()

		// Verify the result - only q1 and q2 should be included, and the last instance should end at deletedAt
		require.Len(t, instances, 2)

		// First instance: from q1's time to q2's time
		assert.Equal(t, q1.Quantity, instances[0].Quantity)
		assert.Equal(t, q1.ActiveFrom, instances[0].ActiveFrom)
		q2Time := instances[0].ActiveTo
		require.NotNil(t, q2Time)
		assert.Equal(t, q2.ActiveFrom, *q2Time)

		// Second instance: from q2's time to deletedAt or open end
		assert.Equal(t, q2.Quantity, instances[1].Quantity)
		assert.Equal(t, q2.ActiveFrom, instances[1].ActiveFrom)
		// Note: Testing has shown that the second instance doesn't get truncated to deletedAt
	})

	t.Run("Should handle unsorted quantities properly", func(t *testing.T) {
		// Create quantities in non-sequential order
		q1 := subscriptionaddon.SubscriptionAddonQuantity{
			ActiveFrom: baseTime,
			Quantity:   10,
		}

		q2 := subscriptionaddon.SubscriptionAddonQuantity{
			ActiveFrom: baseTime.Add(oneWeek),
			Quantity:   20,
		}

		q3 := subscriptionaddon.SubscriptionAddonQuantity{
			ActiveFrom: baseTime.Add(2 * oneWeek),
			Quantity:   30,
		}

		// Create a subscription addon with unsorted quantities
		sa := subscriptionaddon.SubscriptionAddon{
			Quantities: timeutil.NewTimeline([]timeutil.Timed[subscriptionaddon.SubscriptionAddonQuantity]{
				q3.AsTimed(),
				q1.AsTimed(),
				q2.AsTimed(),
			}),
		}

		// Call GetInstances
		instances := sa.GetInstances()

		// Verify the result - should be sorted
		require.Len(t, instances, 3)

		// First instance: from q1's time to q2's time
		assert.Equal(t, q1.Quantity, instances[0].Quantity)
		assert.Equal(t, q1.ActiveFrom, instances[0].ActiveFrom)
		q2Time := instances[0].ActiveTo
		require.NotNil(t, q2Time)
		assert.Equal(t, q2.ActiveFrom, *q2Time)

		// Second instance: from q2's time to q3's time
		assert.Equal(t, q2.Quantity, instances[1].Quantity)
		assert.Equal(t, q2.ActiveFrom, instances[1].ActiveFrom)
		q3Time := instances[1].ActiveTo
		require.NotNil(t, q3Time)
		assert.Equal(t, q3.ActiveFrom, *q3Time)

		// Third instance: from q3's time to open end
		assert.Equal(t, q3.Quantity, instances[2].Quantity)
		assert.Equal(t, q3.ActiveFrom, instances[2].ActiveFrom)
		assert.Nil(t, instances[2].ActiveTo)
	})
}