Skip to content

Commit b06cdeb

Browse files
committed
fix: free tiers should be removed from volume based if they exists
1 parent b164b31 commit b06cdeb

File tree

3 files changed

+106
-31
lines changed

3 files changed

+106
-31
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@crystallize/js-api-client",
33
"license": "MIT",
4-
"version": "2.4.0",
4+
"version": "2.5.0",
55
"author": "Crystallize <[email protected]> (https://crystallize.com)",
66
"contributors": [
77
"Sébastien Morel <[email protected]>",

src/core/pricing.ts

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,54 @@
11
import { Prices, Tier } from '../types/pricing';
22

33
export function pricesForUsageOnTier(usage: number, tiers: Tier[], tierType: 'volume' | 'graduated'): Prices {
4-
const sortedTiers = [...tiers].sort((a: Tier, b: Tier) => a.threshold - b.threshold);
4+
const sortedTiers = tiers.sort((a: Tier, b: Tier) => a.threshold - b.threshold);
5+
// let's add the implicit tiers id it does not exists
6+
if (sortedTiers[0].threshold > 0) {
7+
sortedTiers.unshift({ threshold: 0, price: 0, currency: tiers[0].currency });
8+
}
59

610
if (tierType === 'volume') {
7-
return volumeBasedPriceFor(usage, sortedTiers);
11+
return volumeBasedPriceFor(Math.max(0, usage), sortedTiers);
812
}
913
return graduatedBasedPriceFor(usage, sortedTiers);
1014
}
1115

1216
function volumeBasedPriceFor(usage: number, tiers: Tier[]): Prices {
17+
const freeUsage = tiers.reduce((memo: number, tier: Tier, tierIndex) => {
18+
if (tier.price === 0) {
19+
return tiers[tierIndex + 1]?.threshold || 0;
20+
}
21+
return memo;
22+
}, 0);
23+
const forCalculationUsage = Math.max(0, usage - freeUsage);
1324
const tiersLength = tiers.length;
14-
1525
for (let i = tiersLength - 1; i >= 0; i--) {
1626
const tier: Tier = tiers[i];
1727
if (usage < tier.threshold && i > 0) {
1828
continue;
1929
}
20-
// manage also an inexistent tier (threshold = 0)
21-
return { [tier.currency]: (usage >= tier.threshold ? tier.price : 0) * usage };
30+
return { [tier.currency]: (usage >= tier.threshold ? tier.price || 0 : 0) * forCalculationUsage };
2231
}
2332
return { USD: 0.0 };
2433
}
2534

2635
function graduatedBasedPriceFor(usage: number, tiers: Tier[]): Prices {
2736
let rest = usage;
28-
29-
// manage also an inexistent tier (threshold = 0)
30-
if (tiers[0].threshold > 0) {
31-
rest = Math.max(0, rest - (tiers[0].threshold - 1));
32-
}
33-
3437
const splitUsage: Array<Tier & { usage: number }> = tiers.map((tier: Tier, tierIndex: number) => {
35-
const limit = tiers[tierIndex + 1]?.threshold || Infinity;
36-
const tierUsage = rest > limit ? limit : rest;
38+
const currentThreshold = tier.threshold;
39+
const nextThreshold = tiers[tierIndex + 1]?.threshold;
40+
const maxTierUsage = nextThreshold ? nextThreshold - currentThreshold : Infinity;
41+
const tierUsage = rest <= maxTierUsage ? rest : maxTierUsage;
3742
rest -= tierUsage;
3843
return {
3944
...tier,
4045
usage: tierUsage,
4146
};
4247
});
43-
4448
return splitUsage.reduce((memo: Prices, tier: Tier & { usage: number }) => {
4549
return {
4650
...memo,
47-
[tier.currency]: (memo[tier.currency] || 0.0) + tier.usage * tier.price,
51+
[tier.currency]: (memo[tier.currency] || 0.0) + tier.usage * (tier.price || 0),
4852
};
4953
}, {});
5054
}

tests/pricesForUsageOnTier.test.js

Lines changed: 86 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -102,26 +102,97 @@ const tiers5 = [
102102

103103
test('Price For Volume', () => {
104104
expect(pricesForUsageOnTier(0, tiers1, 'volume')).toEqual({ EUR: 0 });
105-
expect(pricesForUsageOnTier(2, tiers1, 'volume')).toEqual({ EUR: 500 });
106-
expect(pricesForUsageOnTier(3, tiers1, 'volume')).toEqual({ EUR: 750 });
107-
expect(pricesForUsageOnTier(20, tiers1, 'volume')).toEqual({ EUR: 400 });
108-
expect(pricesForUsageOnTier(40, tiers1, 'volume')).toEqual({ EUR: 800 });
109-
105+
expect(pricesForUsageOnTier(2, tiers1, 'volume')).toEqual({ EUR: 0 }); // 2 are still free
106+
expect(pricesForUsageOnTier(3, tiers1, 'volume')).toEqual({ EUR: 250 }); // we pay for 1
107+
expect(pricesForUsageOnTier(20, tiers1, 'volume')).toEqual({ EUR: 360 }); // 20$ bracket but 2 free
108+
expect(pricesForUsageOnTier(40, tiers1, 'volume')).toEqual({ EUR: 760 }); // 20$ bracket but 2 free
110109
expect(pricesForUsageOnTier(3, tiers2, 'volume')).toEqual({ EUR: 0 });
111110
expect(pricesForUsageOnTier(14, tiers2, 'volume')).toEqual({ EUR: 0 });
112-
expect(pricesForUsageOnTier(15, tiers2, 'volume')).toEqual({ EUR: 7500 });
113-
expect(pricesForUsageOnTier(16, tiers2, 'volume')).toEqual({ EUR: 8000 });
114-
expect(pricesForUsageOnTier(30, tiers2, 'volume')).toEqual({ EUR: 9000 });
115-
expect(pricesForUsageOnTier(31, tiers2, 'volume')).toEqual({ EUR: 9300 });
116-
expect(pricesForUsageOnTier(40, tiers2, 'volume')).toEqual({ EUR: 4000 });
117-
expect(pricesForUsageOnTier(41, tiers2, 'volume')).toEqual({ EUR: 4100 });
118-
expect(pricesForUsageOnTier(131, tiers2, 'volume')).toEqual({ EUR: 13100 });
111+
expect(pricesForUsageOnTier(15, tiers2, 'volume')).toEqual({ EUR: 0 });
112+
expect(pricesForUsageOnTier(16, tiers2, 'volume')).toEqual({ EUR: 500 });
113+
expect(pricesForUsageOnTier(30, tiers2, 'volume')).toEqual({ EUR: 4500 });
114+
expect(pricesForUsageOnTier(31, tiers2, 'volume')).toEqual({ EUR: 4800 });
115+
expect(pricesForUsageOnTier(40, tiers2, 'volume')).toEqual({ EUR: 2500 });
116+
expect(pricesForUsageOnTier(41, tiers2, 'volume')).toEqual({ EUR: 2600 });
117+
expect(pricesForUsageOnTier(131, tiers2, 'volume')).toEqual({ EUR: 11600 });
119118
});
120119

121-
test('Price For Graduated', () => {
120+
test.only('Price For Graduated', () => {
122121
expect(pricesForUsageOnTier(4, tiers3, 'graduated')).toEqual({ EUR: 19 });
123-
expect(pricesForUsageOnTier(211, tiers3, 'graduated')).toEqual({ EUR: 368 });
122+
expect(pricesForUsageOnTier(211, tiers3, 'graduated')).toEqual({ EUR: 304 });
124123
expect(pricesForUsageOnTier(14, tiers4, 'graduated')).toEqual({ EUR: 0 });
125124
expect(pricesForUsageOnTier(15, tiers5, 'graduated')).toEqual({ EUR: 0 });
126-
expect(pricesForUsageOnTier(25, tiers5, 'graduated')).toEqual({ EUR: 2 });
125+
expect(pricesForUsageOnTier(25, tiers5, 'graduated')).toEqual({ EUR: 0 });
126+
expect(pricesForUsageOnTier(26, tiers5, 'graduated')).toEqual({ EUR: 2 });
127+
});
128+
129+
test('Test 3/21/2024 - with existing minimum', () => {
130+
const tiers = [
131+
{
132+
threshold: 0,
133+
price: 0,
134+
currency: 'EUR',
135+
},
136+
{
137+
threshold: 10,
138+
price: 5,
139+
currency: 'EUR',
140+
},
141+
{
142+
threshold: 30,
143+
price: 3,
144+
currency: 'EUR',
145+
},
146+
];
147+
expect(pricesForUsageOnTier(42, tiers, 'graduated')).toEqual({ EUR: 136 });
148+
expect(pricesForUsageOnTier(42, tiers, 'volume')).toEqual({ EUR: 96 });
149+
});
150+
151+
test('Test 3/21/2024 - without existing minimum', () => {
152+
const tiers = [
153+
{
154+
threshold: 10,
155+
price: 5,
156+
currency: 'EUR',
157+
},
158+
{
159+
threshold: 30,
160+
price: 3,
161+
currency: 'EUR',
162+
},
163+
];
164+
expect(pricesForUsageOnTier(42, tiers, 'graduated')).toEqual({ EUR: 136 });
165+
expect(pricesForUsageOnTier(42, tiers, 'volume')).toEqual({ EUR: 96 });
166+
});
167+
168+
test('Test 3/21/2024 - with many free tiers and existing minimum', () => {
169+
const tiers = [
170+
{
171+
threshold: 0,
172+
price: 0,
173+
currency: 'EUR',
174+
},
175+
{
176+
threshold: 2,
177+
price: 0,
178+
currency: 'EUR',
179+
},
180+
{
181+
threshold: 8,
182+
price: 0,
183+
currency: 'EUR',
184+
},
185+
{
186+
threshold: 10,
187+
price: 5,
188+
currency: 'EUR',
189+
},
190+
{
191+
threshold: 30,
192+
price: 3,
193+
currency: 'EUR',
194+
},
195+
];
196+
expect(pricesForUsageOnTier(42, tiers, 'graduated')).toEqual({ EUR: 136 });
197+
expect(pricesForUsageOnTier(42, tiers, 'volume')).toEqual({ EUR: 96 });
127198
});

0 commit comments

Comments
 (0)