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
5 changes: 5 additions & 0 deletions .changeset/major-weeks-stare.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@epilot/pricing': patch
---

Include each cashback individually in computation result
12 changes: 10 additions & 2 deletions src/__tests__/fixtures/pricing.results.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9368,7 +9368,11 @@ export const computedCompositePriceWithComponentsWithCashbacks = {
cashbacks: [
{
cashback_period: '12',
amount_total: 2000,
amount_total: 1000,
},
{
cashback_period: '12',
amount_total: 1000,
},
],
},
Expand Down Expand Up @@ -9771,7 +9775,11 @@ export const computedCompositePriceWithComponentsWithCashbacks = {
cashbacks: [
{
cashback_period: '12',
amount_total: 2000,
amount_total: 1000,
},
{
cashback_period: '12',
amount_total: 1000,
},
],
},
Expand Down
21 changes: 8 additions & 13 deletions src/computations/compute-composite-price-cashbacks.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getAppliedCompositeCashbackCoupons } from '../coupons/utils';
import { toDinero, toDineroFromInteger } from '../money/to-dinero';
import { toDinero } from '../money/to-dinero';
import { convertCashbackAmountsPrecision } from '../prices/convert-precision';
import { getSafeQuantity } from '../shared/get-safe-quantity';
import type { RedeemedPromo, PricingDetails, CompositePriceItem, Dinero, Coupon } from '../shared/types';
Expand Down Expand Up @@ -35,18 +35,13 @@ export const computeCompositePriceCashbacks = (
cashback_amount_decimal: cashbackAmountWithPrecision.cashback_amount_decimal!,
});

// Update existing breakdown
const cashbackMatch = cashbacks.find((cashback) => cashback.cashback_period === cashbackPeriod);

if (cashbackMatch) {
const cashbackAmountTotal = toDineroFromInteger(cashbackMatch.amount_total);
cashbackMatch.amount_total = cashbackAmountTotal.add(toDineroFromInteger(cashback_amount)).getAmount();
} else {
cashbacks.push({
cashback_period: cashbackPeriod,
amount_total: cashback_amount,
});
}
// Preserve one entry per applied cashback rather than summing entries
// that share the same cashback_period, so consumers can render each
// cashback as its own line.
cashbacks.push({
cashback_period: cashbackPeriod,
amount_total: cashback_amount,
});
}

return {
Expand Down
22 changes: 16 additions & 6 deletions src/computations/compute-recurrence-after-cashback-amounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,31 @@ import { normalizeTimeFrequencyFromDineroInputValue } from '../time-frequency/no
/**
* Recurrence amounts after cashbacks can only be computed
* after all recurrences and cashbacks have been computed.
*
* Each cashback entry is subtracted from the recurrence total. Multiple
* entries may share the same cashback_period (one entry per applied
* cashback coupon) so we accumulate across the full array.
*/
export const computeRecurrenceAfterCashbackAmounts = (recurrence: RecurrenceAmount, cashbacks: CashbackAmount[]) => {
/* Only the first cashback is taken into account */
const cashback = cashbacks[0];
if (!recurrence.type) {
return recurrence;
}

const validCashbacks = cashbacks.filter(Boolean);

if (!cashback || !recurrence.type) {
if (!validCashbacks.length) {
return recurrence;
}

const cashbackAmount = toDineroFromInteger(cashback.amount_total);
const summedCashback = validCashbacks.reduce(
(acc, cashback) => acc.add(toDineroFromInteger(cashback.amount_total)),
toDineroFromInteger(0),
);

const normalizedCashbackAmount =
recurrence.type === 'recurring'
? normalizeTimeFrequencyFromDineroInputValue(cashbackAmount, 'yearly', recurrence.billing_period!)
: cashbackAmount;
? normalizeTimeFrequencyFromDineroInputValue(summedCashback, 'yearly', recurrence.billing_period!)
: summedCashback;

const afterCashbackAmountTotal = toDineroFromInteger(recurrence.amount_total).subtract(normalizedCashbackAmount);

Expand Down
36 changes: 29 additions & 7 deletions src/computations/compute-totals.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -910,7 +910,7 @@ it('should compute multiple fixed cashbacks correctly when applied at the compos
expect(computedPriceItem?.total_details?.breakdown?.cashbacks?.[1].amount_total).toEqual(500);
});

it('should compute multiple fixed cashbacks correctly when applied at the composite price level with same cashback periods', () => {
it('should preserve each cashback as its own breakdown entry when multiple coupons share the same cashback period', () => {
const priceItems = [
{
...samples.compositePriceWithFixedCashbackCoupon,
Expand All @@ -933,9 +933,30 @@ it('should compute multiple fixed cashbacks correctly when applied at the compos
cashback_period: '12',
},
]);
expect(computedPriceItem?.total_details?.breakdown?.cashbacks?.length).toEqual(1);
expect(computedPriceItem?.total_details?.breakdown?.cashbacks?.[0].cashback_period).toEqual('12');
expect(computedPriceItem?.total_details?.breakdown?.cashbacks?.[0].amount_total).toEqual(2000);
expect(computedPriceItem?.total_details?.breakdown?.cashbacks).toEqual([
expect.objectContaining({ cashback_period: '12', amount_total: 1000 }),
expect.objectContaining({ cashback_period: '12', amount_total: 1000 }),
]);
});

it('should preserve each cashback as its own entry when same-period coupons have different amounts', () => {
const sameImmediatePeriodCashback = {
...coupons.lowFixedCashbackCoupon,
fixed_value: 250,
fixed_value_decimal: '2.50',
};
const priceItems = [
{
...samples.compositePriceWithFixedCashbackCoupon,
_coupons: [coupons.lowFixedCashbackCoupon, sameImmediatePeriodCashback],
},
];
const result = computeAggregatedAndPriceTotals(priceItems);
const computedPriceItem = result.items?.[0] as CompositePriceItem;
expect(computedPriceItem?.total_details?.breakdown?.cashbacks).toEqual([
expect.objectContaining({ cashback_period: '0', amount_total: 500 }),
expect.objectContaining({ cashback_period: '0', amount_total: 250 }),
]);
});

it('should compute fixed cashbacks correctly when applied at the composite price level + component level with the same cashback period', () => {
Expand All @@ -953,9 +974,10 @@ it('should compute fixed cashbacks correctly when applied at the composite price
expect(computedPriceItem?.item_components?.[1].cashback_amount_decimal).toEqual('10');
expect(computedPriceItem?.item_components?.[1].after_cashback_amount_total).toEqual(9981);
expect(computedPriceItem?.item_components?.[1].after_cashback_amount_total_decimal).toEqual('99.807692307692');
expect(computedPriceItem?.total_details?.breakdown?.cashbacks?.length).toEqual(1);
expect(computedPriceItem?.total_details?.breakdown?.cashbacks?.[0].cashback_period).toEqual('12');
expect(computedPriceItem?.total_details?.breakdown?.cashbacks?.[0].amount_total).toEqual(2000);
expect(computedPriceItem?.total_details?.breakdown?.cashbacks).toEqual([
expect.objectContaining({ cashback_period: '12', amount_total: 1000 }),
expect.objectContaining({ cashback_period: '12', amount_total: 1000 }),
]);
});

it('should compute fixed cashbacks correctly when applied at the composite price level + component level with different cashback periods', () => {
Expand Down
19 changes: 7 additions & 12 deletions src/computations/compute-totals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -360,19 +360,14 @@ const recomputeDetailTotals = (
? toDineroFromInteger(priceItemToAppend.cashback_amount!)
: undefined;

// Cashback totals
// Cashback totals — preserve one entry per applied cashback rather than
// summing entries that share the same cashback_period, so consumers can
// render each cashback as its own line.
if (priceCashBackAmount && Boolean(coupon)) {
const cashbackMatch = cashbacks.find((cashback) => cashback.cashback_period === cashbackPeriod);

if (cashbackMatch) {
const cashbackAmountTotal = toDineroFromInteger(cashbackMatch.amount_total);
cashbackMatch.amount_total = cashbackAmountTotal.add(priceCashBackAmount).getAmount();
} else {
cashbacks.push({
cashback_period: cashbackPeriod,
amount_total: priceCashBackAmount.getAmount(),
});
}
cashbacks.push({
cashback_period: cashbackPeriod,
amount_total: priceCashBackAmount.getAmount(),
});
}

// Remove empty cashbacks from the breakdown
Expand Down
Loading