Skip to content

Commit

Permalink
fix: free tiers should be removed from volume based if they exists
Browse files Browse the repository at this point in the history
  • Loading branch information
Plopix committed Mar 22, 2024
1 parent ecae92c commit 3102598
Show file tree
Hide file tree
Showing 3 changed files with 106 additions and 31 deletions.
2 changes: 1 addition & 1 deletion components/js-api-client/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@crystallize/js-api-client",
"license": "MIT",
"version": "2.4.0",
"version": "2.5.0",
"author": "Crystallize <[email protected]> (https://crystallize.com)",
"contributors": [
"Sébastien Morel <[email protected]>",
Expand Down
34 changes: 19 additions & 15 deletions components/js-api-client/src/core/pricing.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,54 @@
import { Prices, Tier } from '../types/pricing';

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

if (tierType === 'volume') {
return volumeBasedPriceFor(usage, sortedTiers);
return volumeBasedPriceFor(Math.max(0, usage), sortedTiers);
}
return graduatedBasedPriceFor(usage, sortedTiers);
}

function volumeBasedPriceFor(usage: number, tiers: Tier[]): Prices {
const freeUsage = tiers.reduce((memo: number, tier: Tier, tierIndex) => {
if (tier.price === 0) {
return tiers[tierIndex + 1]?.threshold || 0;
}
return memo;
}, 0);
const forCalculationUsage = Math.max(0, usage - freeUsage);
const tiersLength = tiers.length;

for (let i = tiersLength - 1; i >= 0; i--) {
const tier: Tier = tiers[i];
if (usage < tier.threshold && i > 0) {
continue;
}
// manage also an inexistent tier (threshold = 0)
return { [tier.currency]: (usage >= tier.threshold ? tier.price : 0) * usage };
return { [tier.currency]: (usage >= tier.threshold ? tier.price || 0 : 0) * forCalculationUsage };
}
return { USD: 0.0 };
}

function graduatedBasedPriceFor(usage: number, tiers: Tier[]): Prices {
let rest = usage;

// manage also an inexistent tier (threshold = 0)
if (tiers[0].threshold > 0) {
rest = Math.max(0, rest - (tiers[0].threshold - 1));
}

const splitUsage: Array<Tier & { usage: number }> = tiers.map((tier: Tier, tierIndex: number) => {
const limit = tiers[tierIndex + 1]?.threshold || Infinity;
const tierUsage = rest > limit ? limit : rest;
const currentThreshold = tier.threshold;
const nextThreshold = tiers[tierIndex + 1]?.threshold;
const maxTierUsage = nextThreshold ? nextThreshold - currentThreshold : Infinity;
const tierUsage = rest <= maxTierUsage ? rest : maxTierUsage;
rest -= tierUsage;
return {
...tier,
usage: tierUsage,
};
});

return splitUsage.reduce((memo: Prices, tier: Tier & { usage: number }) => {
return {
...memo,
[tier.currency]: (memo[tier.currency] || 0.0) + tier.usage * tier.price,
[tier.currency]: (memo[tier.currency] || 0.0) + tier.usage * (tier.price || 0),
};
}, {});
}
101 changes: 86 additions & 15 deletions components/js-api-client/tests/pricesForUsageOnTier.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,26 +102,97 @@ const tiers5 = [

test('Price For Volume', () => {
expect(pricesForUsageOnTier(0, tiers1, 'volume')).toEqual({ EUR: 0 });
expect(pricesForUsageOnTier(2, tiers1, 'volume')).toEqual({ EUR: 500 });
expect(pricesForUsageOnTier(3, tiers1, 'volume')).toEqual({ EUR: 750 });
expect(pricesForUsageOnTier(20, tiers1, 'volume')).toEqual({ EUR: 400 });
expect(pricesForUsageOnTier(40, tiers1, 'volume')).toEqual({ EUR: 800 });

expect(pricesForUsageOnTier(2, tiers1, 'volume')).toEqual({ EUR: 0 }); // 2 are still free
expect(pricesForUsageOnTier(3, tiers1, 'volume')).toEqual({ EUR: 250 }); // we pay for 1
expect(pricesForUsageOnTier(20, tiers1, 'volume')).toEqual({ EUR: 360 }); // 20$ bracket but 2 free
expect(pricesForUsageOnTier(40, tiers1, 'volume')).toEqual({ EUR: 760 }); // 20$ bracket but 2 free
expect(pricesForUsageOnTier(3, tiers2, 'volume')).toEqual({ EUR: 0 });
expect(pricesForUsageOnTier(14, tiers2, 'volume')).toEqual({ EUR: 0 });
expect(pricesForUsageOnTier(15, tiers2, 'volume')).toEqual({ EUR: 7500 });
expect(pricesForUsageOnTier(16, tiers2, 'volume')).toEqual({ EUR: 8000 });
expect(pricesForUsageOnTier(30, tiers2, 'volume')).toEqual({ EUR: 9000 });
expect(pricesForUsageOnTier(31, tiers2, 'volume')).toEqual({ EUR: 9300 });
expect(pricesForUsageOnTier(40, tiers2, 'volume')).toEqual({ EUR: 4000 });
expect(pricesForUsageOnTier(41, tiers2, 'volume')).toEqual({ EUR: 4100 });
expect(pricesForUsageOnTier(131, tiers2, 'volume')).toEqual({ EUR: 13100 });
expect(pricesForUsageOnTier(15, tiers2, 'volume')).toEqual({ EUR: 0 });
expect(pricesForUsageOnTier(16, tiers2, 'volume')).toEqual({ EUR: 500 });
expect(pricesForUsageOnTier(30, tiers2, 'volume')).toEqual({ EUR: 4500 });
expect(pricesForUsageOnTier(31, tiers2, 'volume')).toEqual({ EUR: 4800 });
expect(pricesForUsageOnTier(40, tiers2, 'volume')).toEqual({ EUR: 2500 });
expect(pricesForUsageOnTier(41, tiers2, 'volume')).toEqual({ EUR: 2600 });
expect(pricesForUsageOnTier(131, tiers2, 'volume')).toEqual({ EUR: 11600 });
});

test('Price For Graduated', () => {
test.only('Price For Graduated', () => {
expect(pricesForUsageOnTier(4, tiers3, 'graduated')).toEqual({ EUR: 19 });
expect(pricesForUsageOnTier(211, tiers3, 'graduated')).toEqual({ EUR: 368 });
expect(pricesForUsageOnTier(211, tiers3, 'graduated')).toEqual({ EUR: 304 });
expect(pricesForUsageOnTier(14, tiers4, 'graduated')).toEqual({ EUR: 0 });
expect(pricesForUsageOnTier(15, tiers5, 'graduated')).toEqual({ EUR: 0 });
expect(pricesForUsageOnTier(25, tiers5, 'graduated')).toEqual({ EUR: 2 });
expect(pricesForUsageOnTier(25, tiers5, 'graduated')).toEqual({ EUR: 0 });
expect(pricesForUsageOnTier(26, tiers5, 'graduated')).toEqual({ EUR: 2 });
});

test('Test 3/21/2024 - with existing minimum', () => {
const tiers = [
{
threshold: 0,
price: 0,
currency: 'EUR',
},
{
threshold: 10,
price: 5,
currency: 'EUR',
},
{
threshold: 30,
price: 3,
currency: 'EUR',
},
];
expect(pricesForUsageOnTier(42, tiers, 'graduated')).toEqual({ EUR: 136 });
expect(pricesForUsageOnTier(42, tiers, 'volume')).toEqual({ EUR: 96 });
});

test('Test 3/21/2024 - without existing minimum', () => {
const tiers = [
{
threshold: 10,
price: 5,
currency: 'EUR',
},
{
threshold: 30,
price: 3,
currency: 'EUR',
},
];
expect(pricesForUsageOnTier(42, tiers, 'graduated')).toEqual({ EUR: 136 });
expect(pricesForUsageOnTier(42, tiers, 'volume')).toEqual({ EUR: 96 });
});

test('Test 3/21/2024 - with many free tiers and existing minimum', () => {
const tiers = [
{
threshold: 0,
price: 0,
currency: 'EUR',
},
{
threshold: 2,
price: 0,
currency: 'EUR',
},
{
threshold: 8,
price: 0,
currency: 'EUR',
},
{
threshold: 10,
price: 5,
currency: 'EUR',
},
{
threshold: 30,
price: 3,
currency: 'EUR',
},
];
expect(pricesForUsageOnTier(42, tiers, 'graduated')).toEqual({ EUR: 136 });
expect(pricesForUsageOnTier(42, tiers, 'volume')).toEqual({ EUR: 96 });
});

0 comments on commit 3102598

Please sign in to comment.