diff --git a/processor/src/services/converters/helper.converter.ts b/processor/src/services/converters/helper.converter.ts index eca0e31..ed6cd95 100644 --- a/processor/src/services/converters/helper.converter.ts +++ b/processor/src/services/converters/helper.converter.ts @@ -24,7 +24,11 @@ export const mapCoCoLineItemToAdyenLineItem = (lineItem: CoCoLineItem): LineItem amountExcludingTax: getItemAmount(getAmountExcludingTax(lineItem), lineItem.quantity), amountIncludingTax: getItemAmount(getAmountIncludingTax(lineItem), lineItem.quantity), taxAmount: getItemAmount(getTaxAmount(lineItem), lineItem.quantity), - taxPercentage: convertTaxPercentageToCentAmount(lineItem.taxRate?.amount), + taxPercentage: convertTaxPercentageToAdyenMinorUnits( + lineItem.totalPrice.fractionDigits, + lineItem.totalPrice.currencyCode, + lineItem.taxRate?.amount, + ), }; }; @@ -36,7 +40,11 @@ export const mapCoCoCustomLineItemToAdyenLineItem = (customLineItem: CustomLineI amountExcludingTax: getItemAmount(getAmountExcludingTax(customLineItem), customLineItem.quantity), amountIncludingTax: getItemAmount(getAmountIncludingTax(customLineItem), customLineItem.quantity), taxAmount: getItemAmount(getTaxAmount(customLineItem), customLineItem.quantity), - taxPercentage: convertTaxPercentageToCentAmount(customLineItem.taxRate?.amount), + taxPercentage: convertTaxPercentageToAdyenMinorUnits( + customLineItem.totalPrice.fractionDigits, + customLineItem.totalPrice.currencyCode, + customLineItem.taxRate?.amount, + ), }; }; @@ -74,7 +82,11 @@ export const mapCoCoShippingInfoToAdyenLineItem = (shippingInfo: ShippingInfo): amountExcludingTax: amountExcludingTaxValue, amountIncludingTax: amountIncludingTaxValue, taxAmount: taxAmountValue, - taxPercentage: convertTaxPercentageToCentAmount(shippingInfo.taxRate?.amount), + taxPercentage: convertTaxPercentageToAdyenMinorUnits( + shippingInfo.price.fractionDigits, + shippingInfo.price.currencyCode, + shippingInfo.taxRate?.amount, + ), }; }; @@ -137,7 +149,6 @@ export const mapCoCoOrderItemsToAdyenLineItems = ( export const mapCoCoCartItemsToAdyenLineItems = ( cart: Pick, ): LineItem[] => { - // TODO: SCC-2800: fix amount parsing values const aydenLineItems: LineItem[] = []; cart.lineItems.forEach((lineItem) => aydenLineItems.push(mapCoCoLineItemToAdyenLineItem(lineItem))); @@ -260,12 +271,41 @@ const getTaxAmount = (lineItem: CoCoLineItem | CustomLineItem): number => { ); }; -const convertTaxPercentageToCentAmount = (decimalTaxRate?: number): number => { +/** + * Convert the CoCo tax percentage, which ranges from 0-1 as floating point numbers to the expected Adyen minor units. + * + * This function applies the given fractionDigits to get the correct minor units. This also takes into account the deviations Adyen has with regards to the fractionDigits. + * + * @example CoCo taxRate of 0.21, normalized is 21% with currencyCode EUR and fractionDigits of 2 = expressed in Adyen minor units value as 2100 + * @example CoCo taxRate of 0.21, normalized is 21% with currencyCode CLP, according to ISO standard has fractionDigits of 0 but Adyen deviats from it and so it has fractionDigits of 2 = expressed in Adyen minor units value as 2100 + * @example CoCo taxRate of 0.21, normalized is 21% with currencyCode JPY and fractionDigits of 0 = expressed in Adyen minor units value as 21 + * + * @param fractionDigits the fractionDigits that are applicable for the currencyCode according to ISO_4217 + * @param currencyCode the applicable currencyCode + * @param decimalTaxRate the tax rate expressed in decimals between 0-1 as floating point numbers taken from the CoCo taxRate (see docs below) + * + * @see https://docs.commercetools.com/api/projects/taxCategories#ctp:api:type:TaxRate + * @see https://docs.adyen.com/api-explorer/Checkout/latest/post/sessions#request-lineItems-taxPercentage + * @see https://docs.adyen.com/development-resources/currency-codes/#minor-units + */ +const convertTaxPercentageToAdyenMinorUnits = ( + fractionDigits: number, + currencyCode: string, + decimalTaxRate?: number, +): number => { if (!decimalTaxRate) { return 0; } + // First go from the range of 0 - 1 (floating numbers) to value expressed as "non-decimals". I.e. 0.15% from CoCo becomes 15% tax rate value. + const normalizedTaxRate = decimalTaxRate * 100; - // TODO: SCC-2800: figure out what to do in this function + // Apply the fractionDigits for the cart (i.e. currencyCode) that is in CoCo according to the ISO_4217 standard but overrule the given fractionDigits from the CoCo if Adyen requires it based on the given mapping. + const result = MoneyConverters.convertWithOverrulingMapping( + CURRENCIES_FROM_ISO_TO_ADYEN_MAPPING, + normalizedTaxRate, + currencyCode, + -fractionDigits, + ); - return decimalTaxRate * 100 * 100; + return result; }; diff --git a/processor/test/data/coco-cart-clp.json b/processor/test/data/coco-cart-clp.json new file mode 100644 index 0000000..45adf17 --- /dev/null +++ b/processor/test/data/coco-cart-clp.json @@ -0,0 +1,256 @@ +{ + "type": "Cart", + "id": "00cdf819-8c36-46bd-a938-22c2712c6461", + "version": 7, + "versionModifiedAt": "2025-01-06T11:43:59.506Z", + "lastMessageSequenceNumber": 1, + "createdAt": "2025-01-06T11:43:48.883Z", + "lastModifiedAt": "2025-01-06T11:43:59.506Z", + "lastModifiedBy": { + "clientId": "1ZgJGJmwvAdOf8w8KcOOLmMX", + "isPlatformClient": false + }, + "createdBy": { + "clientId": "X_zFOeNrElgLiF4yuswXQOuR", + "isPlatformClient": false + }, + "customerEmail": "asd@asd.com", + "lineItems": [ + { + "id": "c4700e7e-5b9c-42c6-a02e-322ea8c0fc8e", + "productId": "ee2fa33e-0a9e-4728-9a32-2e024a88b71d", + "productKey": "teak-serving-platter", + "name": { + "en-US": "Teak Serving Platter", + "en-GB": "Teak Serving Platter", + "de-DE": "Servierplatte aus Teakholz" + }, + "productType": { + "typeId": "product-type", + "id": "c6446851-767c-4378-8e8a-ec0ca91eebc9", + "version": 1 + }, + "productSlug": { + "en-US": "teak-serving-platter", + "en-GB": "teak-serving-platter", + "de-DE": "servierplatte-aus-teakholz" + }, + "variant": { + "id": 1, + "sku": "TST-02", + "prices": [ + { + "id": "108fde77-8df6-4ac6-ba02-f482ec5794a0", + "value": { + "type": "centPrecision", + "currencyCode": "EUR", + "centAmount": 1299, + "fractionDigits": 2 + } + }, + { + "id": "1a09ad24-6bf4-47a2-939a-72b65921cbf8", + "value": { + "type": "centPrecision", + "currencyCode": "GBP", + "centAmount": 1299, + "fractionDigits": 2 + }, + "country": "GB" + }, + { + "id": "d2862da8-cd2f-4ba9-8427-d323af3d74de", + "value": { + "type": "centPrecision", + "currencyCode": "USD", + "centAmount": 1299, + "fractionDigits": 2 + }, + "country": "US" + }, + { + "id": "d677024b-6900-4f55-af5b-8779ee6ddc50", + "value": { + "type": "centPrecision", + "currencyCode": "CLP", + "centAmount": 150, + "fractionDigits": 0 + }, + "key": "clp" + } + ], + "images": [ + { + "url": "https://storage.googleapis.com/merchant-center-europe/sample-data/goodstore/Teak_Serving_Platter-1.1.jpeg", + "dimensions": { + "w": 4331, + "h": 2389 + } + } + ], + "attributes": [ + { + "name": "productspec", + "value": { + "en-GB": "- Made of natural teak\n- Hand wash only", + "en-US": "- Made of natural teak\n- Hand wash only", + "de-DE": "- Hergestellt aus natürlichem Teakholz\n- Handwäsche nur" + } + } + ], + "assets": [], + "availability": { + "isOnStock": false, + "availableQuantity": 0, + "version": 1, + "id": "10f0f9ce-06c9-4d2a-812c-e5b0c266bc00" + } + }, + "price": { + "id": "d677024b-6900-4f55-af5b-8779ee6ddc50", + "value": { + "type": "centPrecision", + "currencyCode": "CLP", + "centAmount": 150, + "fractionDigits": 0 + }, + "key": "clp" + }, + "quantity": 1, + "discountedPricePerQuantity": [], + "taxRate": { + "name": "Chili", + "amount": 0.12, + "includedInPrice": true, + "country": "CL", + "id": "gxuuA8ZL", + "key": "chili-clp", + "subRates": [] + }, + "perMethodTaxRate": [], + "addedAt": "2025-01-06T11:43:48.878Z", + "lastModifiedAt": "2025-01-06T11:43:48.878Z", + "state": [ + { + "quantity": 1, + "state": { + "typeId": "state", + "id": "26f1f5f6-ae77-4314-a7bb-02ebf05b6203" + } + } + ], + "priceMode": "Platform", + "lineItemMode": "Standard", + "totalPrice": { + "type": "centPrecision", + "currencyCode": "CLP", + "centAmount": 150, + "fractionDigits": 0 + }, + "taxedPrice": { + "totalNet": { + "type": "centPrecision", + "currencyCode": "CLP", + "centAmount": 134, + "fractionDigits": 0 + }, + "totalGross": { + "type": "centPrecision", + "currencyCode": "CLP", + "centAmount": 150, + "fractionDigits": 0 + }, + "taxPortions": [ + { + "rate": 0.12, + "amount": { + "type": "centPrecision", + "currencyCode": "CLP", + "centAmount": 16, + "fractionDigits": 0 + }, + "name": "Chili" + } + ], + "totalTax": { + "type": "centPrecision", + "currencyCode": "CLP", + "centAmount": 16, + "fractionDigits": 0 + } + }, + "taxedPricePortions": [] + } + ], + "cartState": "Active", + "totalPrice": { + "type": "centPrecision", + "currencyCode": "CLP", + "centAmount": 150, + "fractionDigits": 0 + }, + "taxedPrice": { + "totalNet": { + "type": "centPrecision", + "currencyCode": "CLP", + "centAmount": 134, + "fractionDigits": 0 + }, + "totalGross": { + "type": "centPrecision", + "currencyCode": "CLP", + "centAmount": 150, + "fractionDigits": 0 + }, + "taxPortions": [ + { + "rate": 0.12, + "amount": { + "type": "centPrecision", + "currencyCode": "CLP", + "centAmount": 16, + "fractionDigits": 0 + }, + "name": "Chili" + } + ], + "totalTax": { + "type": "centPrecision", + "currencyCode": "CLP", + "centAmount": 16, + "fractionDigits": 0 + } + }, + "shippingMode": "Single", + "shippingAddress": { + "firstName": "asd", + "lastName": "asd", + "streetName": "asd", + "postalCode": "1111aa", + "city": "asd", + "country": "CL", + "email": "asd@asd.com" + }, + "shipping": [], + "customLineItems": [], + "discountCodes": [], + "directDiscounts": [], + "inventoryMode": "None", + "taxMode": "Platform", + "taxRoundingMode": "HalfEven", + "taxCalculationMode": "LineItemLevel", + "deleteDaysAfterLastModification": 90, + "refusedGifts": [], + "origin": "Customer", + "billingAddress": { + "firstName": "asd", + "lastName": "asd", + "streetName": "asd", + "postalCode": "1111aa", + "city": "asd", + "country": "CL", + "email": "asd@asd.com" + }, + "itemShippingAddresses": [], + "totalLineItemQuantity": 1 +} diff --git a/processor/test/services/converters/helper.converter.spec.ts b/processor/test/services/converters/helper.converter.spec.ts index c918901..e95072d 100644 --- a/processor/test/services/converters/helper.converter.spec.ts +++ b/processor/test/services/converters/helper.converter.spec.ts @@ -19,6 +19,7 @@ import { ShippingInfo, } from '@commercetools/connect-payments-sdk'; import CoCoCartJSON from '../../data/coco-cart.json'; +import CoCoCartCLPJSON from '../../data/coco-cart-clp.json'; describe('helper.converter', () => { beforeEach(() => { @@ -131,7 +132,7 @@ describe('helper.converter', () => { expect(actual).toEqual(expected); }); - test('should map CoCo shipping info to Adyen line item', () => { + test('should map CoCo discount info to Adyen line item', () => { const input = CoCoCartJSON.discountOnTotalPrice as any; const actual = mapCoCoDiscountOnTotalPriceToAdyenLineItem({ discountOnTotalPrice: input }); @@ -188,4 +189,22 @@ describe('helper.converter', () => { expect(actual).toEqual(expected); }); + + test('should map the CoCo line items to Adyen line items taking into account Adyen deviations', () => { + // CLP currencyCode according to ISO_4217 has 0 fractionDigits but Adyen expects 2 fractionDigits + const input = CoCoCartCLPJSON.lineItems as CoCoLineItem[]; + + const actual = mapCoCoLineItemToAdyenLineItem(input[0]); + const expected: LineItem = { + id: 'TST-02', + description: 'Teak Serving Platter', + quantity: 1, + amountExcludingTax: 13400, + amountIncludingTax: 15000, + taxAmount: 1600, + taxPercentage: 1200, + }; + + expect(actual).toEqual(expected); + }); }); diff --git a/processor/test/utils/mock-payment-data.ts b/processor/test/utils/mock-payment-data.ts index 8924ea9..86f7c9c 100644 --- a/processor/test/utils/mock-payment-data.ts +++ b/processor/test/utils/mock-payment-data.ts @@ -143,6 +143,7 @@ export const mockAdyenRefundPaymentResponse: PaymentRefundResponse = { export const mockGetPaymentAmount: PaymentAmount = { centAmount: 150000, currencyCode: 'USD', + fractionDigits: 2, }; export const mockAdyenCreatePaymentResponse: PaymentResponse = {