diff --git a/src/__tests__/fixtures/price.samples.ts b/src/__tests__/fixtures/price.samples.ts index 7866216..d1966b6 100644 --- a/src/__tests__/fixtures/price.samples.ts +++ b/src/__tests__/fixtures/price.samples.ts @@ -3552,3 +3552,180 @@ export const compositePriceCashbackCombinedWithComponentCashbacks: CompositePric ], }, }; + +export const unorderedPriceItemsOneTimeRecurrencesWithDiscount = [ + { + description: 'Price 1 One time - no discount', + pricing_model: 'per_unit', + unit_amount: 10000000, + unit_amount_currency: 'EUR', + unit_amount_decimal: '100000', + is_tax_inclusive: true, + price_display_in_journeys: 'show_price', + active: true, + type: 'one_time', + billing_duration_unit: 'months', + notice_time_unit: 'months', + termination_time_unit: 'months', + renewal_duration_unit: 'months', + _tags: [], + _schema: 'price', + _id: '2f922147-f3eb-46d6-8ef0-ce35a8542466', + _org: '739224', + _owners: [ + { + org_id: '739224', + user_id: '10009151', + }, + ], + _created_at: '2025-11-26T13:37:10.419Z', + _updated_at: '2025-11-26T13:37:10.419Z', + internal_description: 'Price 1 One time - no discount', + billing_period: 'weekly', + _title: 'Price 1 One time - no discount', + _viewers: {}, + _messages: {}, + _coupons: [], + blockMappingData: {}, + quantity: 1, + }, + { + description: 'Price 2 One time - with discount', + pricing_model: 'per_unit', + unit_amount: 1000000, + unit_amount_currency: 'EUR', + unit_amount_decimal: '10000', + is_tax_inclusive: true, + price_display_in_journeys: 'show_price', + active: true, + type: 'one_time', + billing_duration_unit: 'months', + notice_time_unit: 'months', + termination_time_unit: 'months', + renewal_duration_unit: 'months', + _tags: [], + _schema: 'price', + _id: '4d5b79c9-a3b6-45d7-b078-1b78dd9f4b9e', + _org: '739224', + _created_at: '2025-11-26T13:38:22.953Z', + _updated_at: '2025-11-26T13:38:22.953Z', + internal_description: 'Price 2 One time - with discount', + billing_period: 'weekly', + _title: 'Price 2 One time - with discount', + _viewers: {}, + _messages: {}, + _coupons: [ + { + name: 'PROMO 20', + type: 'percentage', + fixed_value: 0, + fixed_value_currency: 'EUR', + fixed_value_decimal: '0.00', + category: 'discount', + active: true, + prices: [ + { + description: 'Price Issue 2 (PROMO)', + pricing_model: 'per_unit', + unit_amount: 1000000, + unit_amount_currency: 'EUR', + unit_amount_decimal: '10000', + is_tax_inclusive: true, + price_display_in_journeys: 'show_price', + active: true, + type: 'one_time', + billing_duration_unit: 'months', + notice_time_unit: 'months', + termination_time_unit: 'months', + renewal_duration_unit: 'months', + _tags: [], + _schema: 'price', + _id: '4d5b79c9-a3b6-45d7-b078-1b78dd9f4b9e', + _org: '739224', + _owners: [ + { + org_id: '739224', + user_id: '10009151', + }, + ], + _created_at: '2025-11-26T13:38:22.953Z', + _updated_at: '2025-11-26T13:38:22.953Z', + internal_description: 'Price Issue 2 (PROMO)', + billing_period: 'weekly', + _title: 'Price Issue 2 (PROMO)', + _acl: { + view: ['org_739224'], + edit: ['org_739224'], + delete: ['org_739224'], + }, + $relation: { + entity_id: '4d5b79c9-a3b6-45d7-b078-1b78dd9f4b9e', + _schema: 'price', + _tags: [], + }, + }, + ], + _schema: 'coupon', + percentage_value: '20', + _id: 'bd801823-e4d2-47cb-bdfa-58e40392055f', + _org: '739224', + _owners: [ + { + org_id: '739224', + user_id: '10009151', + }, + ], + _created_at: '2025-11-26T13:39:37.146Z', + _updated_at: '2025-11-26T13:39:37.146Z', + cashback_period: '0', + _title: 'PROMO 20', + _acl: { + view: ['org_739224'], + edit: ['org_739224'], + delete: ['org_739224'], + }, + _relations: [ + { + entity_id: '4d5b79c9-a3b6-45d7-b078-1b78dd9f4b9e', + _acl: { + view: ['org_739224'], + edit: ['org_739224'], + delete: ['org_739224'], + }, + }, + ], + _viewers: {}, + _messages: {}, + }, + ], + blockMappingData: {}, + quantity: 1, + }, + { + description: 'Price 3 One time - no discount', + pricing_model: 'per_unit', + unit_amount: 100000, + unit_amount_currency: 'EUR', + unit_amount_decimal: '1000', + is_tax_inclusive: true, + price_display_in_journeys: 'show_price', + active: true, + type: 'one_time', + billing_duration_unit: 'months', + notice_time_unit: 'months', + termination_time_unit: 'months', + renewal_duration_unit: 'months', + _tags: [], + _schema: 'price', + _id: '5d3f3968-6e94-4934-b282-517d990d62e2', + _org: '739224', + _created_at: '2025-11-26T13:39:11.980Z', + _updated_at: '2025-11-26T13:39:11.980Z', + internal_description: 'Price 3 One time - no discount', + billing_period: 'weekly', + _title: 'Price 3 One time - no discount', + _coupons: [], + blockMappingData: {}, + quantity: 1, + }, +] as PriceItemDto[]; diff --git a/src/computations/compute-totals.test.ts b/src/computations/compute-totals.test.ts index 66ce8f8..ffe7a8a 100644 --- a/src/computations/compute-totals.test.ts +++ b/src/computations/compute-totals.test.ts @@ -816,6 +816,17 @@ describe('computeAggregatedAndPriceTotals', () => { ); expect(result).toEqual(results.computedCompositePriceWithComponentsWithPromoCodeRequiredCoupon); }); + it('should compute the pricing total breakdown details with discounts correctly independently of the order of the items', () => { + const result = computeAggregatedAndPriceTotals(samples.unorderedPriceItemsOneTimeRecurrencesWithDiscount, { + redeemedPromos: [], + }); + expect(result.total_details?.breakdown?.recurrences?.[0]?.amount_total_decimal).toEqual('109000'); + expect(result.total_details?.breakdown?.recurrences?.[0]?.amount_total).toEqual(10900000); + expect(result.total_details?.breakdown?.recurrences?.[0]?.before_discount_amount_total_decimal).toEqual('111000'); + expect(result.total_details?.breakdown?.recurrences?.[0]?.before_discount_amount_total).toEqual(11100000); + expect(result.total_details?.breakdown?.recurrences?.[0]?.discount_amount_decimal).toEqual('2000'); + expect(result.total_details?.breakdown?.recurrences?.[0]?.discount_amount).toEqual(200000); + }); }); it('computes the pricing details for a simple price', () => { diff --git a/src/computations/compute-totals.ts b/src/computations/compute-totals.ts index 11fa9c5..bf23117 100644 --- a/src/computations/compute-totals.ts +++ b/src/computations/compute-totals.ts @@ -282,9 +282,13 @@ const recomputeDetailTotals = ( typeof recurrence.discount_amount !== 'undefined' ? toDineroFromInteger(recurrence.discount_amount) : undefined; if (priceBeforeDiscountAmountTotal || existingRecurrenceBeforeDiscountAmountTotal) { + // If recurrence doesn't have before_discount_amount_total yet, initialize it with current total (before adding this item) + const initializedExistingTotal = + existingRecurrenceBeforeDiscountAmountTotal ?? + toDineroFromInteger(recurrence.amount_total).subtract(priceTotal); + const baseAmount = priceBeforeDiscountAmountTotal || priceTotal; - const recurrenceBeforeDiscountAmountTotal = - existingRecurrenceBeforeDiscountAmountTotal?.add(baseAmount) ?? baseAmount; + const recurrenceBeforeDiscountAmountTotal = initializedExistingTotal.add(baseAmount); recurrence.before_discount_amount_total = recurrenceBeforeDiscountAmountTotal.getAmount(); recurrence.before_discount_amount_total_decimal = recurrenceBeforeDiscountAmountTotal.toUnit().toString(); }