Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/exports.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const expectedNamedExports = [
'extractGetAgConfig',
'getAmountWithTax',
'getTaxValue',
'computePriceDiff',
'isVariablePrice',
'isVariablePriceItem',
];
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,5 @@ export { formatFeeAmountFromString } from './getag/formatters';
export { extractGetAgConfig } from './getag/extract-config';
export { getTaxValue } from './taxes/get-tax-value';
export { getAmountWithTax } from './taxes/get-amount-with-tax';
export { computePriceDiff } from './prices/compute-price-diff';
export type { PriceDiff, PriceDiffFormat, PriceDiffOptions } from './prices/compute-price-diff';
64 changes: 64 additions & 0 deletions src/prices/compute-price-diff.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { describe, expect, it } from 'vitest';
import { computePriceDiff } from './compute-price-diff';

describe('computePriceDiff', () => {
describe('returns null', () => {
it('when current amount is zero', () => {
expect(computePriceDiff('10.00', '0.00', { format: 'absolute' })).toBeNull();
});

it('when offer and current amounts are equal', () => {
expect(computePriceDiff('30.00', '30.00', { format: 'absolute' })).toBeNull();
});

it('when offer is worse and onlyIfBetter is true', () => {
expect(computePriceDiff('40.00', '30.00', { format: 'absolute', onlyIfBetter: true })).toBeNull();
});
});

describe('percentage format', () => {
it('returns correct percentage when offer is better (lower)', () => {
const result = computePriceDiff('25.00', '30.00', { format: 'percentage' });
expect(result).not.toBeNull();
expect(result!.isBetter).toBe(true);
expect(result!.formattedDiff).toBe('16.7%');
});

it('returns correct percentage when offer is worse (higher)', () => {
const result = computePriceDiff('36.00', '30.00', { format: 'percentage' });
expect(result).not.toBeNull();
expect(result!.isBetter).toBe(false);
expect(result!.formattedDiff).toBe('20.0%');
});
});

describe('absolute format', () => {
it('returns formatted absolute diff when offer is better', () => {
const result = computePriceDiff('25.00', '30.00', { format: 'absolute', currency: 'EUR', locale: 'de-DE' });
expect(result).not.toBeNull();
expect(result!.isBetter).toBe(true);
expect(result!.formattedDiff).toContain('5');
});

it('returns formatted absolute diff when offer is worse', () => {
const result = computePriceDiff('35.00', '30.00', { format: 'absolute', currency: 'EUR', locale: 'de-DE' });
expect(result).not.toBeNull();
expect(result!.isBetter).toBe(false);
expect(result!.formattedDiff).toContain('5');
});
});

describe('onlyIfBetter', () => {
it('returns result when offer is better and onlyIfBetter is true', () => {
const result = computePriceDiff('20.00', '30.00', { format: 'percentage', onlyIfBetter: true });
expect(result).not.toBeNull();
expect(result!.isBetter).toBe(true);
});

it('returns result when offer is worse and onlyIfBetter is false', () => {
const result = computePriceDiff('40.00', '30.00', { format: 'percentage', onlyIfBetter: false });
expect(result).not.toBeNull();
expect(result!.isBetter).toBe(false);
});
});
});
70 changes: 70 additions & 0 deletions src/prices/compute-price-diff.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import type { Currency as DineroCurrency } from 'dinero.js';
import { DEFAULT_CURRENCY, DEFAULT_INTEGER_AMOUNT_PRECISION } from '../money/constants';
import { formatAmount, parseDecimalValue } from '../money/formatters';
import { toDinero } from '../money/to-dinero';
import type { Currency } from '../shared/types';

export type PriceDiffFormat = 'percentage' | 'absolute';

export type PriceDiffOptions = {
format: PriceDiffFormat;
onlyIfBetter?: boolean;
currency?: string;
locale?: string;
};

export type PriceDiff = {
isBetter: boolean;
formattedDiff: string;
};

/**
* Computes the price difference between an offer price and a current price,
* returning the formatted savings or increase for display purposes.
*
* @param offerAmount - The new/offer price as a decimal string (e.g. "25.00")
* @param currentAmount - The current/reference price as a decimal string (e.g. "30.00")
* @param options - Configuration: format, onlyIfBetter, currency, locale
* @returns `{ isBetter, formattedDiff }` or `null` if not applicable
*/
export const computePriceDiff = (
offerAmount: string,
currentAmount: string,
options: PriceDiffOptions,
): PriceDiff | null => {
const { format, onlyIfBetter = false, currency, locale } = options;
const resolvedCurrency = (currency || DEFAULT_CURRENCY) as DineroCurrency;

const currentDinero = toDinero(parseDecimalValue(currentAmount), resolvedCurrency);
const offerDinero = toDinero(parseDecimalValue(offerAmount), resolvedCurrency);

if (currentDinero.isZero()) return null;

const diff = currentDinero.subtract(offerDinero);

if (diff.isZero()) return null;

const isBetter = !diff.isNegative();

if (onlyIfBetter && !isBetter) return null;

const resolvedLocale = locale ?? (typeof navigator !== 'undefined' ? navigator.language : undefined);

const absDiff = diff.isNegative() ? diff.multiply(-1) : diff;

let formattedDiff: string;

if (format === 'percentage') {
const percentage = (absDiff.getAmount() / currentDinero.getAmount()) * 100;

formattedDiff = `${percentage.toFixed(1)}%`;
} else {
formattedDiff = formatAmount({
amount: absDiff.convertPrecision(DEFAULT_INTEGER_AMOUNT_PRECISION).getAmount(),
currency: resolvedCurrency as Currency,
locale: resolvedLocale,
});
}

return { isBetter, formattedDiff };
};
Loading