diff --git a/.changeset/round-cumulative-breakdown.md b/.changeset/round-cumulative-breakdown.md new file mode 100644 index 0000000..8f09dc0 --- /dev/null +++ b/.changeset/round-cumulative-breakdown.md @@ -0,0 +1,5 @@ +--- +"@epilot/pricing": minor +--- + +`computeCumulativeValue` rounds its `breakdown` for display like its totals: each tier's `quantityUsed` to 2 decimals and its `totalAmountDecimal` to 2 decimals (`precision: 2`, as `total` is). A quantity normalised from a yearly consumption used to show as `83.333333 kWh` with an amount of `€28.333333333333`. `tierAmountDecimal` keeps the tier's real precision, and `totalWithPrecision` / `subtotalWithPrecision` are unchanged. diff --git a/src/tiers/compute-cumulative-value.test.ts b/src/tiers/compute-cumulative-value.test.ts index fb7f684..0564183 100644 --- a/src/tiers/compute-cumulative-value.test.ts +++ b/src/tiers/compute-cumulative-value.test.ts @@ -72,8 +72,8 @@ describe('computeCumulativeValue', () => { ${onRequestTiers} | ${5} | ${'kWh'} | ${'en'} | ${'EUR'} | ${undefined} | ${true} | ${undefined} | ${{ rate: 19 }} | ${{ total: '€50.00', totalWithPrecision: '€50.00', average: '€10.00/kWh', subAverage: '€8.40/kWh', subtotal: '€42.02', subtotalWithPrecision: '€42.016806722689', breakdown: [{ quantityUsed: '5 kWh', tierAmountDecimal: '€10.00/kWh', totalAmountDecimal: '€50.00' }] }} ${onRequestTiers} | ${15} | ${'kWh'} | ${'en'} | ${'EUR'} | ${undefined} | ${true} | ${undefined} | ${{ rate: 19 }} | ${'Price on request'} ${onRequestTiers} | ${15} | ${'kWh'} | ${'en'} | ${'EUR'} | ${undefined} | ${false} | ${undefined} | ${{ rate: 19 }} | ${{ total: '€145.00', totalWithPrecision: '€145.00', average: '€9.67/kWh', subAverage: '€8.12/kWh', subtotal: '€121.85', subtotalWithPrecision: '€121.848739495798', breakdown: [{ quantityUsed: '10 kWh', tierAmountDecimal: '€10.00/kWh', totalAmountDecimal: '€100.00' }, { quantityUsed: '5 kWh', tierAmountDecimal: '€9.00/kWh', totalAmountDecimal: '€45.00' }] }} - ${decimalTiers} | ${7} | ${'kWh'} | ${'en'} | ${'EUR'} | ${undefined} | ${false} | ${undefined} | ${{ rate: 19 }} | ${{ total: '38.59 cents', totalWithPrecision: '38.5915 cents', average: '5.51 cents/kWh', subAverage: '4.63 cents/kWh', subtotal: '32.43 cents', subtotalWithPrecision: '32.429832 cents', breakdown: [{ quantityUsed: '5 kWh', tierAmountDecimal: '5.5451 cents/kWh', totalAmountDecimal: '27.7255 cents' }, { quantityUsed: '2 kWh', tierAmountDecimal: '5.433 cents/kWh', totalAmountDecimal: '10.866 cents' }] }} - ${decimalTiers} | ${7} | ${'kWh'} | ${'en'} | ${'EUR'} | ${undefined} | ${false} | ${true} | ${{ rate: 19 }} | ${{ total: 'Starts at 38.59 cents', totalWithPrecision: 'Starts at 38.5915 cents', average: '5.51 cents/kWh', subAverage: '4.63 cents/kWh', subtotal: 'Starts at 32.43 cents', subtotalWithPrecision: 'Starts at 32.429832 cents', breakdown: [{ quantityUsed: '5 kWh', tierAmountDecimal: '5.5451 cents/kWh', totalAmountDecimal: '27.7255 cents' }, { quantityUsed: '2 kWh', tierAmountDecimal: '5.433 cents/kWh', totalAmountDecimal: '10.866 cents' }] }} + ${decimalTiers} | ${7} | ${'kWh'} | ${'en'} | ${'EUR'} | ${undefined} | ${false} | ${undefined} | ${{ rate: 19 }} | ${{ total: '38.59 cents', totalWithPrecision: '38.5915 cents', average: '5.51 cents/kWh', subAverage: '4.63 cents/kWh', subtotal: '32.43 cents', subtotalWithPrecision: '32.429832 cents', breakdown: [{ quantityUsed: '5 kWh', tierAmountDecimal: '5.5451 cents/kWh', totalAmountDecimal: '27.73 cents' }, { quantityUsed: '2 kWh', tierAmountDecimal: '5.433 cents/kWh', totalAmountDecimal: '10.87 cents' }] }} + ${decimalTiers} | ${7} | ${'kWh'} | ${'en'} | ${'EUR'} | ${undefined} | ${false} | ${true} | ${{ rate: 19 }} | ${{ total: 'Starts at 38.59 cents', totalWithPrecision: 'Starts at 38.5915 cents', average: '5.51 cents/kWh', subAverage: '4.63 cents/kWh', subtotal: 'Starts at 32.43 cents', subtotalWithPrecision: 'Starts at 32.429832 cents', breakdown: [{ quantityUsed: '5 kWh', tierAmountDecimal: '5.5451 cents/kWh', totalAmountDecimal: '27.73 cents' }, { quantityUsed: '2 kWh', tierAmountDecimal: '5.433 cents/kWh', totalAmountDecimal: '10.87 cents' }] }} `( 'should compute cumulative value correctly when quantityToSelectTier=$quantityToSelectTier, isTaxInclusive=$isTaxInclusive and tax=$tax', ({ @@ -105,4 +105,28 @@ describe('computeCumulativeValue', () => { expect(result).toEqual(expected); }, ); + + it('rounds the breakdown of a fractional quantity like the totals', () => { + // 1000 kWh a year, normalised to a monthly price: 83.333… kWh + const result = computeCumulativeValue( + [ + { up_to: 50, unit_amount: 34, unit_amount_decimal: '0.3456' }, + { up_to: null, unit_amount: 30, unit_amount_decimal: '0.30' }, + ], + 1000 / 12, + 'kWh', + 'en', + 'EUR', + mockedTranslationFn, + ); + + expect(result).toMatchObject({ + total: '€27.28', + breakdown: [ + // The unit price keeps its real precision. + { quantityUsed: '50 kWh', tierAmountDecimal: '34.56 cents/kWh', totalAmountDecimal: '€17.28' }, + { quantityUsed: '33.33 kWh', tierAmountDecimal: '30.00 cents/kWh', totalAmountDecimal: '€10.00' }, + ], + }); + }); }); diff --git a/src/tiers/compute-cumulative-value.ts b/src/tiers/compute-cumulative-value.ts index 1acca1c..8d12018 100644 --- a/src/tiers/compute-cumulative-value.ts +++ b/src/tiers/compute-cumulative-value.ts @@ -90,16 +90,21 @@ export const computeCumulativeValue = ( const unitAmountWithTax = isTaxInclusive ? unitAmount : unitAmount.multiply(1 + taxRate); breakdown.push({ + // Rounded for display like the totals: a quantity normalised from a yearly + // consumption is a repeating decimal (1000 / 12 = 83.333…). quantityUsed: `${graduatedQuantity.toLocaleString(formatOptions.locale, { - maximumFractionDigits: 6, + maximumFractionDigits: 2, })} ${formattedUnit}`, tierAmountDecimal: `${formatAmountFromString({ decimalAmount: addSeparatorToDineroString(unitAmountWithTax.getAmount().toString()), ...formatOptions, })}${formattedUnit ? `/${formattedUnit}` : ''}`, + // The tier's unit price keeps its real precision; its amount is rounded like `total`. totalAmountDecimal: formatAmountFromString({ decimalAmount: addSeparatorToDineroString(tierAmountWithTax.getAmount().toString()), ...formatOptions, + precision: 2, + useRealPrecision: false, }), });