diff --git a/package-lock.json b/package-lock.json index 54c06ea7..5d8dd8a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@epilot/pricing", - "version": "4.12.9", + "version": "4.12.10-0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@epilot/pricing", - "version": "4.12.9", + "version": "4.12.10-0", "license": "UNLICENSED", "dependencies": { "@epilot/pricing-client": "^3.11.3-0", diff --git a/package.json b/package.json index 9c4f7aa8..6f4afb0a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@epilot/pricing", - "version": "4.12.9", + "version": "4.12.10-0", "description": "Pricing Library", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -21,7 +21,7 @@ "gen-docs": "rm -rf docs && typedoc --plugin typedoc-plugin-markdown --out docs src && echo '{ \"label\": \"Pricing Library\", \"position\": 0 }' > docs/_category_.json", "clean": "rm -rf dist", "bundle-definition": "webpack --mode=production", - "build": "npm run clean && tsc && mkdir -p dist && cp -r src/types dist/types && npm run bundle-definition", + "build": "npm run clean && tsc && mkdir -p dist && cp -r src/types dist/types && npm run bundle-definition && npm run gen-docs", "typegen": "echo '/* eslint-disable */\n/**\n * DO NOT MODIFY - GENERATED TYPES FROM OPENAPI\n **/' > src/types/pricing-types.d.ts && typegen ../../lambda/ApiHandlerFunction/openapi.yml >> src/types/pricing-types.d.ts", "prepare": "npm run build", "lint": "esw src --ext js,ts,tsx --cache --color", diff --git a/src/pricing.test.ts b/src/pricing.test.ts index 6cbab708..3c1f7b17 100644 --- a/src/pricing.test.ts +++ b/src/pricing.test.ts @@ -19,7 +19,7 @@ import { } from './types'; describe('computeAggregatedAndPriceTotals', () => { - describe('when is_composite_price = false', () => { + describe('when computing Simple Prices (is_composite_price = false)', () => { it('should return the right result when there is one item per recurrence', () => { const priceItems: PriceItemDto[] = [ samples.priceItem1, @@ -483,7 +483,7 @@ describe('computeAggregatedAndPriceTotals', () => { amount_subtotal: -1818, amount_total: -2000, total_details: expect.objectContaining({ - amount_tax: -182, + amount_tax: 0, }), items: expect.arrayContaining([ expect.objectContaining({ @@ -525,7 +525,7 @@ describe('computeAggregatedAndPriceTotals', () => { amount_subtotal: -32727, amount_total: -36000, total_details: expect.objectContaining({ - amount_tax: -3273, + amount_tax: 0, }), items: expect.arrayContaining([ expect.objectContaining({ @@ -854,7 +854,7 @@ describe('computeAggregatedAndPriceTotals', () => { amount_subtotal: -2727, amount_total: -3000, total_details: expect.objectContaining({ - amount_tax: -273, + amount_tax: 0, }), items: expect.arrayContaining([ expect.objectContaining({ @@ -890,7 +890,7 @@ describe('computeAggregatedAndPriceTotals', () => { amount_subtotal: -54545, amount_total: -60000, total_details: expect.objectContaining({ - amount_tax: -5455, + amount_tax: 0, }), items: expect.arrayContaining([ expect.objectContaining({ @@ -1252,7 +1252,7 @@ describe('computeAggregatedAndPriceTotals', () => { amount_subtotal: -9091, amount_total: -10000, total_details: expect.objectContaining({ - amount_tax: -909, + amount_tax: 0, }), items: expect.arrayContaining([ expect.objectContaining({ @@ -1283,7 +1283,7 @@ describe('computeAggregatedAndPriceTotals', () => { amount_subtotal: -5455, amount_total: -6000, total_details: expect.objectContaining({ - amount_tax: -545, + amount_tax: 0, }), items: expect.arrayContaining([ expect.objectContaining({ @@ -1296,9 +1296,178 @@ describe('computeAggregatedAndPriceTotals', () => { ); }); }); + + describe('Negative Prices', () => { + it('should compute taxes as zero when dealing with simple negative prices, even if a tax is specified', () => { + const priceItems: PriceItemDto[] = [ + { + ...samples.priceItem1, + _price: { + ...(samples.priceItem1._price as Price), + is_tax_inclusive: true, + }, + unit_amount: -10000, + unit_amount_decimal: '-100.00', + quantity: 1, + }, + ]; + + const result = computeAggregatedAndPriceTotals(priceItems); + + expect(result).toEqual( + expect.objectContaining({ + amount_subtotal: -8403, + amount_total: -10000, + }), + ); + + expect(result.total_details).toEqual( + expect.objectContaining({ + amount_tax: 0, + }), + ); + + expect(result.total_details?.breakdown).toEqual( + expect.objectContaining({ + recurrences: [ + expect.objectContaining({ + amount_subtotal: -8403, + amount_tax: 0, + amount_total: -10000, + type: 'one_time', + }), + ], + taxes: [{ amount: 0, tax: { _id: '19', rate: 19, type: 'VAT' } }], + }), + ); + + expect(result.items?.[0]).toEqual( + expect.objectContaining({ + _price: expect.any(Object), + _product: expect.any(Object), + amount_subtotal: -8403, + amount_total: -10000, + taxes: [ + { + amount: 0, + tax: expect.objectContaining({ + _id: '19', + rate: 19, + }), + }, + ], + unit_amount: -10000, + unit_amount_decimal: '-100.00', + unit_amount_gross: -10000, + unit_amount_net: -8403, + }), + ); + }); + + it('should compute taxes as zero when dealing with a composite with negative price components, even if a tax is specified', () => { + const priceItems: CompositePriceItemDto[] = [ + { + ...samples.compositePriceItemWithNegativePriceFlatFee, + quantity: 1, + }, + ]; + + const result = computeAggregatedAndPriceTotals(priceItems); + + expect(result).toEqual( + expect.objectContaining({ + amount_subtotal: -9091, + amount_total: -10000, + }), + ); + + expect(result.total_details).toEqual( + expect.objectContaining({ + amount_tax: 0, + }), + ); + + expect(result.total_details?.breakdown).toEqual( + expect.objectContaining({ + recurrences: [ + expect.objectContaining({ + amount_subtotal: -9091, + amount_tax: 0, + amount_total: -10000, + type: 'one_time', + }), + ], + taxes: [{ amount: 0, tax: { _id: '10', rate: 10, type: 'VAT' } }], + }), + ); + + expect(result.items?.[0]).toEqual(expect.objectContaining({ + _price: { is_composite_price: true }, + amount_subtotal: -9091, + amount_total: -10000, + item_components: [ + { + _price: { + _id: 'price#1-tiered-flat-fee', + is_tax_inclusive: true, + pricing_model: 'tiered_flatfee', + tiers: [ + { flat_fee_amount: -10000, flat_fee_amount_decimal: '-100.00', up_to: 5 }, + { flat_fee_amount: -8000, flat_fee_amount_decimal: '-80.00', up_to: 10 }, + { flat_fee_amount: -6000, flat_fee_amount_decimal: '-60.00' }, + ], + }, + _product: undefined, + amount_subtotal: -9091, + amount_total: -10000, + currency: 'EUR', + price_id: 'price#1-tiered-flat-fee', + pricing_model: 'tiered_flatfee', + product_id: undefined, + quantity: 1, + taxes: [ + { + amount: 0, + tax: { + _created_at: '2022-06-29T20:26:19.020Z', + _id: '10', + _org: '739224', + _schema: 'tax', + _title: '', + _updated_at: '2022-06-29T20:26:19.020Z', + rate: 10, + type: 'VAT', + }, + }, + ], + type: undefined, + unit_amount: 0, + unit_amount_decimal: '0.0', + unit_amount_gross: -10000, + }, + ], + quantity: 1, + total_details: { + amount_tax: 0, + breakdown: { + recurrences: [ + { + amount_subtotal: -9091, + amount_tax: 0, + amount_total: -10000, + type: 'one_time', + unit_amount_gross: -10000, + }, + ], + taxes: [{ amount: 0, tax: { _id: '10', rate: 10, type: 'VAT' } }], + }, + } + })); + }); + }); }); - describe('when is_composite_price = true', () => { + describe('when computing with Composite Prices (is_composite_price = true)', () => { it('should return 0 when number input is 0', () => { const priceItems: CompositePriceItemDto[] = [samples.compositePriceWithNumberInputEqualsToZero]; expect(computeAggregatedAndPriceTotals(priceItems)).toStrictEqual( @@ -1409,7 +1578,7 @@ describe('computeAggregatedAndPriceTotals', () => { }); }); - describe('when computing custom items', () => { + describe('when computing individual adjustments', () => { it('should compute prices correctly when computing a composite price item with a custom item inside', () => { const priceItems = [samples.compositePriceWithCustomItem]; @@ -1653,7 +1822,7 @@ describe('computeAggregatedAndPriceTotals', () => { amount_subtotal: -1818, amount_total: -2000, total_details: expect.objectContaining({ - amount_tax: -182, + amount_tax: 0, }), items: expect.arrayContaining([ expect.objectContaining({ @@ -1684,7 +1853,7 @@ describe('computeAggregatedAndPriceTotals', () => { amount_subtotal: -60000, unit_amount_gross: -2400, total_details: expect.objectContaining({ - amount_tax: -6000, + amount_tax: 0, }), items: expect.arrayContaining([ expect.objectContaining({ @@ -1841,7 +2010,7 @@ describe('computeAggregatedAndPriceTotals', () => { amount_subtotal: -909, amount_total: -1000, total_details: expect.objectContaining({ - amount_tax: -91, + amount_tax: 0, }), items: expect.arrayContaining([ expect.objectContaining({ @@ -1872,7 +2041,7 @@ describe('computeAggregatedAndPriceTotals', () => { amount_subtotal: -54545, unit_amount_gross: -600, total_details: expect.objectContaining({ - amount_tax: -5455, + amount_tax: 0, }), items: expect.arrayContaining([ expect.objectContaining({ @@ -2029,7 +2198,7 @@ describe('computeAggregatedAndPriceTotals', () => { amount_subtotal: -9091, amount_total: -10000, total_details: expect.objectContaining({ - amount_tax: -909, + amount_tax: 0, }), items: expect.arrayContaining([ expect.objectContaining({ @@ -2060,7 +2229,7 @@ describe('computeAggregatedAndPriceTotals', () => { amount_subtotal: -5455, amount_total: -6000, total_details: expect.objectContaining({ - amount_tax: -545, + amount_tax: 0, }), items: expect.arrayContaining([ expect.objectContaining({ @@ -2074,7 +2243,9 @@ describe('computeAggregatedAndPriceTotals', () => { }); }); }); +}); +describe('Utility Functions', () => { describe('extractPricingEntitiesBySlug', () => { it('should return the pricing relations without duplicates', () => { const priceItems = [ diff --git a/src/pricing.ts b/src/pricing.ts index 7684f9b6..0dbfe240 100644 --- a/src/pricing.ts +++ b/src/pricing.ts @@ -543,7 +543,7 @@ export const computePriceItem = ( taxes: [ { ...(priceTax ? { tax: priceTax } : { rate: 'nontaxable', rateValue: 0 }), - amount: itemValues.taxAmount, + amount: Number(itemValues.unitAmount) < 0 ? 0 : itemValues.taxAmount, }, ], _price: { diff --git a/src/utils/index.ts b/src/utils/index.ts index 7d30c462..23731dde 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -86,7 +86,7 @@ export const computePriceItemValues = ( const amountSubtotal = unitAmountNet.multiply(unitAmountMultiplier); const amountTotal = unitAmountGross.multiply(unitAmountMultiplier); - const taxAmount = unitTaxAmount.multiply(unitAmountMultiplier); + const taxAmount = Number(unitAmount) < 0 ? d(0) : unitTaxAmount.multiply(unitAmountMultiplier); return { unitAmount: unitAmount.getAmount(), @@ -160,7 +160,7 @@ export const computeTieredVolumePriceItemValues = ( unitAmountGross: d(tierValues.unitAmountGross!).getAmount(), amountSubtotal: d(tierValues.amountSubtotal).getAmount(), amountTotal: d(tierValues.amountTotal).getAmount(), - taxAmount: d(tierValues.taxAmount).getAmount(), + taxAmount: Number(tierValues.unitAmount) < 0 ? 0 : d(tierValues.taxAmount).getAmount(), displayMode, }; }; @@ -198,7 +198,7 @@ export const computeTieredFlatFeePriceItemValues = ( unitAmountGross: d(tierValues.unitAmountGross!).getAmount(), amountSubtotal: d(tierValues.amountSubtotal).getAmount(), amountTotal: d(tierValues.amountTotal).getAmount(), - taxAmount: d(tierValues.taxAmount).getAmount(), + taxAmount: Number(tierValues.unitAmount) < 0 ? 0 : d(tierValues.taxAmount).getAmount(), displayMode, }; }; @@ -236,7 +236,7 @@ export const computeTieredGraduatedPriceItemValues = ( unitAmountGross: d(totals.unitAmountGross!).add(d(tierValues.unitAmountGross!)).getAmount(), amountSubtotal: d(totals.amountSubtotal).add(d(tierValues.amountSubtotal)).getAmount(), amountTotal: d(totals.amountTotal).add(d(tierValues.amountTotal)).getAmount(), - taxAmount: d(totals.taxAmount).add(d(tierValues.taxAmount)).getAmount(), + taxAmount: Number(tierValues.unitAmount) < 0 ? 0 : d(totals.taxAmount).add(d(tierValues.taxAmount)).getAmount(), displayMode, }; },