diff --git a/components/js-api-client/package.json b/components/js-api-client/package.json index 925cd27f..c027d982 100644 --- a/components/js-api-client/package.json +++ b/components/js-api-client/package.json @@ -1,7 +1,7 @@ { "name": "@crystallize/js-api-client", "license": "MIT", - "version": "2.4.0", + "version": "2.5.0", "author": "Crystallize (https://crystallize.com)", "contributors": [ "Sébastien Morel ", diff --git a/components/js-api-client/src/core/pricing.ts b/components/js-api-client/src/core/pricing.ts index 9a99670d..8f69fdf1 100644 --- a/components/js-api-client/src/core/pricing.ts +++ b/components/js-api-client/src/core/pricing.ts @@ -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 = 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), }; }, {}); } diff --git a/components/js-api-client/tests/pricesForUsageOnTier.test.js b/components/js-api-client/tests/pricesForUsageOnTier.test.js index f5c49f4d..5855d161 100644 --- a/components/js-api-client/tests/pricesForUsageOnTier.test.js +++ b/components/js-api-client/tests/pricesForUsageOnTier.test.js @@ -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 }); });