diff --git a/src/__tests__/fixtures/coupon.samples.ts b/src/__tests__/fixtures/coupon.samples.ts index 289076d..e86cdcb 100644 --- a/src/__tests__/fixtures/coupon.samples.ts +++ b/src/__tests__/fixtures/coupon.samples.ts @@ -55,3 +55,8 @@ export const percentageCashbackCoupon: Coupon = { percentage_value: '10', cashback_period: '12', }; + +export const immediateFixedCashbackCoupon: Coupon = { + ...fixedCashbackCoupon, + cashback_period: '0', +}; diff --git a/src/__tests__/fixtures/price.samples.ts b/src/__tests__/fixtures/price.samples.ts index 23f0c35..8c6cd15 100644 --- a/src/__tests__/fixtures/price.samples.ts +++ b/src/__tests__/fixtures/price.samples.ts @@ -3081,3 +3081,21 @@ export const priceItemWithPercentageCashbackCoupon = { ...baseForPriceItemWithDiscount, _coupons: [percentageCashbackCoupon], }; + +export const recurringPriceItemWithFixedAmountCashbackCoupon = { + ...baseForPriceItemWithDiscount, + _price: { + ...baseForPriceItemWithDiscount._price, + type: 'recurring', + billing_period: 'monthly', + billing_duration_amount: 1, + billing_duration_unit: 'years', + notice_time_amount: 1, + notice_time_unit: 'months', + termination_time_amount: 2, + termination_time_unit: 'weeks', + renewal_duration_amount: 1, + renewal_duration_unit: 'years', + }, + _coupons: [fixedCashbackCoupon], +}; diff --git a/src/__tests__/fixtures/pricing.results.ts b/src/__tests__/fixtures/pricing.results.ts index ece7921..66d7941 100644 --- a/src/__tests__/fixtures/pricing.results.ts +++ b/src/__tests__/fixtures/pricing.results.ts @@ -7008,6 +7008,8 @@ export const computedPriceWithFixedAmountCashbackCoupon = { amount_total: 10000, cashback_amount: 1000, cashback_amount_decimal: '10', + after_cashback_amount_total: 9000, + after_cashback_amount_total_decimal: '90', amount_tax: 909, amount_subtotal_decimal: '90.909090909091', amount_total_decimal: '100', @@ -7141,6 +7143,156 @@ export const computedPriceWithPercentageCashbackCoupon = { amount_total: 10000, cashback_amount: 1000, cashback_amount_decimal: '10', + after_cashback_amount_total: 9000, + after_cashback_amount_total_decimal: '90', + amount_tax: 909, + amount_subtotal_decimal: '90.909090909091', + amount_total_decimal: '100', + }, + ], + currency: 'EUR', +}; + +/** + * Simple price with a fixed amount cashback coupon applied to it + */ +export const computedRecurringPriceWithFixedAmountCashbackCoupon = { + amount_subtotal: 9091, + amount_total: 10000, + amount_tax: 909, + total_details: { + amount_tax: 909, + breakdown: { + taxes: [ + { + tax: { + _id: '10', + type: 'VAT', + rate: 10, + }, + amount: 909, + }, + ], + recurrences: [ + { + type: 'recurring', + billing_period: 'monthly', + unit_amount_gross: 10000, + unit_amount_net: 9091, + amount_subtotal: 9091, + amount_total: 10000, + amount_subtotal_decimal: '90.909090909091', + amount_total_decimal: '100', + amount_tax: 909, + }, + ], + recurrencesByTax: [ + { + type: 'recurring', + billing_period: 'monthly', + amount_total: 10000, + amount_subtotal: 9091, + amount_tax: 909, + tax: { + tax: { + _id: '10', + type: 'VAT', + rate: 10, + }, + amount: 909, + }, + }, + ], + }, + }, + items: [ + { + quantity: 1, + product_id: 'prod-id#12324', + price_id: 'price#1', + taxes: [ + { + tax: { + _id: '10', + rate: 10, + type: 'VAT', + _schema: 'tax', + _org: '739224', + _title: '', + _created_at: '2022-06-29T20:26:19.020Z', + _updated_at: '2022-06-29T20:26:19.020Z', + }, + amount: 909, + }, + ], + _price: { + _id: 'price#1', + unit_amount: 10000, + unit_amount_currency: 'EUR', + unit_amount_decimal: '100', + type: 'recurring', + tax: [ + { + _id: '10', + rate: 10, + type: 'VAT', + _schema: 'tax', + _org: '739224', + _title: '', + _created_at: '2022-06-29T20:26:19.020Z', + _updated_at: '2022-06-29T20:26:19.020Z', + }, + ], + is_tax_inclusive: true, + description: 'Winter Sale', + _title: 'Winter Sale', + pricing_model: 'per_unit', + billing_period: 'monthly', + billing_duration_amount: 1, + billing_duration_unit: 'years', + notice_time_amount: 1, + notice_time_unit: 'months', + termination_time_amount: 2, + termination_time_unit: 'weeks', + renewal_duration_amount: 1, + renewal_duration_unit: 'years', + }, + _product: { + _tags: ['product-tag-1', 'product-tag-2'], + }, + pricing_model: 'per_unit', + is_tax_inclusive: true, + _coupons: [ + { + _id: 'coupon#3', + _schema: 'coupon', + _org: 'org#1', + _created_at: '2022-06-15T09:17:06.510Z', + _updated_at: '2022-06-17T11:48:20.104Z', + _title: 'Summer Cashback', + name: 'Summer Cashback', + type: 'fixed', + category: 'cashback', + fixed_value: 1000, + fixed_value_decimal: '10.00', + fixed_value_currency: 'EUR', + cashback_period: '12', + }, + ], + currency: 'EUR', + description: 'Winter Sale', + unit_amount: 10000, + unit_amount_net: 9091, + unit_amount_net_decimal: '90.909090909091', + unit_amount_gross: 10000, + unit_amount_gross_decimal: '100', + unit_amount_decimal: '100', + amount_subtotal: 9091, + amount_total: 10000, + cashback_amount: 1000, + cashback_amount_decimal: '10', + after_cashback_amount_total: 9917, + after_cashback_amount_total_decimal: '99.166666666667', amount_tax: 909, amount_subtotal_decimal: '90.909090909091', amount_total_decimal: '100', diff --git a/src/pricing.test.ts b/src/pricing.test.ts index 04d3ca5..966719c 100644 --- a/src/pricing.test.ts +++ b/src/pricing.test.ts @@ -524,6 +524,11 @@ describe('computeAggregatedAndPriceTotals', () => { const result = computeAggregatedAndPriceTotals([samples.priceItemWithPercentageCashbackCoupon]); expect(result).toEqual(results.computedPriceWithPercentageCashbackCoupon); }); + + it('should compute fixed amount cashbacks and totals correctly for recurring price', () => { + const result = computeAggregatedAndPriceTotals([samples.recurringPriceItemWithFixedAmountCashbackCoupon]); + expect(result).toEqual(results.computedRecurringPriceWithFixedAmountCashbackCoupon); + }); }); }); diff --git a/src/pricing.ts b/src/pricing.ts index 467e5fb..24a0537 100644 --- a/src/pricing.ts +++ b/src/pricing.ts @@ -784,6 +784,12 @@ export const computePriceItem = ( cashback_amount: itemValues.cashback_amount, }), ...(itemValues.cashback_amount_decimal && { cashback_amount_decimal: itemValues.cashback_amount_decimal }), + ...(Number.isInteger(itemValues.after_cashback_amount_total) && { + after_cashback_amount_total: itemValues.after_cashback_amount_total, + }), + ...(itemValues.after_cashback_amount_total_decimal && { + after_cashback_amount_total_decimal: itemValues.after_cashback_amount_total_decimal, + }), ...(itemValues.before_discount_amount_total && { before_discount_amount_total: itemValues.before_discount_amount_total, }), @@ -891,6 +897,12 @@ const convertPriceItemPrecision = (priceItem: PriceItem, precision = 2): PriceIt cashback_amount: toDineroFromInteger(priceItem.cashback_amount).convertPrecision(precision).getAmount(), cashback_amount_decimal: toDineroFromInteger(priceItem.cashback_amount).toUnit().toString(), }), + ...(typeof priceItem.after_cashback_amount_total === 'number' && { + after_cashback_amount_total: toDineroFromInteger(priceItem.after_cashback_amount_total) + .convertPrecision(precision) + .getAmount(), + after_cashback_amount_total_decimal: toDineroFromInteger(priceItem.after_cashback_amount_total).toUnit().toString(), + }), amount_tax: toDineroFromInteger(priceItem.amount_tax || 0) .convertPrecision(precision) .getAmount(), diff --git a/src/utils/index.ts b/src/utils/index.ts index 6f65b23..780f33f 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -3,8 +3,9 @@ import type { Currency, Dinero } from 'dinero.js'; import { DEFAULT_CURRENCY } from '../currencies'; import { toDineroFromInteger, toDinero } from '../formatters'; import { TaxRates } from '../formatters/constants'; +import { normalizeTimeFrequencyFromDineroInputValue } from '../normalizers'; import { MarkupPricingModel, TypeGetAg } from '../pricing'; -import type { Price, PriceGetAg, PriceItem, PriceItemDto, PriceTier, Tax } from '../types'; +import type { BillingPeriod, Price, PriceGetAg, PriceItem, PriceItemDto, PriceTier, Tax } from '../types'; import { isPercentageCoupon, isValidCoupon, isCashbackCoupon, isFixedValueCoupon } from './guards/coupon'; @@ -29,6 +30,8 @@ export type PriceItemsTotals = Pick< | 'before_discount_amount_total' | 'cashback_amount' | 'cashback_amount_decimal' + | 'after_cashback_amount_total' + | 'after_cashback_amount_total_decimal' | 'get_ag' | 'tiers_details' > & { @@ -121,6 +124,7 @@ export const computePriceItemValues = ( let unitDiscountAmount: Dinero | undefined; let unitDiscountAmountNet: Dinero | undefined; let cashbackAmount: Dinero | undefined; + let afterCashbackAmountTotal: Dinero | undefined; if (coupon) { if (isCashbackCoupon(coupon)) { @@ -130,6 +134,14 @@ export const computePriceItemValues = ( const cashbackPercentage = clamp(Number(coupon.percentage_value), 0, 100); cashbackAmount = unitAmount.multiply(cashbackPercentage).divide(100); } + + const normalizedCashbackAmount = normalizeTimeFrequencyFromDineroInputValue( + cashbackAmount, + 'yearly', + priceItem?._price?.billing_period as BillingPeriod, + ); + + afterCashbackAmountTotal = unitAmount.subtract(normalizedCashbackAmount); } else { unitAmountBeforeDiscount = unitAmount; if (isPercentageCoupon(coupon)) { @@ -209,6 +221,8 @@ export const computePriceItemValues = ( ...(cashbackAmount && { cashback_amount: cashbackAmount.getAmount(), cashback_amount_decimal: cashbackAmount.toUnit().toString(), + after_cashback_amount_total: afterCashbackAmountTotal?.getAmount(), + after_cashback_amount_total_decimal: afterCashbackAmountTotal?.toUnit().toString(), }), }; };