From 5b1cfd4613b5de334f5f378009b3e91670c9453d Mon Sep 17 00:00:00 2001 From: Zenit Shkreli <69572953+zenit2001@users.noreply.github.com> Date: Tue, 23 Jul 2024 09:49:24 +0200 Subject: [PATCH] PayPal Express in Cart and Mini-Cart page (#1122) * Rendering PayPal Expres button (#1076) * Paypal express payment (#1077) * Merchant reference set for PayPal Express (#1080) * Populate payment instrument fields for PayPal Express (#1081) * Adding the possibility to enable/disable paypal express from BM (#1085) * feat: adding the possibility to enable/disable paypal ecs from BM * chore: linting * fix: fixing klarna e2e test * PayPal Express update order (#1086) * feat: paypalUpdateOrder endpoint * test: unit tests and jsdoc annotations * feat: paypal update order * feat(SFI-696): show paypal express if enabled in BM * chore(SFI-696): add sonar properties file * refactor(SFI-696): function to create redirectUrl * feat(SFI-696): add stacktrace for error and fatal logs * chore(SFI-696): exclude e2e tests from sonar * fix(SFI-696): clear session.privacy data for paypal express * chore(SFI-696): undo changes to cartridges * fix(SFI-696): zero-auth flow * add spinner for paypal express flow (#1091) * Added the review page template and controller to render it (#1096) * Place order button for express payments on review page (#1112) * feat(SFI-790): new template for checkout review button * feat(SFI-789): handle payments details call from checkout review page * feat(SFI-789): create basket view data on express review page * feat(SFI-789): create basket view data on express review page * chore: merging develop into SFI-42 and adding required mocks (#1115) * Added e2e tests for paypal express (#1117) * test(SFI-798): paypal express unit tests (#1121) * fix: fixing apple pay express flow --------- Co-authored-by: Shani <31096696+shanikantsingh@users.noreply.github.com> --- .github/workflows/SFCC.yml | 2 +- .../expressPayments/shippingMethods.js | 2 +- jest/__mocks__/dw/order/BasketMgr.js | 38 +- jest/__mocks__/dw/order/OrderMgr.js | 29 +- jest/__mocks__/dw/order/ShippingLocation.js | 5 + jest/__mocks__/dw/order/ShippingMgr.js | 44 +- jest/__mocks__/dw/order/TaxMgr.js | 2 + jest/__mocks__/dw/util/UUIDUtils.js | 2 +- jest/__mocks__/dw/value/Money.js | 35 +- jest/sfccCartridgeMocks.js | 352 +++++---- jest/sfccPathSetup.js | 675 ++++++++++++------ .../meta/system-objecttype-extensions.xml | 25 + metadata/site_import/services.xml | 19 + sonar-project.properties | 5 + .../__snapshots__/paypalExpress.test.js.snap | 32 + .../js/__tests__/paypalExpress.test.js | 522 ++++++++++++++ .../default/js/amazonPayExpressPart1.js | 3 +- .../client/default/js/applePayExpress.js | 104 +-- .../default/js/checkoutReviewButtons.js | 41 ++ .../client/default/js/commons/index.js | 6 +- .../cartridge/client/default/js/constants.js | 2 + .../client/default/js/paypalExpress.js | 225 ++++++ .../default/adyen/checkoutReviewButtons.isml | 23 + .../default/cart/checkoutButtons.isml | 18 +- .../default/cart/checkoutReview.isml | 91 +++ .../templates/resources/adyen.properties | 2 + .../default/css/configurationSettings.css | 28 +- .../cartridge/static/default/icons/paypal.svg | 7 + .../static/default/js/adyenSettings.js | 32 + .../settingCards/epmSettings.isml | 3 + .../cartridge/adyen/config/constants.js | 2 + .../cartridge/adyen/logs/adyenCustomLogs.js | 14 +- .../__tests__/selectShippingMethods.test.js | 17 +- .../__tests__/shippingMethods.test.js | 38 +- .../__tests__/handleCheckoutReview.test.js | 91 +++ .../makeExpressPaymentDetailsCall.test.js | 57 ++ .../__tests__/makeExpressPaymentsCall.test.js | 46 ++ .../paypal/__tests__/saveShopperData.test.js | 46 ++ .../paypal/handleCheckoutReview.js | 90 +++ .../paypal/makeExpressPaymentDetailsCall.js | 69 ++ .../paypal/makeExpressPaymentsCall.js | 66 ++ .../expressPayments/paypal/saveShopperData.js | 17 + .../saveExpressShopperDetails.js | 6 +- .../expressPayments/selectShippingMethods.js | 68 +- .../expressPayments/shippingMethods.js | 113 +-- .../cartridge/adyen/scripts/index.js | 8 + .../payments/__tests__/adyenCheckout.test.js | 14 +- .../getCheckoutPaymentMethods.test.js | 22 +- .../__tests__/paymentsDetails.test.js | 7 +- .../payments/__tests__/paypalHelper.test.js | 50 -- .../adyen/scripts/payments/adyenCheckout.js | 20 +- .../adyen/scripts/payments/adyenZeroAuth.js | 3 +- .../payments/getCheckoutPaymentMethods.js | 25 +- .../adyen/scripts/payments/paymentsDetails.js | 29 +- .../adyen/scripts/payments/paypalHelper.js | 53 -- .../handlePaymentFromComponent.js | 39 +- .../adyen/utils/__tests__/adyenHelper.test.js | 49 ++ .../utils/__tests__/paypalHelper.test.js | 186 +++++ .../cartridge/adyen/utils/adyenConfigs.js | 8 + .../cartridge/adyen/utils/adyenHelper.js | 178 +++-- .../cartridge/adyen/utils/paypalHelper.js | 177 +++++ .../cartridge/controllers/Adyen.js | 40 +- tests/playwright/fixtures/USD.spec.mjs | 46 +- tests/playwright/pages/PaymentMethodsPage.mjs | 28 +- .../paymentFlows/redirectShopper.mjs | 4 +- 65 files changed, 3311 insertions(+), 789 deletions(-) create mode 100644 jest/__mocks__/dw/order/ShippingLocation.js create mode 100644 jest/__mocks__/dw/order/TaxMgr.js create mode 100644 sonar-project.properties create mode 100644 src/cartridges/app_adyen_SFRA/cartridge/client/default/js/__tests__/__snapshots__/paypalExpress.test.js.snap create mode 100644 src/cartridges/app_adyen_SFRA/cartridge/client/default/js/__tests__/paypalExpress.test.js create mode 100644 src/cartridges/app_adyen_SFRA/cartridge/client/default/js/checkoutReviewButtons.js create mode 100644 src/cartridges/app_adyen_SFRA/cartridge/client/default/js/paypalExpress.js create mode 100644 src/cartridges/app_adyen_SFRA/cartridge/templates/default/adyen/checkoutReviewButtons.isml create mode 100644 src/cartridges/app_adyen_SFRA/cartridge/templates/default/cart/checkoutReview.isml create mode 100644 src/cartridges/bm_adyen/cartridge/static/default/icons/paypal.svg create mode 100644 src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/paypal/__tests__/handleCheckoutReview.test.js create mode 100644 src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/paypal/__tests__/makeExpressPaymentDetailsCall.test.js create mode 100644 src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/paypal/__tests__/makeExpressPaymentsCall.test.js create mode 100644 src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/paypal/__tests__/saveShopperData.test.js create mode 100644 src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/paypal/handleCheckoutReview.js create mode 100644 src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/paypal/makeExpressPaymentDetailsCall.js create mode 100644 src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/paypal/makeExpressPaymentsCall.js create mode 100644 src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/paypal/saveShopperData.js delete mode 100644 src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/payments/__tests__/paypalHelper.test.js delete mode 100644 src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/payments/paypalHelper.js create mode 100644 src/cartridges/int_adyen_SFRA/cartridge/adyen/utils/__tests__/paypalHelper.test.js create mode 100644 src/cartridges/int_adyen_SFRA/cartridge/adyen/utils/paypalHelper.js diff --git a/.github/workflows/SFCC.yml b/.github/workflows/SFCC.yml index b0cf4046b..055bcb6d4 100644 --- a/.github/workflows/SFCC.yml +++ b/.github/workflows/SFCC.yml @@ -22,6 +22,6 @@ jobs: npm install npm run lint:fix npm run lint - npm test + npm run test:coverage env: CI: true diff --git a/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/shippingMethods.js b/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/shippingMethods.js index f7d40efab..db1c55ef1 100644 --- a/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/shippingMethods.js +++ b/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/shippingMethods.js @@ -61,4 +61,4 @@ function callGetShippingMethods(req, res, next) { return next(); } } -module.exports = callGetShippingMethods; \ No newline at end of file +module.exports = callGetShippingMethods; diff --git a/jest/__mocks__/dw/order/BasketMgr.js b/jest/__mocks__/dw/order/BasketMgr.js index 1f4eaf346..c7164fd42 100644 --- a/jest/__mocks__/dw/order/BasketMgr.js +++ b/jest/__mocks__/dw/order/BasketMgr.js @@ -36,6 +36,11 @@ export const getTotalGrossPrice = jest.fn(() => ({ isAvailable, })); +export const getAdjustedMerchandizeTotalGrossPrice = jest.fn(() => ({ + currencyCode: 'EUR', + isAvailable, +})); + export const getCreditCardToken = jest.fn(() => 'mockedCreditCardToken'); export const getPaymentMethod = jest.fn(() => 'mockedPaymentMethod'); @@ -47,8 +52,14 @@ export const setCreditCardToken = jest.fn(); export const toArray = jest.fn(() => [ { - custom: {}, - paymentTransaction: { paymentProcessor: 'mocked_payment_processor' }, + custom: { adyenPaymentMethod: '' }, + paymentTransaction: { + paymentProcessor: 'mocked_payment_processor', + amount: { + value: 'mockedValue', + currencyCode: 'mockedValue', + }, + }, setCreditCardNumber, setCreditCardType, setCreditCardExpirationMonth, @@ -73,7 +84,6 @@ export const getDefaultShipment = jest.fn(() => ({ })); export const getBillingAddress = jest.fn(() => 'mocked_billing_address'); - function formatCustomerObject(shopperDetails) { return { addressBook: { @@ -121,7 +131,9 @@ export const getCurrentBasket = jest.fn(() => ({ setCustomerEmail: jest.fn(), getAllProductLineItems, getTotalGrossPrice, + getAdjustedMerchandizeTotalGrossPrice, getPaymentInstruments, + removeAllPaymentInstruments: jest.fn(), removePaymentInstrument: jest.fn(), custom: { amazonExpressShopperDetails: JSON.stringify({ @@ -138,15 +150,15 @@ export const getCurrentBasket = jest.fn(() => ({ }, addressBook: { preferredAddress: { - address1: 'address1', - address2: 'mocked address2', - phone: 'mocked phone', - postalCode: 'mocked postalCode', - countryCode: 'mocked CC', - city: 'mocked city', - lastName: 'mocked name', - firstName: 'mocked name', - stateCode: 'mocked state', + address1: 'address1', + address2: 'mocked address2', + phone: 'mocked phone', + postalCode: 'mocked postalCode', + countryCode: 'mocked CC', + city: 'mocked city', + lastName: 'mocked name', + firstName: 'mocked name', + stateCode: 'mocked state', }, }, profile: { @@ -158,7 +170,7 @@ export const getCurrentBasket = jest.fn(() => ({ }), }, getUUID: jest.fn(), - createBillingAddress, + createBillingAddress: jest.fn(() => createBillingAddress), createPaymentInstrument: jest.fn(() => toArray()[0]), defaultShipment: getDefaultShipment(), getDefaultShipment, diff --git a/jest/__mocks__/dw/order/OrderMgr.js b/jest/__mocks__/dw/order/OrderMgr.js index fd98c11c1..7db7cbafe 100644 --- a/jest/__mocks__/dw/order/OrderMgr.js +++ b/jest/__mocks__/dw/order/OrderMgr.js @@ -1,16 +1,16 @@ const paymentInstrument = () => [ { custom: { - adyenPaymentData: "{ \"paymentMethod\": { \"type\": \"mocked_type\" } }", + adyenPaymentData: '{ "paymentMethod": { "type": "mocked_type" } }', adyenRedirectURL: 'https://some_mocked_url/signature', adyenMD: 'mocked_adyen_MD', adyenAction: 'mocked_adyen_action', }, paymentTransaction: { - custom: { - Adyen_merchantSig: 'mocked_signature', - Adyen_authResult: "{ \"data\": \"mock\"}", - }, + custom: { + Adyen_merchantSig: 'mocked_signature', + Adyen_authResult: '{ "data": "mock"}', + }, }, }, ]; @@ -36,9 +36,7 @@ export const getPaymentInstruments = jest.fn(() => ({ 0: toArray()[0], })); - - -export const getOrder = jest.fn((statusValue="4"/* orderNo */) => ({ +export const getOrder = jest.fn((statusValue = '4' /* orderNo */) => ({ getPaymentInstruments, setPaymentStatus: jest.fn(), setExportStatus: jest.fn(), @@ -46,10 +44,23 @@ export const getOrder = jest.fn((statusValue="4"/* orderNo */) => ({ orderToken: 'mocked_orderToken', getUUID: jest.fn(), custom: { Adyen_pspReference: 'mocked_pspRef' }, - status: { value: statusValue} + status: { value: statusValue }, })); export const failOrder = jest.fn((orderNo, bool) => ({ orderNo, bool, })); + +export const createOrderNo = jest.fn(() => 'mocked_orderNo'); + +export const createOrder = jest.fn(() => ({ + getPaymentInstruments, + setPaymentStatus: jest.fn(), + setExportStatus: jest.fn(), + orderNo: 'mocked_orderNo', + orderToken: 'mocked_orderToken', + getUUID: jest.fn(), + custom: { Adyen_pspReference: 'mocked_pspRef' }, + status: { value: 'Created' }, +})); diff --git a/jest/__mocks__/dw/order/ShippingLocation.js b/jest/__mocks__/dw/order/ShippingLocation.js new file mode 100644 index 000000000..50198c874 --- /dev/null +++ b/jest/__mocks__/dw/order/ShippingLocation.js @@ -0,0 +1,5 @@ +function ShippingLocation() { + return jest.fn(); +} + +module.exports = ShippingLocation; diff --git a/jest/__mocks__/dw/order/ShippingMgr.js b/jest/__mocks__/dw/order/ShippingMgr.js index c0ceac6a0..a33339e1e 100644 --- a/jest/__mocks__/dw/order/ShippingMgr.js +++ b/jest/__mocks__/dw/order/ShippingMgr.js @@ -1,6 +1,40 @@ +const Money = require('../value/Money'); - export const getShipmentShippingModel = jest.fn((shipment) => ({ - getApplicableShippingMethods: jest.fn(() => ({ - })), - })); - \ No newline at end of file +const shippingMethods = [ + { + description: 'Order received within 7-10 business days', + displayName: 'Ground', + ID: '001', + custom: { + estimatedArrivalTime: '7-10 Business Days', + }, + getTaxClassID: jest.fn(), + }, + { + description: 'Order received in 2 business days', + displayName: '2-Day Express', + ID: '002', + shippingCost: '$0.00', + custom: { + estimatedArrivalTime: '2 Business Days', + }, + getTaxClassID: jest.fn(), + }, +]; +const shippingCost = Money(); +const shipmentShippingModel = { + getApplicableShippingMethods: jest.fn(() => ({ + toArray: jest.fn(() => shippingMethods), + })), + getShippingCost: jest.fn(() => ({ + getAmount: jest.fn(() => shippingCost), + })), +}; +const productShippingModel = { + getApplicableShippingMethods: jest.fn(() => ({})), + getShippingCost: jest.fn(() => ({ + getAmount: jest.fn(() => shippingCost), + })), +}; +export const getShipmentShippingModel = jest.fn(() => shipmentShippingModel); +export const getProductShippingModel = jest.fn(() => productShippingModel); diff --git a/jest/__mocks__/dw/order/TaxMgr.js b/jest/__mocks__/dw/order/TaxMgr.js new file mode 100644 index 000000000..9722d630d --- /dev/null +++ b/jest/__mocks__/dw/order/TaxMgr.js @@ -0,0 +1,2 @@ +export const getTaxJurisdictionID = jest.fn(); +export const getTaxRate = jest.fn(); \ No newline at end of file diff --git a/jest/__mocks__/dw/util/UUIDUtils.js b/jest/__mocks__/dw/util/UUIDUtils.js index 2d1ac18e7..17acaa93b 100644 --- a/jest/__mocks__/dw/util/UUIDUtils.js +++ b/jest/__mocks__/dw/util/UUIDUtils.js @@ -1 +1 @@ -export const createUUID = jest.fn(); +export const createUUID = jest.fn(() => 'mock_UUID'); diff --git a/jest/__mocks__/dw/value/Money.js b/jest/__mocks__/dw/value/Money.js index a6018404e..91a9cceae 100644 --- a/jest/__mocks__/dw/value/Money.js +++ b/jest/__mocks__/dw/value/Money.js @@ -1,5 +1,30 @@ -export class Money { - constructor() { - return {value: 10, currency: 'TEST'}; - } - } +function Money(isAvailable) { + return { + available: isAvailable, + value: '10.99', + currency: 'USD', + getDecimalValue() { + return '10.99'; + }, + getValue() { + return '10.99'; + }, + getCurrencyCode() { + return 'USD'; + }, + subtract() { + return new Money(isAvailable); + }, + multiply() { + return new Money(isAvailable); + }, + add() { + return new Money(isAvailable); + }, + addRate() { + return new Money(isAvailable); + }, + }; +} + +module.exports = Money; diff --git a/jest/sfccCartridgeMocks.js b/jest/sfccCartridgeMocks.js index 4c5be1f95..938195ade 100644 --- a/jest/sfccCartridgeMocks.js +++ b/jest/sfccCartridgeMocks.js @@ -1,9 +1,16 @@ +/* eslint-disable global-require */ // mocks for all cartridge paths containing asterisk (*) symbol // cartridge/models mocks jest.mock('*/cartridge/models/order', () => jest.fn(), { virtual: true }); jest.mock('*/cartridge/models/cart', () => jest.fn(), { virtual: true }); +jest.mock('*/cartridge/models/account', () => jest.fn(), { virtual: true }); + +jest.mock('*/cartridge/models/shipping/shippingMethod', () => jest.fn(), { + virtual: true, +}); + jest.mock( '*/cartridge/scripts/checkout/shippingHelpers', () => ({ @@ -13,112 +20,125 @@ jest.mock( { virtual: true }, ); -jest.mock('*/cartridge/models/cart', () => jest.fn(), { virtual: true }); - -jest.mock('*/cartridge/adyen/scripts/expressPayments/selectShippingMethods', () => { - return jest.fn(); -}, {virtual: true}); +jest.mock( + '*/cartridge/adyen/scripts/expressPayments/selectShippingMethods', + () => jest.fn(), + { virtual: true }, +); // cartridge/scripts mocks -jest.mock('*/cartridge/adyen/scripts/payments/adyenCheckout', () => { - return { +jest.mock( + '*/cartridge/adyen/scripts/payments/adyenCheckout', + () => ({ + doPaymentsCall: jest.fn(() => ({ + pspReference: 'mocked_pspReference', + })), doPaymentsDetailsCall: jest.fn((payload) => { let resultCode; if (payload.paymentData) { resultCode = payload.paymentData; } else if (payload.details?.MD) { - resultCode = payload.details.MD === 'mocked_md' ? 'Authorised' : 'Not_Authorised'; + resultCode = + payload.details.MD === 'mocked_md' ? 'Authorised' : 'Not_Authorised'; } - return {resultCode}; + return { resultCode, paymentMethod: { type: 'mocked_type' } }; }), createPaymentRequest: jest.fn(() => ({ resultCode: 'Authorised', })), - doCreatePartialPaymentOrderCall: jest.fn(() => { - return {remainingAmount: 'mocked_amount', orderData: 'mocked_data'}; - - }), - }; -}, {virtual: true}); + doCreatePartialPaymentOrderCall: jest.fn(() => ({ + remainingAmount: 'mocked_amount', + orderData: 'mocked_data', + })), + }), + { virtual: true }, +); -jest.mock('*/cartridge/adyen/scripts/payments/adyenDeleteRecurringPayment', () => { - return { deleteRecurringPayment: jest.fn(() => true) }; -}, {virtual: true}); +jest.mock( + '*/cartridge/adyen/scripts/payments/adyenDeleteRecurringPayment', + () => ({ deleteRecurringPayment: jest.fn(() => true) }), + { virtual: true }, +); -jest.mock('*/cartridge/adyen/scripts/expressPayments/saveExpressShopperDetails', () => { - return jest.fn(); -}, {virtual: true}); +jest.mock( + '*/cartridge/adyen/scripts/expressPayments/saveExpressShopperDetails', + () => jest.fn(), + { virtual: true }, +); -jest.mock('*/cartridge/adyen/scripts/payments/adyenGetPaymentMethods', () => { - return { +jest.mock( + '*/cartridge/adyen/scripts/payments/adyenGetPaymentMethods', + () => ({ getMethods: jest.fn(() => ({ - paymentMethods: [{type: 'visa'}], - })) - }; -}, {virtual: true}); + paymentMethods: [{ type: 'visa' }], + })), + }), + { virtual: true }, +); -jest.mock('*/cartridge/adyen/scripts/payments/adyenGetPaymentMethods', () => { - return { - getMethods: jest.fn(() => ({ - paymentMethods: [{type: 'visa'}], - })) - }; -}, {virtual: true}); -jest.mock('*/cartridge/adyen/scripts/payments/adyenTerminalApi', () => { - return { +jest.mock( + '*/cartridge/adyen/scripts/payments/adyenTerminalApi', + () => ({ getTerminals: jest.fn(() => ({ response: JSON.stringify({ foo: 'bar' }), })), createTerminalPayment: jest.fn(() => ({ - response: 'mockedSuccessResponse' - })) - }; -}, {virtual: true}); + response: 'mockedSuccessResponse', + })), + }), + { virtual: true }, +); -jest.mock('*/cartridge/adyen/scripts/payments/adyenZeroAuth', () => { - return { +jest.mock( + '*/cartridge/adyen/scripts/payments/adyenZeroAuth', + () => ({ zeroAuthPayment: jest.fn(() => ({ error: false, resultCode: 'Authorised', - })) - }; -}, {virtual: true}); + })), + }), + { virtual: true }, +); -jest.mock('*/cartridge/adyen/webhooks/checkNotificationAuth', () => { - return { +jest.mock( + '*/cartridge/adyen/webhooks/checkNotificationAuth', + () => ({ check: jest.fn(() => true), validateHmacSignature: jest.fn(() => true), - }; -}, {virtual: true}); - -jest.mock('*/cartridge/adyen/webhooks/handleNotify', () => { - return { - notify: jest.fn(() => ({ success: true })) - }; -}, {virtual: true}); + }), + { virtual: true }, +); -jest.mock('*/cartridge/adyen/scripts/payments/updateSavedCards', () => { - return { - updateSavedCards: jest.fn() - }; -}, {virtual: true}); +jest.mock( + '*/cartridge/adyen/webhooks/handleNotify', + () => ({ + notify: jest.fn(() => ({ success: true })), + }), + { virtual: true }, +); -// cartridge/scripts/checkout mocks -jest.mock('*/cartridge/adyen/utils/authorizationHelper', () => { - return { - validatePayment: jest.fn(() => ({ error: false })), - handlePayments: jest.fn(() => ({ error: false, action: {type: 'mockedAction'} })), - }; - }, {virtual: true}); +jest.mock( + '*/cartridge/adyen/scripts/payments/updateSavedCards', + () => ({ + updateSavedCards: jest.fn(), + }), + { virtual: true }, +); // cartridge/scripts/checkout mocks -jest.mock('*/cartridge/adyen/utils/adyenHelpers', () => { - return { +jest.mock( + '*/cartridge/adyen/utils/authorizationHelper', + () => ({ validatePayment: jest.fn(() => ({ error: false })), - handlePayments: jest.fn(() => ({ error: false, action: {type: 'mockedAction'} })), - }; -}, {virtual: true}); + handlePayments: jest.fn(() => ({ + error: false, + action: { type: 'mockedAction' }, + })), + }), + { virtual: true }, +); +// cartridge/scripts/checkout mocks jest.mock( '*/cartridge/scripts/checkout/checkoutHelpers', () => { @@ -223,14 +243,22 @@ jest.mock( ); // cartridge/scripts/hooks mocks -jest.mock('*/cartridge/scripts/hooks/fraudDetection', () => { return {} }, {virtual: true}) -jest.mock('*/cartridge/scripts/hooks/validateOrder', () => { return {}} , {virtual: true}) -jest.mock('*/cartridge/scripts/hooks/postAuthorizationHandling', () => { return {}} , {virtual: true}) +jest.mock('*/cartridge/scripts/hooks/fraudDetection', () => ({}), { + virtual: true, +}); +jest.mock('*/cartridge/scripts/hooks/validateOrder', () => ({}), { + virtual: true, +}); +jest.mock('*/cartridge/scripts/hooks/postAuthorizationHandling', () => ({}), { + virtual: true, +}); // cartridge/adyen/util mocks jest.mock('*/cartridge/adyen/utils/validatePaymentMethod', () => ({ - validatePaymentMethod: jest.fn(() => jest.fn(() => true)), - })); -jest.mock('*/cartridge/adyen/utils/adyenHelper', () => ({ + validatePaymentMethod: jest.fn(() => jest.fn(() => true)), +})); +jest.mock( + '*/cartridge/adyen/utils/adyenHelper', + () => ({ savePaymentDetails: jest.fn(), getAdyenHash: jest.fn((str, str2) => `${str} __ ${str2}`), getLoadingContext: jest.fn(() => 'mocked_loading_context'), @@ -240,6 +268,7 @@ jest.mock('*/cartridge/adyen/utils/adyenHelper', () => ({ })), isAdyenGivingAvailable: jest.fn(() => true), isApplePay: jest.fn(() => true), + isPayPalExpress: jest.fn(() => false), getAdyenGivingConfig: jest.fn(() => true), getApplicationInfo: jest.fn(() => ({ externalPlatform: { version: 'SFRA' }, @@ -273,12 +302,19 @@ jest.mock('*/cartridge/adyen/utils/adyenHelper', () => ({ getOrderMainPaymentInstrumentType: jest.fn(() => {}), getPaymentInstrumentType: jest.fn((isCreditCard) => (isCreditCard ? 'CREDIT_CARD' : 'AdyenComponent'),), + validatePayment: jest.fn(() => ({ error: false })), + handlePayments: jest.fn(() => ({ + error: false, + action: { type: 'mockedAction' }, + })), + createRedirectUrl: jest.fn(() => 'mocked_RedirectUrl'), }), { virtual: true }, ); -jest.mock('*/cartridge/adyen/utils/adyenConfigs', () => { - return { +jest.mock( + '*/cartridge/adyen/utils/adyenConfigs', + () => ({ getAdyenEnvironment: jest.fn(() => 'TEST'), getAdyenInstallmentsEnabled: jest.fn(() => true), getCreditCardInstallments: jest.fn(() => true), @@ -290,25 +326,21 @@ jest.mock('*/cartridge/adyen/utils/adyenConfigs', () => { getAdyenMerchantAccount: jest.fn(() => 'mocked_merchant_account'), getAdyenGivingEnabled: jest.fn(() => true), getAdyenGivingCharityName: jest.fn(() => '%mocked_charity_name%'), - getAdyenGivingCharityWebsite: jest.fn( - () => 'mocked_charity_website', - ), + getAdyenGivingCharityWebsite: jest.fn(() => 'mocked_charity_website'), getAdyenGivingCharityDescription: jest.fn( - () => '%mocked_charity_description%', - ), - getAdyenGivingBackgroundUrl: jest.fn( - () => 'mocked_background_url', + () => '%mocked_charity_description%', ), + getAdyenGivingBackgroundUrl: jest.fn(() => 'mocked_background_url'), getAdyenGivingLogoUrl: jest.fn(() => 'mocked_logo_url'), getAdyenSFRA6Compatibility: jest.fn(() => false), - getAdyenHmacKey : jest.fn(() => 'mocked_hmacKey'), + getAdyenHmacKey: jest.fn(() => 'mocked_hmacKey'), getAdyenBasketFieldsEnabled: jest.fn(() => false), getAdyen3DS2Enabled: jest.fn(() => false), getAdyenLevel23DataEnabled: jest.fn(() => false), - getAdyenSalePaymentMethods:jest.fn(() => []), - - }; -}, {virtual: true}); + getAdyenSalePaymentMethods: jest.fn(() => []), + }), + { virtual: true }, +); jest.mock( '*/cartridge/client/default/js/adyen_checkout/renderGiftcardComponent', @@ -342,53 +374,97 @@ jest.mock( { virtual: true }, ); -jest.mock('*/cartridge/controllers/middlewares/checkout_services/adyenCheckoutServices', () => { - return { - processPayment: jest.fn(), - isNotAdyen: jest.fn(() => false), - }; - }, {virtual: true}); +jest.mock( + '*/cartridge/adyen/utils/lineItemHelper', + () => ({ + getDescription: jest.fn((lineItem) => lineItem.productName), + getId: jest.fn((lineItem) => lineItem.productID), + getQuantity: jest.fn((lineItem) => lineItem.quantityValue), + getItemAmount: jest.fn((lineItem) => ({ + divide: jest.fn((quantity) => ({ + getValue: jest.fn(() => lineItem.adjustedNetPrice / quantity), + })), + })), + getVatAmount: jest.fn((lineItem) => ({ + divide: jest.fn((quantity) => ({ + getValue: jest.fn(() => lineItem.getAdjustedTax / quantity), + })), + })), + getAllLineItems: jest.fn((lineItem) => lineItem), + }), + { virtual: true }, +); jest.mock( - '*/cartridge/adyen/utils/lineItemHelper', - () => ({ - getDescription: jest.fn((lineItem) => lineItem.productName), - getId: jest.fn((lineItem) => lineItem.productID), - getQuantity: jest.fn((lineItem) => lineItem.quantityValue), - getItemAmount: jest.fn((lineItem) => ({ - divide: jest.fn((quantity) => ({ - getValue: jest.fn(() => lineItem.adjustedNetPrice / quantity), - })), - })), - getVatAmount: jest.fn((lineItem) => ({ - divide: jest.fn((quantity) => ({ - getValue: jest.fn(() => lineItem.getAdjustedTax / quantity), - })), - })), - getAllLineItems: jest.fn((lineItem) => lineItem), - }), - { virtual: true }, - ); + '*/cartridge/adyen/utils/lineItemHelper', + () => ({ + getDescription: jest.fn((lineItem) => lineItem.productName), + getId: jest.fn((lineItem) => lineItem.productID), + getQuantity: jest.fn((lineItem) => lineItem.quantityValue), + getItemAmount: jest.fn((lineItem) => ({ + divide: jest.fn((quantity) => ({ + getValue: jest.fn(() => lineItem.adjustedNetPrice / quantity), + })), + })), + getVatAmount: jest.fn((lineItem) => ({ + divide: jest.fn((quantity) => ({ + getValue: jest.fn(() => lineItem.getAdjustedTax / quantity), + })), + })), + getAllLineItems: jest.fn((lineItem) => lineItem), + }), + { virtual: true }, +); jest.mock( - '*/cartridge/adyen/utils/lineItemHelper', - () => ({ - getDescription: jest.fn((lineItem) => lineItem.productName), - getId: jest.fn((lineItem) => lineItem.productID), - getQuantity: jest.fn((lineItem) => lineItem.quantityValue), - getItemAmount: jest.fn((lineItem) => ({ - divide: jest.fn((quantity) => ({ - getValue: jest.fn(() => lineItem.adjustedNetPrice / quantity), - })), - })), - getVatAmount: jest.fn((lineItem) => ({ - divide: jest.fn((quantity) => ({ - getValue: jest.fn(() => lineItem.getAdjustedTax / quantity), - })), - })), - getAllLineItems: jest.fn((lineItem) => lineItem), - }), - { virtual: true }, - ); - -jest.mock('*/cartridge/models/shipping/shippingMethod', () => jest.fn(), { virtual: true }); + '*/cartridge/adyen/utils/paypalHelper', + () => ({ + getLineItems: jest.fn(() => [ + { + quantity: '1', + description: 'test', + itemCategory: 'PHYSICAL_GOODS', + sku: '123', + amountExcludingTax: '10000', + taxAmount: '1000', + }, + ]), + createPaypalUpdateOrderRequest: jest.fn(() => ({ + pspReference: 'test', + paymentData: 'test', + amount: { + value: '1000', + currency: 'USD', + }, + deliveryMethods: [ + { + reference: '001', + description: 'test', + type: 'Shipping', + amount: { + currency: 'USD', + value: '1000', + }, + selected: true, + }, + ], + })), + setBillingAndShippingAddress: jest.fn(), + }), + { virtual: true }, +); + +jest.mock( + '*/cartridge/client/default/js/adyen_checkout/helpers', + () => ({ + setOrderFormData: jest.fn(), + assignPaymentMethodValue: jest.fn(), + paymentFromComponent: jest.fn(), + resetPaymentMethod: jest.fn(), + displaySelectedMethod: jest.fn(), + showValidation: jest.fn(), + createShowConfirmationForm: jest.fn(), + getInstallmentValues: jest.fn(), + }), + { virtual: true }, +); \ No newline at end of file diff --git a/jest/sfccPathSetup.js b/jest/sfccPathSetup.js index f6592db7d..9ddc8e915 100644 --- a/jest/sfccPathSetup.js +++ b/jest/sfccPathSetup.js @@ -1,257 +1,466 @@ +/* eslint-disable global-require */ // List of mocks to let node resolve SFCC cartridge paths - // int_adyen_SFRA mocks -jest.mock('*/cartridge/adyen/scripts/index', () => { - return require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/index'); -}, {virtual: true}); - -jest.mock('*/cartridge/adyen/scripts/payments/redirect3ds1Response', () => { - return require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/payments/redirect3ds1Response'); -}, {virtual: true}); - -jest.mock('*/cartridge/controllers/middlewares/adyen/adyen3d', () => { - return require('../src/cartridges/int_adyen_SFRA/cartridge/controllers/middlewares/adyen/adyen3d'); -}, {virtual: true}); - -jest.mock('*/cartridge/controllers/middlewares/adyen/adyen3ds2', () => { - return require('../src/cartridges/int_adyen_SFRA/cartridge/controllers/middlewares/adyen/adyen3ds2'); -}, {virtual: true}); - -jest.mock('*/cartridge/adyen/scripts/payments/paymentsDetails', () => { - return require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/payments/paymentsDetails'); -}, {virtual: true}); - -jest.mock('*/cartridge/adyen/webhooks/notify', () => { - return require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/webhooks/notify'); -}, {virtual: true}); - -jest.mock('*/cartridge/adyen/scripts/payments/paymentFromComponent', () => { - return require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/payments/paymentFromComponent'); -}, {virtual: true}); - -jest.mock('*/cartridge/adyen/scripts/partialPayments/checkBalance', () => { - return require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/partialPayments/checkBalance'); -}, {virtual: true}); - -jest.mock('*/cartridge/adyen/scripts/partialPayments/cancelPartialPaymentOrder', () => { - return require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/partialPayments/cancelPartialPaymentOrder'); -}, {virtual: true}); - -jest.mock('*/cartridge/models/cart', () => { - return require('../cartridge/models/cart'); -}, {virtual: true}); - -jest.mock('*/cartridge/adyen/scripts/partialPayments/partialPaymentsOrder', () => { - return require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/partialPayments/partialPaymentsOrder'); -}, {virtual: true}); - -jest.mock('*/cartridge/adyen/scripts/partialPayments/partialPayment', () => { - return require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/partialPayments/partialPayment'); -}, {virtual: true}); - -jest.mock('*/cartridge/adyen/scripts/expressPayments/shippingMethods', () => { - return require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/shippingMethods'); -}, {virtual: true}); - -jest.mock('*/cartridge/adyen/scripts/partialPayments/fetchGiftCards', () => { - return require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/partialPayments/fetchGiftCards'); - }, {virtual: true}); - -jest.mock('*/cartridge/adyen/scripts/expressPayments/saveExpressShopperDetails', () => { - return require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/saveExpressShopperDetails'); - }, {virtual: true}); - -jest.mock('*/cartridge/adyen/scripts/payments/getCheckoutPaymentMethods', () => { - return require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/payments/getCheckoutPaymentMethods'); - }, {virtual: true}); - -jest.mock('*/cartridge/adyen/scripts/showConfirmation/showConfirmationPaymentFromComponent', () => { - return require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/showConfirmation/showConfirmationPaymentFromComponent'); -}, {virtual: true}); - -jest.mock('*/cartridge/adyen/scripts/showConfirmation/handlePaymentFromComponent', () => { - return require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/showConfirmation/handlePaymentFromComponent'); -}, {virtual: true}); +jest.mock( + '*/cartridge/adyen/scripts/index', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/index'), + { virtual: true }, +); + +jest.mock( + '*/cartridge/adyen/scripts/payments/redirect3ds1Response', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/payments/redirect3ds1Response'), + { virtual: true }, +); + +jest.mock( + '*/cartridge/controllers/middlewares/adyen/adyen3d', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/controllers/middlewares/adyen/adyen3d'), + { virtual: true }, +); + +jest.mock( + '*/cartridge/controllers/middlewares/adyen/adyen3ds2', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/controllers/middlewares/adyen/adyen3ds2'), + { virtual: true }, +); + +jest.mock( + '*/cartridge/adyen/scripts/payments/paymentsDetails', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/payments/paymentsDetails'), + { virtual: true }, +); + +jest.mock( + '*/cartridge/adyen/webhooks/notify', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/webhooks/notify'), + { virtual: true }, +); + +jest.mock( + '*/cartridge/adyen/scripts/payments/paymentFromComponent', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/payments/paymentFromComponent'), + { virtual: true }, +); + +jest.mock( + '*/cartridge/adyen/scripts/partialPayments/checkBalance', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/partialPayments/checkBalance'), + { virtual: true }, +); + +jest.mock( + '*/cartridge/adyen/scripts/partialPayments/cancelPartialPaymentOrder', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/partialPayments/cancelPartialPaymentOrder'), + { virtual: true }, +); + +jest.mock( + '*/cartridge/models/cart', + () => require('../cartridge/models/cart'), + { virtual: true }, +); + +jest.mock( + '*/cartridge/models/shipping/shippingMethod', + () => require('../cartridge/models/shipping/shippingMethod'), + { virtual: true }, + ); + +jest.mock( + '*/cartridge/adyen/scripts/partialPayments/partialPaymentsOrder', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/partialPayments/partialPaymentsOrder'), + { virtual: true }, +); + +jest.mock( + '*/cartridge/adyen/scripts/partialPayments/partialPayment', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/partialPayments/partialPayment'), + { virtual: true }, +); + +jest.mock( + '*/cartridge/adyen/scripts/expressPayments/shippingMethods', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/shippingMethods'), + { virtual: true }, +); + +jest.mock( + '*/cartridge/adyen/scripts/expressPayments/paypal/makeExpressPaymentsCall', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/paypal/makeExpressPaymentsCall'), + { virtual: true }, +); + +jest.mock( + '*/cartridge/adyen/scripts/expressPayments/paypal/makeExpressPaymentDetailsCall', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/paypal/makeExpressPaymentDetailsCall'), + { virtual: true }, +); + +jest.mock( + '*/cartridge/adyen/scripts/expressPayments/paypal/saveShopperData', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/paypal/saveShopperData'), + { virtual: true }, +); + +jest.mock( + '*/cartridge/adyen/scripts/expressPayments/paypal/handleCheckoutReview', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/paypal/handleCheckoutReview'), + { virtual: true }, +); + +jest.mock( + '*/cartridge/adyen/scripts/partialPayments/fetchGiftCards', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/partialPayments/fetchGiftCards'), + { virtual: true }, +); + +jest.mock( + '*/cartridge/adyen/scripts/expressPayments/saveExpressShopperDetails', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/saveExpressShopperDetails'), + { virtual: true }, +); + +jest.mock( + '*/cartridge/adyen/scripts/payments/getCheckoutPaymentMethods', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/payments/getCheckoutPaymentMethods'), + { virtual: true }, +); + +jest.mock( + '*/cartridge/adyen/scripts/showConfirmation/showConfirmationPaymentFromComponent', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/showConfirmation/showConfirmationPaymentFromComponent'), + { virtual: true }, +); + +jest.mock( + '*/cartridge/adyen/scripts/showConfirmation/handlePaymentFromComponent', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/showConfirmation/handlePaymentFromComponent'), + { virtual: true }, +); // middlewares/adyen/authorizeWithForm subclasses -jest.mock('*/cartridge/controllers/middlewares/adyen/authorizeWithForm', () => { - return require('../src/cartridges/int_adyen_SFRA/cartridge/controllers/middlewares/adyen/authorizeWithForm'); -}, {virtual: true}); - -jest.mock('*/cartridge/controllers/middlewares/adyen/authorizeWithForm/authorize', () => { - return require('../src/cartridges/int_adyen_SFRA/cartridge/controllers/middlewares/adyen/authorizeWithForm/authorize'); -}, {virtual: true}); - -jest.mock('*/cartridge/controllers/middlewares/adyen/authorizeWithForm/error', () => { - return require('../src/cartridges/int_adyen_SFRA/cartridge/controllers/middlewares/adyen/authorizeWithForm/error'); -}, {virtual: true}); - -jest.mock('*/cartridge/controllers/middlewares/adyen/authorizeWithForm/payment', () => { - return require('../src/cartridges/int_adyen_SFRA/cartridge/controllers/middlewares/adyen/authorizeWithForm/payment'); -}, {virtual: true}); - -jest.mock('*/cartridge/controllers/middlewares/adyen/authorizeWithForm/order', () => { - return require('../src/cartridges/int_adyen_SFRA/cartridge/controllers/middlewares/adyen/authorizeWithForm/order'); -}, {virtual: true}); +jest.mock( + '*/cartridge/controllers/middlewares/adyen/authorizeWithForm', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/controllers/middlewares/adyen/authorizeWithForm'), + { virtual: true }, +); + +jest.mock( + '*/cartridge/controllers/middlewares/adyen/authorizeWithForm/authorize', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/controllers/middlewares/adyen/authorizeWithForm/authorize'), + { virtual: true }, +); + +jest.mock( + '*/cartridge/controllers/middlewares/adyen/authorizeWithForm/error', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/controllers/middlewares/adyen/authorizeWithForm/error'), + { virtual: true }, +); + +jest.mock( + '*/cartridge/controllers/middlewares/adyen/authorizeWithForm/payment', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/controllers/middlewares/adyen/authorizeWithForm/payment'), + { virtual: true }, +); + +jest.mock( + '*/cartridge/controllers/middlewares/adyen/authorizeWithForm/order', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/controllers/middlewares/adyen/authorizeWithForm/order'), + { virtual: true }, +); // middlewares/adyen/authorize3ds2 subclasses -jest.mock('*/cartridge/controllers/middlewares/adyen/authorize3ds2', () => { - return require('../src/cartridges/int_adyen_SFRA/cartridge/controllers/middlewares/adyen/authorize3ds2'); -}, {virtual: true}); - -jest.mock('*/cartridge/controllers/middlewares/adyen/authorize3ds2/payment', () => { - return require('../src/cartridges/int_adyen_SFRA/cartridge/controllers/middlewares/adyen/authorize3ds2/payment'); -}, {virtual: true}); - -jest.mock('*/cartridge/controllers/middlewares/adyen/authorize3ds2/auth', () => { - return require('../src/cartridges/int_adyen_SFRA/cartridge/controllers/middlewares/adyen/authorize3ds2/auth'); -}, {virtual: true}); - -jest.mock('*/cartridge/controllers/middlewares/adyen/authorize3ds2/errorHandler', () => { - return require('../src/cartridges/int_adyen_SFRA/cartridge/controllers/middlewares/adyen/authorize3ds2/errorHandler'); -}, {virtual: true}); - -jest.mock('*/cartridge/controllers/middlewares/adyen/authorize3ds2/order', () => { - return require('../src/cartridges/int_adyen_SFRA/cartridge/controllers/middlewares/adyen/authorize3ds2/order'); -}, {virtual: true}); +jest.mock( + '*/cartridge/controllers/middlewares/adyen/authorize3ds2', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/controllers/middlewares/adyen/authorize3ds2'), + { virtual: true }, +); + +jest.mock( + '*/cartridge/controllers/middlewares/adyen/authorize3ds2/payment', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/controllers/middlewares/adyen/authorize3ds2/payment'), + { virtual: true }, +); + +jest.mock( + '*/cartridge/controllers/middlewares/adyen/authorize3ds2/auth', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/controllers/middlewares/adyen/authorize3ds2/auth'), + { virtual: true }, +); + +jest.mock( + '*/cartridge/controllers/middlewares/adyen/authorize3ds2/errorHandler', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/controllers/middlewares/adyen/authorize3ds2/errorHandler'), + { virtual: true }, +); + +jest.mock( + '*/cartridge/controllers/middlewares/adyen/authorize3ds2/order', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/controllers/middlewares/adyen/authorize3ds2/order'), + { virtual: true }, +); // middlewares/adyen/redirect subclasses -jest.mock('*/cartridge/controllers/middlewares/adyen/redirect', () => { - return require('../src/cartridges/int_adyen_SFRA/cartridge/controllers/middlewares/adyen/redirect'); -}, {virtual: true}); - -jest.mock('*/cartridge/controllers/middlewares/adyen/redirect/signature', () => { - return require('../src/cartridges/int_adyen_SFRA/cartridge/controllers/middlewares/adyen/redirect/signature'); -}, {virtual: true}); - -jest.mock('*/cartridge/adyen/scripts/showConfirmation/showConfirmation', () => { - return require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/showConfirmation/showConfirmation'); -}, {virtual: true}); - -jest.mock('*/cartridge/adyen/scripts/showConfirmation/order', () => { - return require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/showConfirmation/order'); -}, {virtual: true}); - -jest.mock('*/cartridge/adyen/scripts/showConfirmation/handlePayment', () => { - return require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/showConfirmation/handlePayment'); -}, {virtual: true}); - -jest.mock('*/cartridge/adyen/scripts/showConfirmation/authorise', () => { - return require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/showConfirmation/authorise'); -}, {virtual: true}); +jest.mock( + '*/cartridge/controllers/middlewares/adyen/redirect', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/controllers/middlewares/adyen/redirect'), + { virtual: true }, +); + +jest.mock( + '*/cartridge/controllers/middlewares/adyen/redirect/signature', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/controllers/middlewares/adyen/redirect/signature'), + { virtual: true }, +); + +jest.mock( + '*/cartridge/adyen/scripts/showConfirmation/showConfirmation', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/showConfirmation/showConfirmation'), + { virtual: true }, +); + +jest.mock( + '*/cartridge/adyen/scripts/showConfirmation/order', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/showConfirmation/order'), + { virtual: true }, +); + +jest.mock( + '*/cartridge/adyen/scripts/showConfirmation/handlePayment', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/showConfirmation/handlePayment'), + { virtual: true }, +); + +jest.mock( + '*/cartridge/adyen/scripts/showConfirmation/authorise', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/showConfirmation/authorise'), + { virtual: true }, +); // controllers/utils subclasses -jest.mock('*/cartridge/controllers/utils/index', () => { - return require('../src/cartridges/int_adyen_SFRA/cartridge/controllers/utils/index'); -}, {virtual: true}); - -jest.mock('*/cartridge/adyen/utils/clearForms', () => { - return require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/utils/clearForms'); -}, {virtual: true}); +jest.mock( + '*/cartridge/controllers/utils/index', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/controllers/utils/index'), + { virtual: true }, +); + +jest.mock( + '*/cartridge/adyen/utils/clearForms', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/utils/clearForms'), + { virtual: true }, +); // controllers/middlewares/checkout_services subclasses -jest.mock('*/cartridge/controllers/middlewares/checkout_services/placeOrder', () => { - return require('../src/cartridges/int_adyen_SFRA/cartridge/controllers/middlewares/checkout_services/placeOrder'); -}, {virtual: true}); - -jest.mock('*/cartridge/controllers/middlewares/checkout_services/placeOrder', () => { - return require('../src/cartridges/int_adyen_SFRA/cartridge/controllers/middlewares/checkout_services/placeOrder'); -}, {virtual: true}); +jest.mock( + '*/cartridge/controllers/middlewares/checkout_services/placeOrder', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/controllers/middlewares/checkout_services/placeOrder'), + { virtual: true }, +); + +jest.mock( + '*/cartridge/controllers/middlewares/checkout_services/placeOrder', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/controllers/middlewares/checkout_services/placeOrder'), + { virtual: true }, +); // controllers/middlewares/checkout subclasses -jest.mock('*/cartridge/controllers/middlewares/checkout/index', () => { - return require('../src/cartridges/int_adyen_SFRA/cartridge/controllers/middlewares/checkout/index'); -}, {virtual: true}); - -jest.mock('*/cartridge/controllers/middlewares/checkout/begin', () => { - return require('../src/cartridges/int_adyen_SFRA/cartridge/controllers/middlewares/checkout/begin'); -}, {virtual: true}); +jest.mock( + '*/cartridge/controllers/middlewares/checkout/index', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/controllers/middlewares/checkout/index'), + { virtual: true }, +); + +jest.mock( + '*/cartridge/controllers/middlewares/checkout/begin', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/controllers/middlewares/checkout/begin'), + { virtual: true }, +); // controllers/middlewares/order subclasses -jest.mock('*/cartridge/controllers/middlewares/order/index', () => { - return require('../src/cartridges/int_adyen_SFRA/cartridge/controllers/middlewares/order/index'); -}, {virtual: true}); - -jest.mock('*/cartridge/controllers/middlewares/order/confirm', () => { - return require('../src/cartridges/int_adyen_SFRA/cartridge/controllers/middlewares/order/confirm'); -}, {virtual: true}); +jest.mock( + '*/cartridge/controllers/middlewares/order/index', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/controllers/middlewares/order/index'), + { virtual: true }, +); + +jest.mock( + '*/cartridge/controllers/middlewares/order/confirm', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/controllers/middlewares/order/confirm'), + { virtual: true }, +); // controllers/middlewares/payment_instruments subclasses -jest.mock('*/cartridge/controllers/middlewares/payment_instruments/index', () => { - return require('../src/cartridges/int_adyen_SFRA/cartridge/controllers/middlewares/payment_instruments/index'); -}, {virtual: true}); - -jest.mock('*/cartridge/controllers/middlewares/payment_instruments/deletePayment', () => { - return require('../src/cartridges/int_adyen_SFRA/cartridge/controllers/middlewares/payment_instruments/deletePayment'); -}, {virtual: true}); - -jest.mock('*/cartridge/controllers/middlewares/payment_instruments/paymentProcessorIDs', () => { - return require('../src/cartridges/int_adyen_SFRA/cartridge/controllers/middlewares/payment_instruments/paymentProcessorIDs'); -}, {virtual: true}); - -jest.mock('*/cartridge/controllers/middlewares/payment_instruments/savePayment', () => { - return require('../src/cartridges/int_adyen_SFRA/cartridge/controllers/middlewares/payment_instruments/savePayment'); -}, {virtual: true}); +jest.mock( + '*/cartridge/controllers/middlewares/payment_instruments/index', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/controllers/middlewares/payment_instruments/index'), + { virtual: true }, +); + +jest.mock( + '*/cartridge/controllers/middlewares/payment_instruments/deletePayment', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/controllers/middlewares/payment_instruments/deletePayment'), + { virtual: true }, +); + +jest.mock( + '*/cartridge/controllers/middlewares/payment_instruments/paymentProcessorIDs', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/controllers/middlewares/payment_instruments/paymentProcessorIDs'), + { virtual: true }, +); + +jest.mock( + '*/cartridge/controllers/middlewares/payment_instruments/savePayment', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/controllers/middlewares/payment_instruments/savePayment'), + { virtual: true }, +); // scripts/checkout subclasses -jest.mock('*/cartridge/scripts/checkout/utils/index', () => { - return require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/checkout/utils/index'); -}, {virtual: true}); - -jest.mock('*/cartridge/adyen/utils/getPayments', () => { - return require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/utils/getPayments'); -}, {virtual: true}); - -jest.mock('*/cartridge/adyen/utils/validatePaymentMethod', () => { - return require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/utils/validatePaymentMethod'); -}, {virtual: true}); - -jest.mock('*/cartridge/scripts/checkout/shippingHelpers', () => { - return require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/checkout/shippingHelpers'); -}, {virtual: true}); - -jest.mock('*/cartridge/client/default/js/adyen_checkout/renderGiftcardComponent', () => { - return require('../src/cartridges/int_adyen_SFRA/client/default/js/adyen_checkout/renderGiftcardComponent'); -}, {virtual: true}); +jest.mock( + '*/cartridge/scripts/checkout/utils/index', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/checkout/utils/index'), + { virtual: true }, +); + +jest.mock( + '*/cartridge/adyen/utils/getPayments', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/utils/getPayments'), + { virtual: true }, +); + +jest.mock( + '*/cartridge/adyen/utils/validatePaymentMethod', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/utils/validatePaymentMethod'), + { virtual: true }, +); + +jest.mock( + '*/cartridge/scripts/checkout/shippingHelpers', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/checkout/shippingHelpers'), + { virtual: true }, +); + +jest.mock( + '*/cartridge/client/default/js/adyen_checkout/renderGiftcardComponent', + () => + require('../src/cartridges/int_adyen_SFRA/client/default/js/adyen_checkout/renderGiftcardComponent'), + { virtual: true }, +); // scripts/hooks/payment/processor/middlewares/authorize subclasses -jest.mock('*/cartridge/adyen/scripts/hooks/payment/processor/middlewares/authorize/paymentResponse', () => { - return require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/hooks/payment/processor/middlewares/authorize/paymentResponse'); -}, {virtual: true}); +jest.mock( + '*/cartridge/adyen/scripts/hooks/payment/processor/middlewares/authorize/paymentResponse', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/hooks/payment/processor/middlewares/authorize/paymentResponse'), + { virtual: true }, +); // int_adyen_overlay mocks -jest.mock('*/cartridge/adyen/config/constants', () => { - return require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/config/constants'); -}, {virtual: true}); - -jest.mock('*/cartridge/adyen/config/paymentMethodDescriptions', () => { - return require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/config/paymentMethodDescriptions'); -}, {virtual: true}); - -jest.mock('*/cartridge/adyen/utils/riskDataHelper', () => { - return require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/utils/riskDataHelper'); -}, {virtual: true}); - -jest.mock('*/cartridge/adyen/utils/lineItemHelper', () => { - return require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/utils/lineItemHelper'); -}, {virtual: true}); - -jest.mock('*/cartridge/adyen/scripts/payments/adyenGetOpenInvoiceData', () => { - return require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/payments/adyenGetOpenInvoiceData'); -}, {virtual: true}); - -jest.mock('*/cartridge/adyen/scripts/payments/adyenLevelTwoThreeData', () => { - return require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/payments/adyenLevelTwoThreeData'); -}, {virtual: true}); - -jest.mock('*/cartridge/adyen/logs/adyenCustomLogs', () => { - return require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/logs/adyenCustomLogs'); - }, {virtual: true}); - -jest.mock('*/cartridge/adyen/utils/giftCardsHelper', () => { - return require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/utils/giftCardsHelper'); -}, {virtual: true}); - -jest.mock('*/cartridge/adyen/scripts/payments/paypalHelper', () => { - return require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/payments/paypalHelper'); -}, {virtual: true}); \ No newline at end of file +jest.mock( + '*/cartridge/adyen/config/constants', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/config/constants'), + { virtual: true }, +); + +jest.mock( + '*/cartridge/adyen/config/paymentMethodDescriptions', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/config/paymentMethodDescriptions'), + { virtual: true }, +); + +jest.mock( + '*/cartridge/adyen/utils/riskDataHelper', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/utils/riskDataHelper'), + { virtual: true }, +); + +jest.mock( + '*/cartridge/adyen/utils/lineItemHelper', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/utils/lineItemHelper'), + { virtual: true }, +); + +jest.mock( + '*/cartridge/adyen/scripts/payments/adyenGetOpenInvoiceData', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/payments/adyenGetOpenInvoiceData'), + { virtual: true }, +); + +jest.mock( + '*/cartridge/adyen/scripts/payments/adyenLevelTwoThreeData', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/payments/adyenLevelTwoThreeData'), + { virtual: true }, +); + +jest.mock( + '*/cartridge/adyen/logs/adyenCustomLogs', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/logs/adyenCustomLogs'), + { virtual: true }, +); + +jest.mock( + '*/cartridge/adyen/utils/giftCardsHelper', + () => + require('../src/cartridges/int_adyen_SFRA/cartridge/adyen/utils/giftCardsHelper'), + { virtual: true }, +); diff --git a/metadata/site_import/meta/system-objecttype-extensions.xml b/metadata/site_import/meta/system-objecttype-extensions.xml index 1705975a4..3d08ae711 100644 --- a/metadata/site_import/meta/system-objecttype-extensions.xml +++ b/metadata/site_import/meta/system-objecttype-extensions.xml @@ -44,6 +44,14 @@ false 0 + + Adyen PayPal Express Response + PayPal Express response used to render the confirmation form + text + false + false + 0 + @@ -53,6 +61,7 @@ + @@ -544,6 +553,20 @@ false true + + Enable PayPal express checkout + boolean + false + false + true + + + Enable PayPal express review page + boolean + false + false + false + Enable Adyen Installments boolean @@ -697,6 +720,8 @@ + + diff --git a/metadata/site_import/services.xml b/metadata/site_import/services.xml index 013f37276..97edaca9e 100644 --- a/metadata/site_import/services.xml +++ b/metadata/site_import/services.xml @@ -100,6 +100,16 @@ + + https://checkout-test.adyen.com/v71/paypal/updateOrder + + + + + https://[YOUR_LIVE_PREFIX]-checkout-live.adyenpayments.com/checkout/v71/paypal/updateOrder + + + 30000 @@ -209,4 +219,13 @@ Adyen AdyenCheckBalance + + HTTP + true + adyen + true + false + Adyen + AdyenPaypalUpdateOrder + diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 000000000..3882f9338 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,5 @@ +sonar.organization=adyen +sonar.projectKey=Adyen_adyen-salesforce-commerce-cloud +sonar.sources=. +sonar.exclusions=cartridges/**/*, jest/**/*, tests/**/* +sonar.javascript.lcov.reportPaths=./coverage/lcov.info diff --git a/src/cartridges/app_adyen_SFRA/cartridge/client/default/js/__tests__/__snapshots__/paypalExpress.test.js.snap b/src/cartridges/app_adyen_SFRA/cartridge/client/default/js/__tests__/__snapshots__/paypalExpress.test.js.snap new file mode 100644 index 000000000..4f1f42c1e --- /dev/null +++ b/src/cartridges/app_adyen_SFRA/cartridge/client/default/js/__tests__/__snapshots__/paypalExpress.test.js.snap @@ -0,0 +1,32 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`paypal express getPaypalButtonConfig should return config when review page is enabled 1`] = ` +{ + "configuration": {}, + "isExpress": true, + "onAdditionalDetails": [Function], + "onError": [Function], + "onShippingAddressChange": [Function], + "onShippingOptionsChange": [Function], + "onShopperDetails": [Function], + "onSubmit": [Function], + "returnUrl": "test_returnUrl", + "showPayButton": true, + "userAction": "continue", +} +`; + +exports[`paypal express getPaypalButtonConfig should return config when review page is not enabled 1`] = ` +{ + "configuration": {}, + "isExpress": true, + "onAdditionalDetails": [Function], + "onError": [Function], + "onShippingAddressChange": [Function], + "onShippingOptionsChange": [Function], + "onShopperDetails": [Function], + "onSubmit": [Function], + "returnUrl": "test_returnUrl", + "showPayButton": true, +} +`; diff --git a/src/cartridges/app_adyen_SFRA/cartridge/client/default/js/__tests__/paypalExpress.test.js b/src/cartridges/app_adyen_SFRA/cartridge/client/default/js/__tests__/paypalExpress.test.js new file mode 100644 index 000000000..2a42b5e4c --- /dev/null +++ b/src/cartridges/app_adyen_SFRA/cartridge/client/default/js/__tests__/paypalExpress.test.js @@ -0,0 +1,522 @@ +/** + * @jest-environment jsdom + */ + +const { + callPaymentFromComponent, + saveShopperDetails, + redirectToReviewPage, + makeExpressPaymentDetailsCall, + updateComponent, + handleShippingAddressChange, + handleShippingOptionChange, + getPaypalButtonConfig, + mountPaypalComponent, +} = require('../paypalExpress.js') +const helpers = require('../adyen_checkout/helpers'); + +describe('paypal express', () => { + describe('callPaymentFromComponent', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + afterEach(() => { + jest.resetModules(); + }); + it('should make successful payments call for express', async () => { + const start = jest.fn(); + global.$.spinner = jest.fn(() => {return { + start: start + }}) + global.fetch = jest.fn().mockResolvedValueOnce({ + ok: true, + json: jest.fn(() => {return {action: {}}}) + }) + const component = { + handleError: jest.fn(), + handleAction: jest.fn() + } + await callPaymentFromComponent({}, component); + expect(start).toHaveBeenCalledTimes(1); + expect(component.handleAction).toHaveBeenCalledTimes(1); + expect(component.handleError).not.toHaveBeenCalled(); + }) + it('should handle failed payments call for express when response is not ok', async () => { + const start = jest.fn(); + global.$.spinner = jest.fn(() => {return { + start: start + }}) + global.fetch = jest.fn().mockResolvedValueOnce({ + ok: true, + json: jest.fn(() => {return {}}) + }) + const component = { + handleError: jest.fn(), + handleAction: jest.fn() + } + await callPaymentFromComponent({}, component); + expect(start).toHaveBeenCalledTimes(1); + expect(component.handleError).toHaveBeenCalledTimes(1); + expect(component.handleAction).not.toHaveBeenCalled(); + }) + it('should handle failed payments call for express when response is ok but there is no "action" in response', async () => { + const start = jest.fn(); + global.$.spinner = jest.fn(() => {return { + start: start + }}) + global.fetch = jest.fn().mockRejectedValueOnce({}) + const component = { + handleError: jest.fn(), + handleAction: jest.fn() + } + await callPaymentFromComponent({}, component); + expect(start).toHaveBeenCalledTimes(1); + expect(component.handleError).toHaveBeenCalledTimes(1); + expect(component.handleAction).not.toHaveBeenCalled(); + }) +}) + describe('saveShopperDetails', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + afterEach(() => { + jest.resetModules(); + }); + it('should make successful save shopper data call', async () => { + const stop = jest.fn(); + global.$.spinner = jest.fn(() => {return { + stop: stop + }}) + $.ajax = jest.fn().mockImplementation(({success}) => Promise.resolve(success())); + const actions = { + resolve: jest.fn() + } + await saveShopperDetails({}, actions); + expect(actions.resolve).toHaveBeenCalledTimes(1); + expect(stop).not.toHaveBeenCalled(); + }) + it('should stop spinner if save shopper data call fails', async () => { + const stop = jest.fn(); + global.$.spinner = jest.fn(() => {return { + stop: stop + }}) + $.ajax = jest.fn().mockImplementation(({error}) => Promise.resolve(error())); + const actions = { + resolve: jest.fn() + } + await saveShopperDetails({}, actions); + expect(stop).toHaveBeenCalledTimes(1); + expect(actions.resolve).not.toHaveBeenCalled(); + }) + }) + describe('makeExpressPaymentDetailsCall', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + afterEach(() => { + jest.resetModules(); + }); + it('should make successful express payment details call', async () => { + const stop = jest.fn(); + const createShowConfirmationForm = jest.fn(); + const setOrderFormData = jest.fn(); + global.$.spinner = jest.fn(() => {return { + stop: stop + }}) + helpers.setOrderFormData = setOrderFormData; + helpers.createShowConfirmationForm = createShowConfirmationForm; + $.ajax = jest.fn().mockImplementation(({success}) => Promise.resolve(success())); + + await makeExpressPaymentDetailsCall({}); + expect(createShowConfirmationForm).toHaveBeenCalledTimes(1); + expect(setOrderFormData).toHaveBeenCalledTimes(1); + expect(stop).not.toHaveBeenCalled(); + }) + it('should stop spinner if express payment details call fails', async () => { + const stop = jest.fn(); + const createShowConfirmationForm = jest.fn(); + const setOrderFormData = jest.fn(); + global.$.spinner = jest.fn(() => {return { + stop: stop + }}) + helpers.setOrderFormData = setOrderFormData; + helpers.createShowConfirmationForm = createShowConfirmationForm; + $.ajax = jest.fn().mockImplementation(({error}) => Promise.resolve(error())); + + await makeExpressPaymentDetailsCall({}); + expect(stop).toHaveBeenCalledTimes(1); + expect(createShowConfirmationForm).not.toHaveBeenCalled(); + expect(setOrderFormData).not.toHaveBeenCalled(); + }) + }) + describe('handleShippingAddressChange', () => { + window.shippingMethodsUrl= 'test_url'; + beforeEach(() => { + jest.clearAllMocks(); + }); + afterEach(() => { + jest.resetModules(); + }); + it('should make successful shipping address change call', async () => { + const data = { + shippingAddress: { + city: 'Amsterdam', + country: 'Netherlands', + countryCode: 'NL', + state: 'AMS', + postalCode: '1001', + }, + errors: { + ADDRESS_ERROR: 'test_error' + } + } + const actions = { + reject: jest.fn() + } + const component = { + updatePaymentData: jest.fn(), + paymentData: 'test_paymentData' + } + global.fetch = jest.fn().mockResolvedValueOnce({ + ok: true, + json: jest.fn(() => ({paymentData: 'test_paymentData', status: 'success'})) + }) + + const request = { + method: 'POST', + headers: { + 'Content-Type': 'application/json; charset=utf-8', + }, + body: JSON.stringify({ paymentMethodType: 'paypal', currentPaymentData: 'test_paymentData', address: { + city: 'Amsterdam', + country: 'Netherlands', + countryCode: 'NL', + stateCode: 'AMS', + postalCode: '1001', + } }), + } + + await handleShippingAddressChange(data, actions, component); + expect(global.fetch).toHaveBeenCalledWith('test_url', request); + expect(component.updatePaymentData).toHaveBeenCalledTimes(1); + expect(actions.reject).not.toHaveBeenCalled(); + }) + it('should not make shipping address change call if no shipping address is present', async () => { + const data = { + errors: { + ADDRESS_ERROR: 'test_error' + } + } + const actions = { + reject: jest.fn() + } + const component = { + updatePaymentData: jest.fn(), + paymentData: 'test_paymentData' + } + global.fetch = jest.fn().mockResolvedValueOnce({ + ok: true, + json: jest.fn(() => ({paymentData: 'test_paymentData', status: 'success'})) + }) + + await handleShippingAddressChange(data, actions, component); + expect(actions.reject).toHaveBeenCalledTimes(1); + expect(global.fetch).not.toHaveBeenCalled(); + expect(component.updatePaymentData).not.toHaveBeenCalled(); + }) + it('should handle failed shipping address change call', async () => { + const data = { + shippingAddress: { + city: 'Amsterdam', + country: 'Netherlands', + countryCode: 'NL', + state: 'AMS', + postalCode: '1001', + }, + errors: { + ADDRESS_ERROR: 'test_error' + } + } + const actions = { + reject: jest.fn() + } + const component = { + updatePaymentData: jest.fn(), + paymentData: 'test_paymentData' + } + global.fetch = jest.fn().mockRejectedValueOnce({}) + + await handleShippingAddressChange(data, actions, component); + expect(actions.reject).toHaveBeenCalledTimes(1); + expect(component.updatePaymentData).not.toHaveBeenCalled(); + }) + it('should handle shipping address change call when response is not ok', async () => { + const data = { + shippingAddress: { + city: 'Amsterdam', + country: 'Netherlands', + countryCode: 'NL', + state: 'AMS', + postalCode: '1001', + }, + errors: { + ADDRESS_ERROR: 'test_error' + } + } + const actions = { + reject: jest.fn() + } + const component = { + updatePaymentData: jest.fn(), + paymentData: 'test_paymentData' + } + global.fetch = jest.fn().mockResolvedValueOnce({ + ok: false, + json: jest.fn(() => ({paymentData: 'test_paymentData', status: 'success'})) + }) + + await handleShippingAddressChange(data, actions, component); + expect(actions.reject).toHaveBeenCalledTimes(1); + expect(component.updatePaymentData).not.toHaveBeenCalled(); + }) + it('should handle shipping address change call when status is failed', async () => { + const data = { + shippingAddress: { + city: 'Amsterdam', + country: 'Netherlands', + countryCode: 'NL', + state: 'AMS', + postalCode: '1001', + }, + errors: { + ADDRESS_ERROR: 'test_error' + } + } + const actions = { + reject: jest.fn() + } + const component = { + updatePaymentData: jest.fn(), + paymentData: 'test_paymentData' + } + global.fetch = jest.fn().mockResolvedValueOnce({ + ok: false, + json: jest.fn(() => ({paymentData: 'test_paymentData', status: 'failed'})) + }) + + await handleShippingAddressChange(data, actions, component); + expect(actions.reject).toHaveBeenCalledTimes(1); + expect(component.updatePaymentData).not.toHaveBeenCalled(); + }) + it('should handle shipping address change call when paymentData is not returned', async () => { + const data = { + shippingAddress: { + city: 'Amsterdam', + country: 'Netherlands', + countryCode: 'NL', + state: 'AMS', + postalCode: '1001', + }, + errors: { + ADDRESS_ERROR: 'test_error' + } + } + const actions = { + reject: jest.fn() + } + const component = { + updatePaymentData: jest.fn(), + paymentData: 'test_paymentData' + } + global.fetch = jest.fn().mockResolvedValueOnce({ + ok: false, + json: jest.fn(() => ({ status: 'success'})) + }) + + await handleShippingAddressChange(data, actions, component); + expect(actions.reject).toHaveBeenCalledTimes(1); + expect(component.updatePaymentData).not.toHaveBeenCalled(); + }) + }) + describe('handleShippingOptionChange', () => { + window.selectShippingMethodUrl= 'test_url'; + beforeEach(() => { + jest.clearAllMocks(); + }); + afterEach(() => { + jest.resetModules(); + }); + it('should make successful shipping option change call', async () => { + const data = { + selectedShippingOption: { + id: 'test', + }, + errors: { + METHOD_UNAVAILABLE: 'test_error' + } + } + const actions = { + reject: jest.fn() + } + const component = { + updatePaymentData: jest.fn(), + paymentData: 'test_paymentData' + } + global.fetch = jest.fn().mockResolvedValueOnce({ + ok: true, + json: jest.fn(() => ({paymentData: 'test_paymentData', status: 'success'})) + }) + + const request = { + method: 'POST', + headers: { + 'Content-Type': 'application/json; charset=utf-8', + }, + body: JSON.stringify({ paymentMethodType: 'paypal', currentPaymentData: 'test_paymentData', methodID: 'test' }), + } + + await handleShippingOptionChange(data, actions, component); + expect(global.fetch).toHaveBeenCalledWith('test_url', request); + expect(component.updatePaymentData).toHaveBeenCalledTimes(1); + expect(actions.reject).not.toHaveBeenCalled(); + }) + it('should not make shipping option change call if no shipping option is present', async () => { + const data = { + errors: { + METHOD_UNAVAILABLE: 'test_error' + } + } + const actions = { + reject: jest.fn() + } + const component = { + updatePaymentData: jest.fn(), + paymentData: 'test_paymentData' + } + global.fetch = jest.fn().mockResolvedValueOnce({ + ok: true, + json: jest.fn(() => ({paymentData: 'test_paymentData', status: 'success'})) + }) + + await handleShippingOptionChange(data, actions, component); + expect(actions.reject).toHaveBeenCalledTimes(1); + expect(global.fetch).not.toHaveBeenCalled(); + expect(component.updatePaymentData).not.toHaveBeenCalled(); + }) + it('should handle failed shipping options change call', async () => { + const data = { + selectedShippingOption: { + id: 'test', + }, + errors: { + METHOD_UNAVAILABLE: 'test_error' + } + } + const actions = { + reject: jest.fn() + } + const component = { + updatePaymentData: jest.fn(), + paymentData: 'test_paymentData' + } + global.fetch = jest.fn().mockRejectedValueOnce({}) + + await handleShippingOptionChange(data, actions, component); + expect(actions.reject).toHaveBeenCalledTimes(1); + expect(component.updatePaymentData).not.toHaveBeenCalled(); + }) + it('should handle shipping option change call when response is not ok', async () => { + const data = { + selectedShippingOption: { + id: 'test', + }, + errors: { + METHOD_UNAVAILABLE: 'test_error' + } + } + const actions = { + reject: jest.fn() + } + const component = { + updatePaymentData: jest.fn(), + paymentData: 'test_paymentData' + } + global.fetch = jest.fn().mockResolvedValueOnce({ + ok: false, + json: jest.fn(() => ({paymentData: 'test_paymentData', status: 'success'})) + }) + + await handleShippingOptionChange(data, actions, component); + expect(actions.reject).toHaveBeenCalledTimes(1); + expect(component.updatePaymentData).not.toHaveBeenCalled(); + }) + it('should handle shipping option change call when status is failed', async () => { + const data = { + selectedShippingOption: { + id: 'test', + }, + errors: { + METHOD_UNAVAILABLE: 'test_error' + } + } + const actions = { + reject: jest.fn() + } + const component = { + updatePaymentData: jest.fn(), + paymentData: 'test_paymentData' + } + global.fetch = jest.fn().mockResolvedValueOnce({ + ok: false, + json: jest.fn(() => ({paymentData: 'test_paymentData', status: 'failed'})) + }) + + await handleShippingOptionChange(data, actions, component); + expect(actions.reject).toHaveBeenCalledTimes(1); + expect(component.updatePaymentData).not.toHaveBeenCalled(); + }) + it('should handle shipping option change call when paymentData is not returned', async () => { + const data = { + selectedShippingOption: { + id: 'test', + }, + errors: { + METHOD_UNAVAILABLE: 'test_error' + } + } + const actions = { + reject: jest.fn() + } + const component = { + updatePaymentData: jest.fn(), + paymentData: 'test_paymentData' + } + global.fetch = jest.fn().mockResolvedValueOnce({ + ok: false, + json: jest.fn(() => ({ status: 'success'})) + }) + + await handleShippingOptionChange(data, actions, component); + expect(actions.reject).toHaveBeenCalledTimes(1); + expect(component.updatePaymentData).not.toHaveBeenCalled(); + }) + }) + describe('getPaypalButtonConfig',() => { + window.returnUrl = 'test_returnUrl'; + beforeEach(() => { + jest.clearAllMocks(); + }); + afterEach(() => { + jest.resetModules(); + }); + it('should return config when review page is not enabled',() => { + const paypalButtonConfig = getPaypalButtonConfig({}); + expect(paypalButtonConfig).toMatchSnapshot(); + }) + it('should return config when review page is enabled',() => { + window.paypalReviewPageEnabled = true; + const paypalButtonConfig = getPaypalButtonConfig({}); + expect(paypalButtonConfig).toMatchSnapshot(); + }) + }) +}) diff --git a/src/cartridges/app_adyen_SFRA/cartridge/client/default/js/amazonPayExpressPart1.js b/src/cartridges/app_adyen_SFRA/cartridge/client/default/js/amazonPayExpressPart1.js index 3cb422045..5301e79c4 100644 --- a/src/cartridges/app_adyen_SFRA/cartridge/client/default/js/amazonPayExpressPart1.js +++ b/src/cartridges/app_adyen_SFRA/cartridge/client/default/js/amazonPayExpressPart1.js @@ -3,8 +3,7 @@ const { updateLoadedExpressMethods, getPaymentMethods, } = require('./commons'); - -const AMAZON_PAY = 'amazonpay'; +const { AMAZON_PAY } = require('./constants'); async function mountAmazonPayComponent() { try { diff --git a/src/cartridges/app_adyen_SFRA/cartridge/client/default/js/applePayExpress.js b/src/cartridges/app_adyen_SFRA/cartridge/client/default/js/applePayExpress.js index 696599ddd..ebd0c3027 100644 --- a/src/cartridges/app_adyen_SFRA/cartridge/client/default/js/applePayExpress.js +++ b/src/cartridges/app_adyen_SFRA/cartridge/client/default/js/applePayExpress.js @@ -7,25 +7,6 @@ let checkout; let shippingMethodsData; let paymentMethodsResponse; -async function initializeCheckout() { - paymentMethodsResponse = await getPaymentMethods(); - const shippingMethods = await fetch(window.shippingMethodsUrl); - shippingMethodsData = await shippingMethods.json(); - const applicationInfo = paymentMethodsResponse?.applicationInfo; - checkout = await AdyenCheckout({ - environment: window.environment, - clientKey: window.clientKey, - locale: window.locale, - analytics: { - analyticsData: { applicationInfo }, - }, - }); -} - -async function createApplePayButton(applePayButtonConfig) { - return checkout.create(APPLE_PAY, applePayButtonConfig); -} - function formatCustomerObject(customerData, billingData) { return { addressBook: { @@ -124,6 +105,61 @@ function callPaymentFromComponent(data, resolveApplePay, rejectApplePay) { }); } +function selectShippingMethod({ shipmentUUID, ID }) { + const request = { + paymentMethodType: APPLE_PAY, + shipmentUUID, + methodID: ID, + }; + return fetch(window.selectShippingMethodUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json; charset=utf-8', + }, + body: JSON.stringify(request), + }); +} + +function getShippingMethod(shippingContact) { + const request = { + paymentMethodType: APPLE_PAY, + }; + if (shippingContact) { + request.address = { + city: shippingContact.locality, + country: shippingContact.country, + countryCode: shippingContact.countryCode, + stateCode: shippingContact.administrativeArea, + postalCode: shippingContact.postalCode, + }; + } + return fetch(window.shippingMethodsUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json; charset=utf-8', + }, + body: JSON.stringify(request), + }); +} + +async function initializeCheckout() { + paymentMethodsResponse = await getPaymentMethods(); + const shippingMethods = await getShippingMethod(); + shippingMethodsData = await shippingMethods.json(); + const applicationInfo = paymentMethodsResponse?.applicationInfo; + checkout = await AdyenCheckout({ + environment: window.environment, + clientKey: window.clientKey, + locale: window.locale, + analytics: { + analyticsData: { applicationInfo }, + }, + }); +} + +async function createApplePayButton(applePayButtonConfig) { + return checkout.create(APPLE_PAY, applePayButtonConfig); +} initializeCheckout() .then(async () => { const applePayPaymentMethod = @@ -198,14 +234,8 @@ initializeCheckout() const matchingShippingMethod = shippingMethodsData.shippingMethods.find( (sm) => sm.ID === shippingMethod.identifier, ); - const calculationResponse = await fetch( - `${window.calculateAmountUrl}?${new URLSearchParams({ - shipmentUUID: matchingShippingMethod.shipmentUUID, - methodID: matchingShippingMethod.ID, - })}`, - { - method: 'POST', - }, + const calculationResponse = await selectShippingMethod( + matchingShippingMethod, ); if (calculationResponse.ok) { const newCalculation = await calculationResponse.json(); @@ -227,28 +257,14 @@ initializeCheckout() }, onShippingContactSelected: async (resolve, reject, event) => { const { shippingContact } = event; - const shippingMethods = await fetch( - `${window.shippingMethodsUrl}?${new URLSearchParams({ - city: shippingContact.locality, - country: shippingContact.country, - countryCode: shippingContact.countryCode, - stateCode: shippingContact.administrativeArea, - postalCode: shippingContact.postalCode, - })}`, - ); + const shippingMethods = await getShippingMethod(shippingContact); if (shippingMethods.ok) { shippingMethodsData = await shippingMethods.json(); if (shippingMethodsData.shippingMethods?.length) { const selectedShippingMethod = shippingMethodsData.shippingMethods[0]; - const calculationResponse = await fetch( - `${window.calculateAmountUrl}?${new URLSearchParams({ - shipmentUUID: selectedShippingMethod.shipmentUUID, - methodID: selectedShippingMethod.ID, - })}`, - { - method: 'POST', - }, + const calculationResponse = await selectShippingMethod( + selectedShippingMethod, ); if (calculationResponse.ok) { const shippingMethodsStructured = diff --git a/src/cartridges/app_adyen_SFRA/cartridge/client/default/js/checkoutReviewButtons.js b/src/cartridges/app_adyen_SFRA/cartridge/client/default/js/checkoutReviewButtons.js new file mode 100644 index 000000000..6a33eb2da --- /dev/null +++ b/src/cartridges/app_adyen_SFRA/cartridge/client/default/js/checkoutReviewButtons.js @@ -0,0 +1,41 @@ +const helpers = require('./adyen_checkout/helpers'); + +/** + * make payment details call for express payment methods from review page . + * @param data - state data from adyen checkout component + * @return {undefined} + */ +function makeExpressPaymentDetailsCall(data) { + $.ajax({ + type: 'POST', + url: window.makeExpressPaymentDetailsCall, + data: JSON.stringify({ data }), + contentType: 'application/json; charset=utf-8', + async: false, + success(response) { + helpers.setOrderFormData(response); + }, + error() { + $.spinner().stop(); + }, + }); +} + +/** + * initializes place order button on checkout review page. + * @return {undefined} + */ +function initCheckoutReviewButtons() { + $(document).ready(() => { + $("button[name='place-order']").click(() => { + $.spinner().start(); + const stateData = document.querySelector( + '#additionalDetailsHidden', + ).value; + makeExpressPaymentDetailsCall(JSON.parse(stateData)); + document.querySelector('#showConfirmationForm').submit(); + }); + }); +} + +initCheckoutReviewButtons(); diff --git a/src/cartridges/app_adyen_SFRA/cartridge/client/default/js/commons/index.js b/src/cartridges/app_adyen_SFRA/cartridge/client/default/js/commons/index.js index 5b89aef32..8314e7d2d 100644 --- a/src/cartridges/app_adyen_SFRA/cartridge/client/default/js/commons/index.js +++ b/src/cartridges/app_adyen_SFRA/cartridge/client/default/js/commons/index.js @@ -1,4 +1,5 @@ const store = require('../../../../store'); +const { PAYPAL, APPLE_PAY, AMAZON_PAY } = require('../constants'); module.exports.onFieldValid = function onFieldValid(data) { if (data.endDigits) { @@ -34,8 +35,9 @@ module.exports.getPaymentMethods = async function getPaymentMethods() { module.exports.checkIfExpressMethodsAreReady = function checkIfExpressMethodsAreReady() { const expressMethodsConfig = { - applepay: window.isApplePayExpressEnabled === 'true', - amazonpay: window.isAmazonPayExpressEnabled === 'true', + [APPLE_PAY]: window.isApplePayExpressEnabled === 'true', + [AMAZON_PAY]: window.isAmazonPayExpressEnabled === 'true', + [PAYPAL]: window.isPayPalExpressEnabled === 'true', }; let enabledExpressMethods = []; Object.keys(expressMethodsConfig).forEach((key) => { diff --git a/src/cartridges/app_adyen_SFRA/cartridge/client/default/js/constants.js b/src/cartridges/app_adyen_SFRA/cartridge/client/default/js/constants.js index ccc163818..4de7972c9 100644 --- a/src/cartridges/app_adyen_SFRA/cartridge/client/default/js/constants.js +++ b/src/cartridges/app_adyen_SFRA/cartridge/client/default/js/constants.js @@ -11,6 +11,8 @@ module.exports = { SCHEME: 'scheme', GIROPAY: 'giropay', APPLE_PAY: 'applepay', + PAYPAL: 'paypal', + AMAZON_PAY: 'amazonpay', ACTIONTYPE: { QRCODE: 'qrCode', }, diff --git a/src/cartridges/app_adyen_SFRA/cartridge/client/default/js/paypalExpress.js b/src/cartridges/app_adyen_SFRA/cartridge/client/default/js/paypalExpress.js new file mode 100644 index 000000000..fe25b39da --- /dev/null +++ b/src/cartridges/app_adyen_SFRA/cartridge/client/default/js/paypalExpress.js @@ -0,0 +1,225 @@ +const { + getPaymentMethods, + updateLoadedExpressMethods, + checkIfExpressMethodsAreReady, +} = require('./commons'); +const helpers = require('./adyen_checkout/helpers'); +const { PAYPAL } = require('./constants'); + +async function callPaymentFromComponent(data, component) { + try { + $.spinner().start(); + const response = await fetch(window.makeExpressPaymentsCall, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }); + const { action, errorMessage = '' } = await response.json(); + if (response.ok && action) { + component.handleAction(action); + } else { + throw new Error(errorMessage); + } + } catch (e) { + component.handleError(); + } +} + +async function saveShopperDetails(details, actions) { + return $.ajax({ + url: window.saveShopperData, + type: 'post', + data: { + shopperDetails: JSON.stringify(details), + }, + success() { + actions.resolve(); + }, + error() { + $.spinner().stop(); + }, + }); +} + +function redirectToReviewPage(data) { + const redirect = $('
').appendTo(document.body).attr({ + method: 'POST', + action: window.checkoutReview, + }); + $('') + .appendTo(redirect) + .attr({ + name: 'data', + value: JSON.stringify(data), + }); + + redirect.submit(); +} + +function makeExpressPaymentDetailsCall(data) { + return $.ajax({ + type: 'POST', + url: window.makeExpressPaymentDetailsCall, + data: JSON.stringify({ data }), + contentType: 'application/json; charset=utf-8', + async: false, + success(response) { + helpers.createShowConfirmationForm(window.showConfirmationAction); + helpers.setOrderFormData(response); + }, + error() { + $.spinner().stop(); + }, + }); +} + +async function updateComponent(response, component) { + if (response.ok) { + const { paymentData, status, errorMessage = '' } = await response.json(); + if (!paymentData || status !== 'success') { + throw new Error(errorMessage); + } + // Update the Component paymentData value with the new one. + component.updatePaymentData(paymentData); + } else { + const { errorMessage = '' } = await response.json(); + throw new Error(errorMessage); + } + return false; +} +async function handleShippingAddressChange(data, actions, component) { + try { + const { shippingAddress, errors } = data; + const currentPaymentData = component.paymentData; + if (!shippingAddress) { + throw new Error(errors?.ADDRESS_ERROR); + } + const request = { + paymentMethodType: PAYPAL, + currentPaymentData, + address: { + city: shippingAddress.city, + country: shippingAddress.country, + countryCode: shippingAddress.countryCode, + stateCode: shippingAddress.state, + postalCode: shippingAddress.postalCode, + }, + }; + const response = await fetch(window.shippingMethodsUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json; charset=utf-8', + }, + body: JSON.stringify(request), + }); + await updateComponent(response, component); + } catch (e) { + actions.reject(); + } + return false; +} + +async function handleShippingOptionChange(data, actions, component) { + try { + const { selectedShippingOption, errors } = data; + const currentPaymentData = component.paymentData; + if (!selectedShippingOption) { + throw new Error(errors?.METHOD_UNAVAILABLE); + } + const request = { + paymentMethodType: PAYPAL, + currentPaymentData, + methodID: selectedShippingOption?.id, + }; + const response = await fetch(window.selectShippingMethodUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json; charset=utf-8', + }, + body: JSON.stringify(request), + }); + await updateComponent(response, component); + } catch (e) { + actions.reject(); + } + return false; +} + +function getPaypalButtonConfig(paypalConfig) { + const { paypalReviewPageEnabled } = window; + return { + showPayButton: true, + configuration: paypalConfig, + returnUrl: window.returnUrl, + isExpress: true, + ...(paypalReviewPageEnabled ? { userAction: 'continue' } : {}), + onSubmit: async (state, component) => { + await callPaymentFromComponent(state.data, component); + }, + onError: async () => { + $.spinner().stop(); + }, + onShopperDetails: async (shopperDetails, rawData, actions) => { + await saveShopperDetails(shopperDetails, actions); + }, + onAdditionalDetails: (state) => { + if (paypalReviewPageEnabled) { + redirectToReviewPage(state.data); + } else { + makeExpressPaymentDetailsCall(state.data); + document.querySelector('#additionalDetailsHidden').value = + JSON.stringify(state.data); + document.querySelector('#showConfirmationForm').submit(); + } + }, + onShippingAddressChange: async (data, actions, component) => { + await handleShippingAddressChange(data, actions, component); + }, + onShippingOptionsChange: async (data, actions, component) => { + await handleShippingOptionChange(data, actions, component); + }, + }; +} + +async function mountPaypalComponent() { + try { + const paymentMethod = await getPaymentMethods(); + const paymentMethodsResponse = paymentMethod?.AdyenPaymentMethods; + const applicationInfo = paymentMethod?.applicationInfo; + const paypalConfig = paymentMethodsResponse?.paymentMethods.find( + (pm) => pm.type === PAYPAL, + )?.configuration; + if (!paypalConfig) return; + const checkout = await AdyenCheckout({ + environment: window.environment, + clientKey: window.clientKey, + locale: window.locale, + analytics: { + analyticsData: { applicationInfo }, + }, + }); + const paypalButtonConfig = getPaypalButtonConfig(paypalConfig); + const paypalExpressButton = checkout.create(PAYPAL, paypalButtonConfig); + paypalExpressButton.mount('#paypal-container'); + updateLoadedExpressMethods(PAYPAL); + checkIfExpressMethodsAreReady(); + } catch (e) { + // + } +} + +mountPaypalComponent(); + +module.exports = { + callPaymentFromComponent, + saveShopperDetails, + redirectToReviewPage, + makeExpressPaymentDetailsCall, + updateComponent, + handleShippingAddressChange, + handleShippingOptionChange, + getPaypalButtonConfig, + mountPaypalComponent, +}; diff --git a/src/cartridges/app_adyen_SFRA/cartridge/templates/default/adyen/checkoutReviewButtons.isml b/src/cartridges/app_adyen_SFRA/cartridge/templates/default/adyen/checkoutReviewButtons.isml new file mode 100644 index 000000000..cf4924988 --- /dev/null +++ b/src/cartridges/app_adyen_SFRA/cartridge/templates/default/adyen/checkoutReviewButtons.isml @@ -0,0 +1,23 @@ + + var assets = require('*/cartridge/scripts/assets.js'); + assets.addJs('/js/checkoutReviewButtons.js'); + assets.addCss('/css/checkout/checkout.css'); + + +
+
+
+ + + + + + + +
+
+
diff --git a/src/cartridges/app_adyen_SFRA/cartridge/templates/default/cart/checkoutButtons.isml b/src/cartridges/app_adyen_SFRA/cartridge/templates/default/cart/checkoutButtons.isml index b187a14c8..630c5db42 100644 --- a/src/cartridges/app_adyen_SFRA/cartridge/templates/default/cart/checkoutButtons.isml +++ b/src/cartridges/app_adyen_SFRA/cartridge/templates/default/cart/checkoutButtons.isml @@ -43,9 +43,9 @@
+ +
+
+ ${Resource.msgf('label.number.items.in.cart','cart', null, pdict.order.items.totalQuantity)} + ${pdict.order.totals.subTotal} +
+
+
+
+ + + +
+
+

${Resource.msg('heading.checkout.customer', 'checkout', null)}

+
+
+ +
+
+
+ + +
+
+

${Resource.msg('heading.checkout.shipping', 'checkout', null)}

+
+
+ +
+
+ + +
+
+

${Resource.msg('heading.payment', 'checkout', null)}

+
+ +
+ +
+
+ +
+ + + + +
+ +
+
+

${Resource.msg('heading.order.summary', 'checkout', null)}

+
+
+ +
+
+ + +
+
+
+ + diff --git a/src/cartridges/app_adyen_SFRA/cartridge/templates/resources/adyen.properties b/src/cartridges/app_adyen_SFRA/cartridge/templates/resources/adyen.properties index ef57083a9..4af6b7d5c 100644 --- a/src/cartridges/app_adyen_SFRA/cartridge/templates/resources/adyen.properties +++ b/src/cartridges/app_adyen_SFRA/cartridge/templates/resources/adyen.properties @@ -16,3 +16,5 @@ myAccount.SaveCard=Please add a new payment instrument through the checkout terminal.selectTerminal=Please select your terminal terminal.noTerminals=There are no terminals connected adyen.paymentFailed=Payment failed, please try again. +button.submit.checkout.review=Place Order +title.checkout.review=Checkout Review diff --git a/src/cartridges/bm_adyen/cartridge/static/default/css/configurationSettings.css b/src/cartridges/bm_adyen/cartridge/static/default/css/configurationSettings.css index bad57ac55..6cf3d0a85 100644 --- a/src/cartridges/bm_adyen/cartridge/static/default/css/configurationSettings.css +++ b/src/cartridges/bm_adyen/cartridge/static/default/css/configurationSettings.css @@ -65,7 +65,6 @@ padding-right: 51px; } - .form-check-input, .form-check-label{ width: 26px; height: 20px; @@ -240,7 +239,7 @@ html { #saveChangesAlert{ width: 420px; left: 0; - margin: auto; // Centers alert component + margin: auto; /* Centers alert component */ right: 0; text-align: center; top: 1em; @@ -276,8 +275,8 @@ html { #notSavedChangesAlert{ left: 0; - margin: auto; // Centers alert component - position: absolute; // So that it will display relative to the entire page + margin: auto; /* Centers alert component */ + position: absolute; /* So that it will display relative to the entire page */ right: 0; text-align: center; top: 1em; @@ -460,7 +459,8 @@ ul{ .draggable { cursor: move; display: flex; - align-items: center; + flex-direction: column; + align-items: flex-start; /* Align items to the start */ justify-content: space-between; padding: 15px; flex: 1; @@ -468,8 +468,7 @@ ul{ } .draggable .switch-button { - position: initial; - left: 95.5%; + left: 89%; } .draggable .title { @@ -513,3 +512,18 @@ ul{ font-style: normal; font-size: 14px; } + +.additional-item-container { + display: flex; + align-items: center; + margin-top: 10px; +} + +.additional-item-container .additional-switch-button{ + position: absolute; + left: 89%; +} + +.additional-item{ + margin-left: 48px; +} diff --git a/src/cartridges/bm_adyen/cartridge/static/default/icons/paypal.svg b/src/cartridges/bm_adyen/cartridge/static/default/icons/paypal.svg new file mode 100644 index 000000000..1b9444423 --- /dev/null +++ b/src/cartridges/bm_adyen/cartridge/static/default/icons/paypal.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/cartridges/bm_adyen/cartridge/static/default/js/adyenSettings.js b/src/cartridges/bm_adyen/cartridge/static/default/js/adyenSettings.js index 7cc758c4f..33d2c244f 100644 --- a/src/cartridges/bm_adyen/cartridge/static/default/js/adyenSettings.js +++ b/src/cartridges/bm_adyen/cartridge/static/default/js/adyenSettings.js @@ -13,6 +13,18 @@ const expressPaymentMethods = [ icon: window.amazonPayIcon, checked: window.isAmazonPayEnabled, }, + { + id: 'paypal', + name: 'PayPalExpress_Enabled', + text: 'PayPal', + icon: window.paypalIcon, + checked: window.isPayPalExpressEnabled, + reviewPage: window.isPayPalExpressReviewPageEnabled, + additionalField: { + name: 'PayPalExpress_ReviewPage_Enabled', + text: 'Show shopper order review page', + }, + }, ]; document.addEventListener('DOMContentLoaded', () => { @@ -152,6 +164,25 @@ document.addEventListener('DOMContentLoaded', () => { const listItem = document.createElement('li'); listItem.setAttribute('data-index', index.toString()); + let additionalFieldHtml = ''; + if (item.additionalField) { + additionalFieldHtml = ` +
+

${item.additionalField.text}

+
+
+ +
+
+
+ `; + } + listItem.innerHTML = `
@@ -174,6 +205,7 @@ document.addEventListener('DOMContentLoaded', () => { >
+ ${additionalFieldHtml} `; diff --git a/src/cartridges/bm_adyen/cartridge/templates/default/adyenSettings/settingCards/epmSettings.isml b/src/cartridges/bm_adyen/cartridge/templates/default/adyenSettings/settingCards/epmSettings.isml index b6d28d69e..5e0a81c31 100644 --- a/src/cartridges/bm_adyen/cartridge/templates/default/adyenSettings/settingCards/epmSettings.isml +++ b/src/cartridges/bm_adyen/cartridge/templates/default/adyenSettings/settingCards/epmSettings.isml @@ -4,9 +4,12 @@ window.dragIcon = "${URLUtils.staticURL('icons/drag.svg')}"; window.applePayIcon = "${URLUtils.staticURL('icons/applepay.svg')}"; window.amazonPayIcon = "${URLUtils.staticURL('icons/amazonpay.svg')}"; + window.paypalIcon = "${URLUtils.staticURL('icons/paypal.svg')}"; window.isApplePayEnabled = ${AdyenConfigs.isApplePayExpressEnabled() || false}; window.isAmazonPayEnabled = ${AdyenConfigs.isAmazonPayExpressEnabled() || false}; + window.isPayPalExpressEnabled = ${AdyenConfigs.isPayPalExpressEnabled() || false}; + window.isPayPalExpressReviewPageEnabled = ${AdyenConfigs.isPayPalExpressReviewPageEnabled() || false}; window.expressMethodsOrder = "${AdyenConfigs.getExpressPaymentsOrder()}";
diff --git a/src/cartridges/int_adyen_SFRA/cartridge/adyen/config/constants.js b/src/cartridges/int_adyen_SFRA/cartridge/adyen/config/constants.js index bbb8c8750..c0f418e29 100644 --- a/src/cartridges/int_adyen_SFRA/cartridge/adyen/config/constants.js +++ b/src/cartridges/int_adyen_SFRA/cartridge/adyen/config/constants.js @@ -32,6 +32,7 @@ module.exports = { PAYMENTMETHODS: { APPLEPAY: 'applepay', AMAZONPAY: 'amazonpay', + PAYPAL: 'paypal', }, CAN_SKIP_SUMMARY_PAGE: ['applepay', 'cashapp'], @@ -47,6 +48,7 @@ module.exports = { CHECKBALANCE: 'AdyenCheckBalance', CANCELPARTIALPAYMENTORDER: 'AdyenCancelPartialPaymentOrder', PARTIALPAYMENTSORDER: 'AdyenPartialPaymentsOrder', + PAYPALUPDATEORDER: 'AdyenPaypalUpdateOrder', }, CONTRACT: { ONECLICK: 'ONECLICK', diff --git a/src/cartridges/int_adyen_SFRA/cartridge/adyen/logs/adyenCustomLogs.js b/src/cartridges/int_adyen_SFRA/cartridge/adyen/logs/adyenCustomLogs.js index cbae1a2d4..96dc8de43 100644 --- a/src/cartridges/int_adyen_SFRA/cartridge/adyen/logs/adyenCustomLogs.js +++ b/src/cartridges/int_adyen_SFRA/cartridge/adyen/logs/adyenCustomLogs.js @@ -1,19 +1,21 @@ const Logger = require('dw/system/Logger'); -function fatal_log(msg) { - return Logger.getLogger('Adyen_fatal', 'Adyen').fatal(msg); +function fatal_log(msg, error) { + const logMsg = [msg, error?.toString(), error?.stack].join('\n').trim(); + Logger.getLogger('Adyen_fatal', 'Adyen').fatal(logMsg); } -function error_log(msg) { - return Logger.getLogger('Adyen_error', 'Adyen').error(msg); +function error_log(msg, error) { + const logMsg = [msg, error?.toString(), error?.stack].join('\n').trim(); + Logger.getLogger('Adyen_error', 'Adyen').error(logMsg); } function debug_log(msg) { - return Logger.getLogger('Adyen_debug', 'Adyen').debug(msg); + Logger.getLogger('Adyen_debug', 'Adyen').debug(msg); } function info_log(msg) { - return Logger.getLogger('Adyen_info', 'Adyen').info(msg); + Logger.getLogger('Adyen_info', 'Adyen').info(msg); } module.exports = { diff --git a/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/__tests__/selectShippingMethods.test.js b/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/__tests__/selectShippingMethods.test.js index 9feb161ac..885126b55 100644 --- a/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/__tests__/selectShippingMethods.test.js +++ b/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/__tests__/selectShippingMethods.test.js @@ -13,10 +13,7 @@ describe('callSelectShippingMethod', () => { jest.clearAllMocks(); req = { - querystring: { - }, - form: { - }, + body: JSON.stringify({}) }; res = { @@ -26,6 +23,10 @@ describe('callSelectShippingMethod', () => { next = jest.fn(); }); + afterEach(() => { + jest.clearAllMocks(); + }) + it('should handle the case when there is no current basket', () => { currentBasket = BasketMgr.getCurrentBasket.mockReturnValueOnce(null); callSelectShippingMethod(req, res, next); @@ -37,13 +38,13 @@ describe('callSelectShippingMethod', () => { }); it('should handle the case when there is an error selecting the shipping method', () => { - req.querystring.shipmentUUID = 'mocked_uuid'; + req.body = JSON.stringify({shipmentUUID: 'mocked_uuid'}); currentBasket = { defaultShipment: {}, }; BasketMgr.getCurrentBasket.mockReturnValueOnce(currentBasket.defaultShipment); shippingHelper.getShipmentByUUID.mockReturnValueOnce(currentBasket.defaultShipment); - + callSelectShippingMethod(req, res, next); expect(res.setStatusCode).toHaveBeenCalledWith(500); @@ -75,8 +76,8 @@ describe('callSelectShippingMethod', () => { someMethod: jest.fn(), }; CartModel.mockReturnValueOnce(basketModelInstance); - - callSelectShippingMethod(req, res, next); + + callSelectShippingMethod(req, res, next); expect(res.json).toHaveBeenCalledWith({ ...basketModelInstance, diff --git a/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/__tests__/shippingMethods.test.js b/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/__tests__/shippingMethods.test.js index 44fbf2982..dc170df23 100644 --- a/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/__tests__/shippingMethods.test.js +++ b/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/__tests__/shippingMethods.test.js @@ -13,22 +13,19 @@ beforeEach(() => { jest.clearAllMocks(); req = { - querystring: { + body: JSON.stringify({address:{ city: 'Amsterdam', countryCode: 'NL', stateCode: 'AMS', postalCode: '1001', shipmentUUID: 'mocked_uuid', - }, - locale: { id: 'nl_NL' }, - form: { - methodID: 'mocked_methodID', - }, + }}), }; res = { redirect: jest.fn(), json: jest.fn(), + setStatusCode: jest.fn(), }; }); @@ -39,6 +36,25 @@ afterEach(() => { describe('Shipping methods', () => { it('Should return available shipping methods', () => { const Logger = require('../../../../../../../../jest/__mocks__/dw/system/Logger'); + currentBasket = { + getDefaultShipment: jest.fn(() => { + return { + shippingAddress: { + setCity: jest.fn(), + setPostalCode: jest.fn(), + setStateCode: jest.fn(), + setCountryCode: jest.fn(), + }} + }), + getTotalGrossPrice: jest.fn(() => { + return { + currencyCode: 'EUR', + value: '1000' + } + }), + updateTotals: jest.fn(), + }; + BasketMgr.getCurrentBasket.mockReturnValueOnce(currentBasket); callGetShippingMethods(req, res, next); expect(AdyenHelper.getApplicableShippingMethods).toHaveBeenCalledTimes(1); expect(res.json).toHaveBeenCalledWith({ @@ -53,8 +69,14 @@ describe('Shipping methods', () => { new Logger.error('error'), ); callGetShippingMethods(req, res, next); - expect(res.json).not.toHaveBeenCalled(); + expect(res.setStatusCode).toHaveBeenCalledWith(500); + expect(res.json).toHaveBeenCalledWith({ + errorMessage: 'mocked_error.cannot.find.shipping.methods', + }); + expect(next).toHaveBeenCalled(); }); +}); + it('Should update shipping address for the basket', () => { const Logger = require('../../../../../../../../jest/__mocks__/dw/system/Logger'); const setCityMock = jest.fn() @@ -79,4 +101,4 @@ describe('Shipping methods', () => { expect(setCountryCodeMock).toHaveBeenCalledWith('NL'); expect(Logger.error.mock.calls.length).toBe(0); }); -}); \ No newline at end of file + diff --git a/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/paypal/__tests__/handleCheckoutReview.test.js b/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/paypal/__tests__/handleCheckoutReview.test.js new file mode 100644 index 000000000..e5e39982a --- /dev/null +++ b/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/paypal/__tests__/handleCheckoutReview.test.js @@ -0,0 +1,91 @@ +/* eslint-disable global-require */ +const BasketMgr = require('dw/order/BasketMgr'); +const URLUtils = require('dw/web/URLUtils'); +const validationHelpers = require('*/cartridge/scripts/helpers/basketValidationHelpers'); +const AdyenLogs = require('*/cartridge/adyen/logs/adyenCustomLogs'); + +let res; +let req; +const next = jest.fn(); + +const handleCheckoutReview = require('../handleCheckoutReview'); + +beforeEach(() => { + jest.clearAllMocks(); + currentBasket = { + getPaymentInstruments: jest.fn(() => ([{ + custom: { adyenPaymentMethod: '' } + }])), + }; + + req = { + form: { + data: JSON.stringify({details: 'test_paymentsDetails'}), + }, + currentCustomer: { + raw: '' + }, + locale: { + id: 'NL' + }, + session: { + privacyCache: { + get: jest.fn() + } + } + }; + + res = { + redirect: jest.fn(), + render: jest.fn(), + setStatusCode: jest.fn(), + }; + AdyenLogs.error_log = jest.fn(); + URLUtils.url =jest.fn(); + BasketMgr.getCurrentBasket.mockReturnValueOnce(currentBasket); +}); + +afterEach(() => { + jest.resetModules(); +}); + +describe('Checkout Review controller', () => { + it('Should return Checkout Review page', () => { + handleCheckoutReview(req, res, next); + expect(res.render.mock.calls[0][0]).toBe('cart/checkoutReview'); + expect(res.render.mock.calls[0][1]).toMatchObject( { + data: '{"details":"test_paymentsDetails"}', + showConfirmationUrl: expect.anything(), + order: expect.anything(), + customer: expect.anything(), + } + ) + expect(AdyenLogs.error_log).not.toHaveBeenCalled(); + expect(next).toHaveBeenCalled(); + }); + + it('Should fail returning Checkout Review page when no state data is submitted', () => { + req.form = ''; + handleCheckoutReview(req, res, next); + expect(AdyenLogs.error_log).toHaveBeenCalledTimes(1); + expect(res.redirect).toHaveBeenCalledTimes(1) + expect(URLUtils.url).toHaveBeenCalledWith("Error-ErrorCode", "err", "general"); + expect(next).toHaveBeenCalled(); + }); + it('Should redirect to Cart if there is no current Basket', () => { + BasketMgr.getCurrentBasket = jest.fn().mockImplementationOnce(() => ('')) + handleCheckoutReview(req, res, next); + expect(res.redirect).toHaveBeenCalledTimes(1) + expect(URLUtils.url).toHaveBeenCalledWith("Cart-Show"); + expect(AdyenLogs.error_log).not.toHaveBeenCalled(); + expect(next).toHaveBeenCalled(); + }); + it('Should redirect to Cart if product validation fails', () => { + validationHelpers.validateProducts = jest.fn(() => ({error: true})) + handleCheckoutReview(req, res, next); + expect(res.redirect).toHaveBeenCalledTimes(1) + expect(URLUtils.url).toHaveBeenCalledWith("Cart-Show"); + expect(AdyenLogs.error_log).not.toHaveBeenCalled(); + expect(next).toHaveBeenCalled(); + }); +}); diff --git a/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/paypal/__tests__/makeExpressPaymentDetailsCall.test.js b/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/paypal/__tests__/makeExpressPaymentDetailsCall.test.js new file mode 100644 index 000000000..75dd0cf47 --- /dev/null +++ b/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/paypal/__tests__/makeExpressPaymentDetailsCall.test.js @@ -0,0 +1,57 @@ +/* eslint-disable global-require */ +const URLUtils = require('dw/web/URLUtils'); +const COHelpers = require('*/cartridge/scripts/checkout/checkoutHelpers'); +const AdyenLogs = require('*/cartridge/adyen/logs/adyenCustomLogs'); +const adyenCheckout = require('*/cartridge/adyen/scripts/payments/adyenCheckout'); + +let res; +let req; +const next = jest.fn(); + +const makeExpressPaymentDetailsCall = require('../makeExpressPaymentDetailsCall'); + +beforeEach(() => { + jest.clearAllMocks(); + req = { + body: JSON.stringify({data: {}}) + }; + + res = { + redirect: jest.fn(), + json: jest.fn(), + setStatusCode: jest.fn(), + }; + AdyenLogs.error_log = jest.fn(); + AdyenLogs.fatal_log = jest.fn(); + URLUtils.url = jest.fn(); +}); + +afterEach(() => { + jest.resetModules(); +}); + +describe('Express Payment Details controller', () => { + it('Should return response when payment details call is successful', () => { + makeExpressPaymentDetailsCall(req, res, next); + expect(res.json).toHaveBeenCalledWith({"orderNo": "mocked_orderNo", "orderToken": "mocked_orderToken"}); + expect(AdyenLogs.error_log).not.toHaveBeenCalled(); + expect(next).toHaveBeenCalled(); + }); + + it('Should return error response when payment details call is not successful', () => { + adyenCheckout.doPaymentsDetailsCall = jest.fn().mockImplementationOnce(() => {throw new Error('unexpected mock error')}); + makeExpressPaymentDetailsCall(req, res, next); + expect(AdyenLogs.error_log).toHaveBeenCalledTimes(1); + expect(res.redirect).toHaveBeenCalledTimes(1); + expect(URLUtils.url).toHaveBeenCalledWith('Error-ErrorCode', 'err', 'general'); + expect(next).toHaveBeenCalled(); + }); + it('Should return error response when place Order is not successful', () => { + COHelpers.placeOrder = jest.fn(() => ({error: true})) + makeExpressPaymentDetailsCall(req, res, next); + expect(AdyenLogs.error_log).toHaveBeenCalledTimes(1); + expect(res.redirect).toHaveBeenCalledTimes(1); + expect(URLUtils.url).toHaveBeenCalledWith('Error-ErrorCode', 'err', 'general'); + expect(next).toHaveBeenCalled(); + }); +}); diff --git a/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/paypal/__tests__/makeExpressPaymentsCall.test.js b/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/paypal/__tests__/makeExpressPaymentsCall.test.js new file mode 100644 index 000000000..d202459e8 --- /dev/null +++ b/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/paypal/__tests__/makeExpressPaymentsCall.test.js @@ -0,0 +1,46 @@ +/* eslint-disable global-require */ +const AdyenLogs = require('*/cartridge/adyen/logs/adyenCustomLogs'); +const adyenCheckout = require('*/cartridge/adyen/scripts/payments/adyenCheckout'); + +let res; +let req; +const next = jest.fn(); + +const makeExpressPaymentsCall = require('../makeExpressPaymentsCall'); + +beforeEach(() => { + jest.clearAllMocks(); + req = { + body: JSON.stringify({}) + }; + + res = { + redirect: jest.fn(), + json: jest.fn(), + setStatusCode: jest.fn(), + }; + AdyenLogs.error_log = jest.fn(); + AdyenLogs.fatal_log = jest.fn(); +}); + +afterEach(() => { + jest.resetModules(); +}); + +describe('Express Payments controller', () => { + it('Should return response when payments call is successful', () => { + makeExpressPaymentsCall(req, res, next); + expect(res.json).toHaveBeenCalledWith({"pspReference": "mocked_pspReference"}); + expect(AdyenLogs.error_log).not.toHaveBeenCalled(); + expect(next).toHaveBeenCalled(); + }); + + it('Should return error response when payments call is not successful', () => { + adyenCheckout.doPaymentsCall = jest.fn(() => {throw new Error('unexpected mock error')}); + makeExpressPaymentsCall(req, res, next); + expect(AdyenLogs.fatal_log).toHaveBeenCalledTimes(1); + expect(res.json).toHaveBeenCalledWith({"errorMessage": "mocked_error.express.paypal.payments"}) + expect(res.setStatusCode).toHaveBeenCalledWith(500); + expect(next).toHaveBeenCalled(); + }); +}); diff --git a/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/paypal/__tests__/saveShopperData.test.js b/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/paypal/__tests__/saveShopperData.test.js new file mode 100644 index 000000000..1d9588311 --- /dev/null +++ b/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/paypal/__tests__/saveShopperData.test.js @@ -0,0 +1,46 @@ +/* eslint-disable global-require */ +const URLUtils = require('dw/web/URLUtils'); +const AdyenLogs = require('*/cartridge/adyen/logs/adyenCustomLogs'); + +let res; +let req; +const next = jest.fn(); + +const saveShopperData = require('../saveShopperData'); + +beforeEach(() => { + jest.clearAllMocks(); + req = { + form: {shopperDetails: JSON.stringify({})} + }; + + res = { + redirect: jest.fn(), + json: jest.fn(), + setStatusCode: jest.fn(), + }; + AdyenLogs.error_log = jest.fn(); + URLUtils.url = jest.fn(); +}); + +afterEach(() => { + jest.resetModules(); +}); + +describe('Save Shopper controller', () => { + it('Should return response when Save Shopper call is successful', () => { + saveShopperData(req, res, next); + expect(res.json).toHaveBeenCalledWith({"success": true}); + expect(AdyenLogs.error_log).not.toHaveBeenCalled(); + expect(next).toHaveBeenCalled(); + }); + + it('Should return response when Save Shopper call is not successful', () => { + res.json = jest.fn(() => {throw new Error('unexpected mock error')}); + saveShopperData(req, res, next); + expect(AdyenLogs.error_log).toHaveBeenCalledTimes(1); + expect(res.redirect).toHaveBeenCalledTimes(1); + expect(URLUtils.url).toHaveBeenCalledWith('Error-ErrorCode', 'err', 'general'); + expect(next).toHaveBeenCalled(); + }); +}); diff --git a/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/paypal/handleCheckoutReview.js b/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/paypal/handleCheckoutReview.js new file mode 100644 index 000000000..12b55162b --- /dev/null +++ b/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/paypal/handleCheckoutReview.js @@ -0,0 +1,90 @@ +const URLUtils = require('dw/web/URLUtils'); +const BasketMgr = require('dw/order/BasketMgr'); +const Locale = require('dw/util/Locale'); +const Transaction = require('dw/system/Transaction'); +const AccountModel = require('*/cartridge/models/account'); +const OrderModel = require('*/cartridge/models/order'); +const validationHelpers = require('*/cartridge/scripts/helpers/basketValidationHelpers'); +const AdyenLogs = require('*/cartridge/adyen/logs/adyenCustomLogs'); +const AdyenHelper = require('*/cartridge/adyen/utils/adyenHelper'); +const paypalHelper = require('*/cartridge/adyen/utils/paypalHelper'); + +/** + * Sets Shipping and Billing address for the basket, + * also updated payment method on the paymentInstrument of Basket. + * @param {dw.order.Basket} currentBasket - the current basket + * @param {sfra.Request} req - request object + * @returns {undefined} + */ +function updateCurrentBasket(currentBasket, req) { + const { details } = JSON.parse(req.form.data); + if (currentBasket.shipments?.length <= 1) { + req.session.privacyCache.set('usingMultiShipping', false); + } + + paypalHelper.setBillingAndShippingAddress(currentBasket); + + const paymentInstrument = currentBasket.getPaymentInstruments()[0]; + Transaction.wrap(() => { + paymentInstrument.custom.adyenPaymentMethod = + AdyenHelper.getAdyenComponentType(details?.paymentSource); + }); +} + +/** + * Controller for the checkout review page for express payment methods + * @param {sfra.Request} req - request + * @param {sfra.Response} res - response + * @param {sfra.Next} next - next + * @returns {sfra.Next} next - next + */ +function handleCheckoutReview(req, res, next) { + try { + if (!req.form.data) { + throw new Error('State data not present in the request'); + } + const currentBasket = BasketMgr.getCurrentBasket(); + if (!currentBasket) { + res.redirect(URLUtils.url('Cart-Show')); + return next(); + } + + const validatedProducts = validationHelpers.validateProducts(currentBasket); + if (validatedProducts.error) { + res.redirect(URLUtils.url('Cart-Show')); + return next(); + } + + updateCurrentBasket(currentBasket, req); + + const currentCustomer = req.currentCustomer.raw; + const currentLocale = Locale.getLocale(req.locale.id); + const usingMultiShipping = + req.session.privacyCache.get('usingMultiShipping'); + + const orderModel = new OrderModel(currentBasket, { + customer: currentCustomer, + usingMultiShipping, + shippable: true, + countryCode: currentLocale.country, + containerView: 'basket', + }); + + const accountModel = new AccountModel(req.currentCustomer); + + res.render('cart/checkoutReview', { + data: req.form.data, + showConfirmationUrl: URLUtils.https( + 'Adyen-ShowConfirmationPaymentFromComponent', + ), + order: orderModel, + customer: accountModel, + }); + } catch (error) { + AdyenLogs.error_log('Could not render checkout review page', error); + res.redirect(URLUtils.url('Error-ErrorCode', 'err', 'general')); + } + return next(); +} + +module.exports = handleCheckoutReview; diff --git a/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/paypal/makeExpressPaymentDetailsCall.js b/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/paypal/makeExpressPaymentDetailsCall.js new file mode 100644 index 000000000..1c77030eb --- /dev/null +++ b/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/paypal/makeExpressPaymentDetailsCall.js @@ -0,0 +1,69 @@ +const URLUtils = require('dw/web/URLUtils'); +const OrderMgr = require('dw/order/OrderMgr'); +const Transaction = require('dw/system/Transaction'); +const BasketMgr = require('dw/order/BasketMgr'); +const adyenCheckout = require('*/cartridge/adyen/scripts/payments/adyenCheckout'); +const AdyenLogs = require('*/cartridge/adyen/logs/adyenCustomLogs'); +const COHelpers = require('*/cartridge/scripts/checkout/checkoutHelpers'); +const AdyenHelper = require('*/cartridge/adyen/utils/adyenHelper'); +const paypalHelper = require('*/cartridge/adyen/utils/paypalHelper'); +const constants = require('*/cartridge/adyen/config/constants'); + +function setPaymentInstrumentFields(paymentInstrument, response) { + paymentInstrument.custom.adyenPaymentMethod = + AdyenHelper.getAdyenComponentType(response.paymentMethod.type); + paymentInstrument.custom[`${constants.OMS_NAMESPACE}__Adyen_Payment_Method`] = + AdyenHelper.getAdyenComponentType(response.paymentMethod.type); + paymentInstrument.custom.Adyen_Payment_Method_Variant = + response.paymentMethod.type.toLowerCase(); + paymentInstrument.custom[ + `${constants.OMS_NAMESPACE}__Adyen_Payment_Method_Variant` + ] = response.paymentMethod.type.toLowerCase(); +} + +/* + * Makes a payment details call to Adyen to confirm the current status of a payment. + It is currently used only for PayPal Express Flow + */ +function makeExpressPaymentDetailsCall(req, res, next) { + try { + const request = JSON.parse(req.body); + const currentBasket = BasketMgr.getCurrentBasket(); + + const response = adyenCheckout.doPaymentsDetailsCall(request.data); + + paypalHelper.setBillingAndShippingAddress(currentBasket); + + // Setting the session variable to null after assigning the shopper data to basket level + session.privacy.shopperDetails = null; + + const order = OrderMgr.createOrder( + currentBasket, + session.privacy.paypalExpressOrderNo, + ); + const fraudDetectionStatus = { status: 'success' }; + const placeOrderResult = COHelpers.placeOrder(order, fraudDetectionStatus); + if (placeOrderResult.error) { + throw new Error('Failed to place the PayPal express order'); + } + + response.orderNo = order.orderNo; + response.orderToken = order.orderToken; + const paymentInstrument = order.getPaymentInstruments( + AdyenHelper.getOrderMainPaymentInstrumentType(order), + )[0]; + // Storing the paypal express response to make use of show confirmation logic + Transaction.wrap(() => { + order.custom.Adyen_paypalExpressResponse = JSON.stringify(response); + setPaymentInstrumentFields(paymentInstrument, response); + }); + res.json({ orderNo: response.orderNo, orderToken: response.orderToken }); + return next(); + } catch (error) { + AdyenLogs.error_log('Could not verify express /payment/details:', error); + res.redirect(URLUtils.url('Error-ErrorCode', 'err', 'general')); + return next(); + } +} + +module.exports = makeExpressPaymentDetailsCall; diff --git a/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/paypal/makeExpressPaymentsCall.js b/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/paypal/makeExpressPaymentsCall.js new file mode 100644 index 000000000..8065c7996 --- /dev/null +++ b/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/paypal/makeExpressPaymentsCall.js @@ -0,0 +1,66 @@ +const BasketMgr = require('dw/order/BasketMgr'); +const PaymentMgr = require('dw/order/PaymentMgr'); +const OrderMgr = require('dw/order/OrderMgr'); +const Transaction = require('dw/system/Transaction'); +const Resource = require('dw/web/Resource'); +const adyenCheckout = require('*/cartridge/adyen/scripts/payments/adyenCheckout'); +const constants = require('*/cartridge/adyen/config/constants'); +const AdyenLogs = require('*/cartridge/adyen/logs/adyenCustomLogs'); +const AdyenHelper = require('*/cartridge/adyen/utils/adyenHelper'); +const paypalHelper = require('*/cartridge/adyen/utils/paypalHelper'); + +function makeExpressPaymentsCall(req, res, next) { + try { + const currentBasket = BasketMgr.getCurrentBasket(); + let paymentInstrument; + Transaction.wrap(() => { + currentBasket.removeAllPaymentInstruments(); + paymentInstrument = currentBasket.createPaymentInstrument( + constants.METHOD_ADYEN_COMPONENT, + currentBasket.getAdjustedMerchandizeTotalGrossPrice(), + ); + const { paymentProcessor } = PaymentMgr.getPaymentMethod( + paymentInstrument.paymentMethod, + ); + paymentInstrument.paymentTransaction.paymentProcessor = paymentProcessor; + paymentInstrument.custom.adyenPaymentData = req.body; + }); + // creates order number to be utilized for PayPal express + const paypalExpressOrderNo = OrderMgr.createOrderNo(); + // Create request object with payment details + const paymentRequest = AdyenHelper.createAdyenRequestObject( + paypalExpressOrderNo, + null, + paymentInstrument, + ); + paymentRequest.amount = { + currency: paymentInstrument.paymentTransaction.amount.currencyCode, + value: AdyenHelper.getCurrencyValueForApi( + paymentInstrument.paymentTransaction.amount, + ).getValueOrNull(), + }; + paymentRequest.lineItems = paypalHelper.getLineItems({ + Basket: currentBasket, + }); + let result; + Transaction.wrap(() => { + result = adyenCheckout.doPaymentsCall( + null, + paymentInstrument, + paymentRequest, + ); + }); + session.privacy.paypalExpressOrderNo = paypalExpressOrderNo; + session.privacy.pspReference = result.pspReference; + res.json(result); + } catch (error) { + AdyenLogs.fatal_log('Paypal express payments request failed', error); + res.setStatusCode(500); + res.json({ + errorMessage: Resource.msg('error.express.paypal.payments', 'cart', null), + }); + } + return next(); +} + +module.exports = makeExpressPaymentsCall; diff --git a/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/paypal/saveShopperData.js b/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/paypal/saveShopperData.js new file mode 100644 index 000000000..f2d3571d4 --- /dev/null +++ b/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/paypal/saveShopperData.js @@ -0,0 +1,17 @@ +const URLUtils = require('dw/web/URLUtils'); +const AdyenLogs = require('*/cartridge/adyen/logs/adyenCustomLogs'); + +function saveShopperData(req, res, next) { + try { + const shopperDetails = JSON.parse(req.form.shopperDetails); + session.privacy.shopperDetails = JSON.stringify(shopperDetails); + res.json({ success: true }); + return next(); + } catch (error) { + AdyenLogs.error_log('Failed to save the shopper details:', error); + res.redirect(URLUtils.url('Error-ErrorCode', 'err', 'general')); + return next(); + } +} + +module.exports = saveShopperData; diff --git a/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/saveExpressShopperDetails.js b/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/saveExpressShopperDetails.js index c53b0a9c2..123e99957 100644 --- a/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/saveExpressShopperDetails.js +++ b/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/saveExpressShopperDetails.js @@ -64,8 +64,10 @@ function saveExpressShopperDetails(req, res, next) { JSON.stringify(shopperDetails); }); setBillingAndShippingAddress(currentBasket); - const shippingMethods = AdyenHelper.callGetShippingMethods( - shopperDetails.shippingAddress, + const { shippingAddress } = currentBasket.getDefaultShipment(); + const shippingMethods = AdyenHelper.getApplicableShippingMethods( + currentBasket.getDefaultShipment(), + shippingAddress, ); res.json({ shippingMethods }); return next(); diff --git a/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/selectShippingMethods.js b/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/selectShippingMethods.js index ba5d3cfe9..2c197c57a 100644 --- a/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/selectShippingMethods.js +++ b/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/selectShippingMethods.js @@ -5,6 +5,11 @@ const URLUtils = require('dw/web/URLUtils'); const CartModel = require('*/cartridge/models/cart'); const shippingHelper = require('*/cartridge/scripts/checkout/shippingHelpers'); const basketCalculationHelpers = require('*/cartridge/scripts/helpers/basketCalculationHelpers'); +const { PAYMENTMETHODS } = require('*/cartridge/adyen/config/constants'); +const adyenCheckout = require('*/cartridge/adyen/scripts/payments/adyenCheckout'); +const AdyenHelper = require('*/cartridge/adyen/utils/adyenHelper'); +const AdyenLogs = require('*/cartridge/adyen/logs/adyenCustomLogs'); +const paypalHelper = require('*/cartridge/adyen/utils/paypalHelper'); /** * Make a request to Adyen to select shipping methods @@ -21,37 +26,56 @@ function callSelectShippingMethod(req, res, next) { return next(); } + try { + const { shipmentUUID, methodID, currentPaymentData, paymentMethodType } = + JSON.parse(req.body); + let shipment; + if (shipmentUUID) { + shipment = shippingHelper.getShipmentByUUID(currentBasket, shipmentUUID); + } else { + shipment = currentBasket.defaultShipment; + } - let error = false; - - const shipUUID = req.querystring.shipmentUUID || req.form.shipmentUUID; - const methodID = req.querystring.methodID || req.form.methodID; - let shipment; - if (shipUUID) { - shipment = shippingHelper.getShipmentByUUID(currentBasket, shipUUID); - } else { - shipment = currentBasket.defaultShipment; - } + Transaction.wrap(() => { + shippingHelper.selectShippingMethod(shipment, methodID); - Transaction.wrap(() => { - shippingHelper.selectShippingMethod(shipment, methodID); + if (currentBasket && !shipment.shippingMethod) { + throw new Error( + `cannot set shippingMethod: ${methodID} for shipment:${shipment?.UUID}`, + ); + } - if (currentBasket && !shipment.shippingMethod) { - error = true; - return; + basketCalculationHelpers.calculateTotals(currentBasket); + }); + let response = {}; + if (paymentMethodType === PAYMENTMETHODS.PAYPAL) { + const currentShippingMethodsModels = + AdyenHelper.getApplicableShippingMethods(shipment); + if (!currentShippingMethodsModels?.length) { + throw new Error('No applicable shipping methods found'); + } + const paypalUpdateOrderResponse = adyenCheckout.doPaypalUpdateOrderCall( + paypalHelper.createPaypalUpdateOrderRequest( + session.privacy.pspReference, + currentBasket, + currentShippingMethodsModels, + currentPaymentData, + ), + ); + AdyenLogs.info_log( + `Paypal Order Update Call: ${paypalUpdateOrderResponse.status}`, + ); + response = { ...response, ...paypalUpdateOrderResponse }; } - - basketCalculationHelpers.calculateTotals(currentBasket); - }); - - if (!error) { const basketModel = new CartModel(currentBasket); const grandTotalAmount = { value: currentBasket.getTotalGrossPrice().value, currency: currentBasket.getTotalGrossPrice().currencyCode, }; - res.json({ ...basketModel, grandTotalAmount }); - } else { + response = { ...response, ...basketModel, grandTotalAmount }; + res.json(response); + } catch (error) { + AdyenLogs.error_log('Failed to set shipping method', error); res.setStatusCode(500); res.json({ errorMessage: Resource.msg( diff --git a/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/shippingMethods.js b/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/shippingMethods.js index b9e95ad76..186928bbf 100644 --- a/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/shippingMethods.js +++ b/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/expressPayments/shippingMethods.js @@ -1,65 +1,84 @@ const BasketMgr = require('dw/order/BasketMgr'); const Transaction = require('dw/system/Transaction'); +const Resource = require('dw/web/Resource'); +const URLUtils = require('dw/web/URLUtils'); const AdyenLogs = require('*/cartridge/adyen/logs/adyenCustomLogs'); const AdyenHelper = require('*/cartridge/adyen/utils/adyenHelper'); +const { PAYMENTMETHODS } = require('*/cartridge/adyen/config/constants'); +const adyenCheckout = require('*/cartridge/adyen/scripts/payments/adyenCheckout'); +const paypalHelper = require('*/cartridge/adyen/utils/paypalHelper'); -const addressMapping = { - city: 'setCity', - countryCode: 'setCountryCode', - stateCode: 'setStateCode', - postalCode: 'setPostalCode', -}; - -/** - * Sets address properties for express PM - * @param {dw.order.shippingAddress} shippingAddress - shippingAddress for the default shipment - * @param {object} inputAddress - address coming from the input field based on shopper selection - * @param {object} mapping - address mapping between property and setter for that property - */ -function setAddressProperties(shippingAddress, inputAddress, mapping) { - Object.keys(inputAddress).forEach((key) => { - if (inputAddress[key] && mapping[key]) { - shippingAddress[mapping[key]](inputAddress[key]); - } - }); -} - -/** - * Make a request to Adyen to get shipping methods - */ -function callGetShippingMethods(req, res, next) { - try { - let address = null; - if (req.querystring) { - address = { - city: req.querystring.city, - countryCode: req.querystring.countryCode, - stateCode: req.querystring.stateCode, - postalCode: req.querystring.postalCode, - }; - } - const currentBasket = BasketMgr.getCurrentBasket(); - const shipment = currentBasket.getDefaultShipment(); +function updateShippingAddress(currentBasket, address) { + if (address) { + let { shippingAddress } = currentBasket.getDefaultShipment(); Transaction.wrap(() => { - let { shippingAddress } = shipment; if (!shippingAddress) { shippingAddress = currentBasket .getDefaultShipment() .createShippingAddress(); } - if (address) { - setAddressProperties(shippingAddress, address, addressMapping); - } + shippingAddress.setCity(address?.city); + shippingAddress.setPostalCode(address?.postalCode); + shippingAddress.setStateCode(address?.stateCode); + shippingAddress.setCountryCode(address?.countryCode); }); + } +} +/** + * Make a request to Adyen to get shipping methods + */ +function callGetShippingMethods(req, res, next) { + try { + const { address, currentPaymentData, paymentMethodType } = JSON.parse( + req.body, + ); + const currentBasket = BasketMgr.getCurrentBasket(); + if (!currentBasket) { + res.json({ + error: true, + redirectUrl: URLUtils.url('Cart-Show').toString(), + }); + + return next(); + } + updateShippingAddress(currentBasket, address); + currentBasket.updateTotals(); const currentShippingMethodsModels = - AdyenHelper.getApplicableShippingMethods(shipment, address); - res.json({ - shippingMethods: currentShippingMethodsModels, - }); + AdyenHelper.getApplicableShippingMethods( + currentBasket.getDefaultShipment(), + address, + ); + if (!currentShippingMethodsModels?.length) { + throw new Error('No applicable shipping methods found'); + } + let response = {}; + if (paymentMethodType === PAYMENTMETHODS.PAYPAL) { + const paypalUpdateOrderResponse = adyenCheckout.doPaypalUpdateOrderCall( + paypalHelper.createPaypalUpdateOrderRequest( + session.privacy.pspReference, + currentBasket, + currentShippingMethodsModels, + currentPaymentData, + ), + ); + AdyenLogs.info_log( + `Paypal Order Update Call: ${paypalUpdateOrderResponse.status}`, + ); + response = { ...response, ...paypalUpdateOrderResponse }; + } + response.shippingMethods = currentShippingMethodsModels; + res.json(response); return next(); } catch (error) { - AdyenLogs.error_log('Failed to fetch shipping methods'); - AdyenLogs.error_log(error); + AdyenLogs.error_log('Failed to fetch shipping methods', error); + res.setStatusCode(500); + res.json({ + errorMessage: Resource.msg( + 'error.cannot.find.shipping.methods', + 'cart', + null, + ), + }); return next(); } } diff --git a/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/index.js b/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/index.js index b34ff4b77..be9abcdc4 100644 --- a/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/index.js +++ b/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/index.js @@ -13,6 +13,10 @@ const fetchGiftCards = require('*/cartridge/adyen/scripts/partialPayments/fetchG const showConfirmationPaymentFromComponent = require('*/cartridge/adyen/scripts/showConfirmation/showConfirmationPaymentFromComponent'); const showConfirmation = require('*/cartridge/adyen/scripts/showConfirmation/showConfirmation'); const notify = require('*/cartridge/adyen/webhooks/notify'); +const makeExpressPaymentsCall = require('*/cartridge/adyen/scripts/expressPayments/paypal/makeExpressPaymentsCall'); +const makeExpressPaymentDetailsCall = require('*/cartridge/adyen/scripts/expressPayments/paypal/makeExpressPaymentDetailsCall'); +const saveShopperData = require('*/cartridge/adyen/scripts/expressPayments/paypal/saveShopperData'); +const handleCheckoutReview = require('*/cartridge/adyen/scripts/expressPayments/paypal/handleCheckoutReview'); module.exports = { getCheckoutPaymentMethods, @@ -30,4 +34,8 @@ module.exports = { showConfirmation, showConfirmationPaymentFromComponent, notify, + makeExpressPaymentsCall, + makeExpressPaymentDetailsCall, + saveShopperData, + handleCheckoutReview, }; diff --git a/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/payments/__tests__/adyenCheckout.test.js b/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/payments/__tests__/adyenCheckout.test.js index 044d1b2b9..e879d11a4 100644 --- a/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/payments/__tests__/adyenCheckout.test.js +++ b/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/payments/__tests__/adyenCheckout.test.js @@ -8,12 +8,14 @@ describe('AdyenCheckout', () => { custom: {}, setPaymentStatus: jest.fn(), setExportStatus: jest.fn(), + getOrderNo: jest.fn(), + getOrderToken: jest.fn(), }, PaymentInstrument: { custom: { adyenPaymentData: "{}", adyenPartialPaymentsOrder: - '{"orderData":"b4c0!BQABAgBzO7ZwfyxJ9ifN0NIgUsuwBdUWb==...",' + + '{"orderData":"b4c0!BQABAgBzO7ZwfyxJ9ifN0NIgUsuwBdUWb==...",' + '"remainingAmount":{"currency":"EUR","value":20799},' + '"amount":{"currency":"EUR","value":1000}}' @@ -39,12 +41,14 @@ describe('AdyenCheckout', () => { custom: {}, setPaymentStatus: jest.fn(), setExportStatus: jest.fn(), + getOrderNo: jest.fn(), + getOrderToken: jest.fn(), }, PaymentInstrument: { custom: { adyenPaymentData: "{}", adyenPartialPaymentsOrder: - '{"orderData":"b4c0!BQABAgBzO7ZwfyxJ9ifN0NIgUsuwBdUWb==...",' + + '{"orderData":"b4c0!BQABAgBzO7ZwfyxJ9ifN0NIgUsuwBdUWb==...",' + '"remainingAmount":{"currency":"EUR","value":20799},' + '"amount":{"currency":"EUR","value":25799}}' @@ -70,12 +74,14 @@ describe('AdyenCheckout', () => { custom: {}, setPaymentStatus: jest.fn(), setExportStatus: jest.fn(), + getOrderNo: jest.fn(), + getOrderToken: jest.fn(), }, PaymentInstrument: { custom: { adyenPaymentData: "{}", adyenPartialPaymentsOrder: - '{"orderData":"b4c0!BQABAgBzO7ZwfyxJ9ifN0NIgUsuwBdUWb==...",' + + '{"orderData":"b4c0!BQABAgBzO7ZwfyxJ9ifN0NIgUsuwBdUWb==...",' + '"remainingAmount":{"currency":"USD","value":20799},' + '"amount":{"currency":"USD","value":1000}}' @@ -93,4 +99,4 @@ describe('AdyenCheckout', () => { expect(Logger.error.mock.calls[0][0]).toContain("Cart has been edited after applying a gift card"); expect(response.error).toEqual(true); }) -}) \ No newline at end of file +}) diff --git a/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/payments/__tests__/getCheckoutPaymentMethods.test.js b/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/payments/__tests__/getCheckoutPaymentMethods.test.js index 84172cd88..0f234d3d5 100644 --- a/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/payments/__tests__/getCheckoutPaymentMethods.test.js +++ b/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/payments/__tests__/getCheckoutPaymentMethods.test.js @@ -1,4 +1,5 @@ /* eslint-disable global-require */ +const BasketMgr = require('dw/order/BasketMgr'); const getCheckoutPaymentMethods = require('*/cartridge/adyen/scripts/payments/getCheckoutPaymentMethods'); const getPaymentMethods = require('*/cartridge/adyen/scripts/payments/adyenGetPaymentMethods'); let req; @@ -24,6 +25,25 @@ afterEach(() => { describe('getCheckoutPaymentMethods', () => { it('returns AdyenPaymentMethods', () => { + currentBasket = { + getDefaultShipment: jest.fn(() => { + return { + shippingAddress: { + getCountryCode: jest.fn(() => { + return { + value: "NL" + } + }) + }} + }), + getTotalGrossPrice: jest.fn(() => { + return { + currencyCode: 'EUR', + value: '1000' + } + }) + }; + BasketMgr.getCurrentBasket.mockReturnValueOnce(currentBasket); getCheckoutPaymentMethods(req, res, next); expect(res.json).toHaveBeenCalledWith({ AdyenPaymentMethods: { @@ -62,7 +82,7 @@ describe('getCheckoutPaymentMethods', () => { getCheckoutPaymentMethods(req, res, next); expect(res.json).toHaveBeenCalledWith({ error: true, - }); + }); expect(Logger.fatal.mock.calls.length).toBe(1); expect(next).toHaveBeenCalled(); }); diff --git a/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/payments/__tests__/paymentsDetails.test.js b/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/payments/__tests__/paymentsDetails.test.js index b0b0323b5..60d335c87 100644 --- a/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/payments/__tests__/paymentsDetails.test.js +++ b/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/payments/__tests__/paymentsDetails.test.js @@ -62,21 +62,18 @@ describe('Confirm paymentsDetails', () => { it('should call paymentDetails request and response handler', () => { const adyenCheckout = require('*/cartridge/adyen/scripts/payments/adyenCheckout'); const AdyenHelper = require('*/cartridge/adyen/utils/adyenHelper'); - const URLUtils = require('dw/web/URLUtils'); - adyenCheckout.doPaymentsDetailsCall.mockImplementation(() => ({ resultCode:'mocked_resultCode', pspReference: 'mocked_pspReference', })); paymentsDetails(req, res, jest.fn()); - expect(URLUtils.url.mock.calls[0][0]).toEqual('Adyen-ShowConfirmation'); + expect(AdyenHelper.createRedirectUrl.mock.calls.length).toEqual(1); expect(adyenCheckout.doPaymentsDetailsCall.mock.calls.length).toEqual(1); expect(AdyenHelper.createAdyenCheckoutResponse.mock.calls.length).toEqual(1); - expect(res.json.mock.calls[0][0]).toEqual({ isFinal: true, isSuccessful: false, - redirectUrl: "[\"Adyen-ShowConfirmation\",\"merchantReference\",null,\"signature\",\"mocked_signature\",\"orderToken\",null]" + redirectUrl: "mocked_RedirectUrl" }); }); }); diff --git a/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/payments/__tests__/paypalHelper.test.js b/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/payments/__tests__/paypalHelper.test.js deleted file mode 100644 index 98c2e0bcc..000000000 --- a/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/payments/__tests__/paypalHelper.test.js +++ /dev/null @@ -1,50 +0,0 @@ -const paypalHelper = require('../paypalHelper') -describe('paypalHelper', () => { - let args,lineItem, result - beforeEach(() => { - args = (item) => ({ - Order: { - getAllLineItems: jest.fn(() => ([item])) - } - }) - - lineItem = { - productName: 'test', - productID: '123', - quantityValue: '1', - getAdjustedTax: '1000', - adjustedNetPrice: '10000', - category: 'PHYSICAL_GOODS', - } - - result = { - quantity: '1', - description: 'test', - itemCategory: 'PHYSICAL_GOODS', - sku: '123', - amountExcludingTax: '10000', - taxAmount: '1000' - } - }) - it('should return lineItems for paypal', () => { -const paypalLineItems = paypalHelper.getLineItems(args(lineItem)) - expect(paypalLineItems[0]).toStrictEqual(result) - }) - - it('should return lineItems for paypal with default itemCategory when category is not as per paypal', () => { - const paypalLineItems = paypalHelper.getLineItems(args({...lineItem, category: 'TEST_GOODS'})) - expect(paypalLineItems[0]).toStrictEqual({ - quantity: '1', - description: 'test', - sku: '123', - amountExcludingTax: '10000', - taxAmount: '1000' - }) - }) - - it('should return no lineItems for paypal if order or basket is not defined', () => { - - const paypalLineItems = paypalHelper.getLineItems({}) - expect(paypalLineItems).toBeNull() - }) -}) \ No newline at end of file diff --git a/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/payments/adyenCheckout.js b/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/payments/adyenCheckout.js index d9f9e36f8..ac2763f83 100644 --- a/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/payments/adyenCheckout.js +++ b/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/payments/adyenCheckout.js @@ -26,7 +26,6 @@ const Resource = require('dw/web/Resource'); const Order = require('dw/order/Order'); const StringUtils = require('dw/util/StringUtils'); - /* Script Modules */ const AdyenHelper = require('*/cartridge/adyen/utils/adyenHelper'); const AdyenConfigs = require('*/cartridge/adyen/utils/adyenConfigs'); @@ -35,7 +34,7 @@ const AdyenGetOpenInvoiceData = require('*/cartridge/adyen/scripts/payments/adye const adyenLevelTwoThreeData = require('*/cartridge/adyen/scripts/payments/adyenLevelTwoThreeData'); const constants = require('*/cartridge/adyen/config/constants'); const AdyenLogs = require('*/cartridge/adyen/logs/adyenCustomLogs'); -const paypalHelper = require('*/cartridge/adyen/scripts/payments/paypalHelper'); +const paypalHelper = require('*/cartridge/adyen/utils/paypalHelper'); // eslint-disable-next-line complexity function doPaymentsCall(order, paymentInstrument, paymentRequest) { @@ -144,13 +143,13 @@ function createPaymentRequest(args) { // Create request object with payment details let paymentRequest = AdyenHelper.createAdyenRequestObject( - order, + order.getOrderNo(), + order.getOrderToken(), paymentInstrument, ); - paymentRequest = AdyenHelper.add3DS2Data(paymentRequest); const paymentMethodType = paymentRequest.paymentMethod.type; - + paymentRequest = AdyenHelper.add3DS2Data(paymentRequest); // Add Risk data if (AdyenConfigs.getAdyenBasketFieldsEnabled()) { paymentRequest.additionalData = @@ -180,7 +179,6 @@ function createPaymentRequest(args) { paymentRequest.installments = { value: numOfInstallments }; } } - const value = AdyenHelper.getCurrencyValueForApi( paymentInstrument.paymentTransaction.amount, ).getValueOrNull(); @@ -213,6 +211,7 @@ function createPaymentRequest(args) { paymentMethodType, paymentRequest, ); + // Create shopper data fields paymentRequest = AdyenHelper.createShopperObject({ order, @@ -268,7 +267,6 @@ function createPaymentRequest(args) { paymentInstrument, paymentRequest.paymentMethod, ); - // make API call return doPaymentsCall(order, paymentInstrument, paymentRequest); } catch (e) { AdyenLogs.error_log( @@ -313,6 +311,13 @@ function doCreatePartialPaymentOrderCall(partialPaymentRequest) { ); } +function doPaypalUpdateOrderCall(paypalUpdateOrderRequest) { + return AdyenHelper.executeCall( + constants.SERVICE.PAYPALUPDATEORDER, + paypalUpdateOrderRequest, + ); +} + module.exports = { createPaymentRequest, doPaymentsCall, @@ -320,4 +325,5 @@ module.exports = { doCheckBalanceCall, doCancelPartialPaymentOrderCall, doCreatePartialPaymentOrderCall, + doPaypalUpdateOrderCall, }; diff --git a/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/payments/adyenZeroAuth.js b/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/payments/adyenZeroAuth.js index 86dcee5ec..e1d101361 100644 --- a/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/payments/adyenZeroAuth.js +++ b/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/payments/adyenZeroAuth.js @@ -31,7 +31,8 @@ const constants = require('*/cartridge/adyen/config/constants'); function zeroAuthPayment(customer, paymentInstrument) { try { let zeroAuthRequest = AdyenHelper.createAdyenRequestObject( - null, + 'recurringPayment-account', + 'recurringPayment-token', paymentInstrument, ); diff --git a/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/payments/getCheckoutPaymentMethods.js b/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/payments/getCheckoutPaymentMethods.js index 958df8a5a..e218b8f73 100644 --- a/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/payments/getCheckoutPaymentMethods.js +++ b/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/payments/getCheckoutPaymentMethods.js @@ -1,6 +1,7 @@ const BasketMgr = require('dw/order/BasketMgr'); const Locale = require('dw/util/Locale'); const PaymentMgr = require('dw/order/PaymentMgr'); +const URLUtils = require('dw/web/URLUtils'); const AdyenHelper = require('*/cartridge/adyen/utils/adyenHelper'); const adyenTerminalApi = require('*/cartridge/adyen/scripts/payments/adyenTerminalApi'); const paymentMethodDescriptions = require('*/cartridge/adyen/config/paymentMethodDescriptions'); @@ -9,12 +10,12 @@ const getPaymentMethods = require('*/cartridge/adyen/scripts/payments/adyenGetPa const AdyenLogs = require('*/cartridge/adyen/logs/adyenCustomLogs'); function getCountryCode(currentBasket, locale) { - const countryCode = Locale.getLocale(locale.id).country; - const firstItem = currentBasket?.getShipments()?.[0]; - if (firstItem?.shippingAddress) { - return firstItem.shippingAddress.getCountryCode().value; + let countryCode; + const { shippingAddress } = currentBasket.getDefaultShipment(); + if (shippingAddress) { + countryCode = shippingAddress.getCountryCode().value; } - return countryCode; + return countryCode || Locale.getLocale(locale.id).country; } function getConnectedTerminals() { @@ -27,11 +28,15 @@ function getConnectedTerminals() { function getCheckoutPaymentMethods(req, res, next) { try { const currentBasket = BasketMgr.getCurrentBasket(); - const countryCode = - currentBasket.getShipments().length > 0 && - currentBasket.getShipments()[0].shippingAddress - ? currentBasket.getShipments()[0].shippingAddress.getCountryCode().value - : getCountryCode(currentBasket, req.locale).value; + if (!currentBasket) { + res.json({ + error: true, + redirectUrl: URLUtils.url('Cart-Show').toString(), + }); + + return next(); + } + const countryCode = getCountryCode(currentBasket, req.locale); const adyenURL = `${AdyenHelper.getLoadingContext()}images/logos/medium/`; const connectedTerminals = JSON.parse(getConnectedTerminals()); const currency = currentBasket.getTotalGrossPrice().currencyCode; diff --git a/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/payments/paymentsDetails.js b/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/payments/paymentsDetails.js index 80077c651..b0052d14a 100644 --- a/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/payments/paymentsDetails.js +++ b/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/payments/paymentsDetails.js @@ -5,7 +5,7 @@ const AdyenHelper = require('*/cartridge/adyen/utils/adyenHelper'); const adyenCheckout = require('*/cartridge/adyen/scripts/payments/adyenCheckout'); const AdyenLogs = require('*/cartridge/adyen/logs/adyenCustomLogs'); -function getSignature(paymentsDetailsResponse, orderToken) { +function getRedirectUrl(paymentsDetailsResponse, orderToken) { const order = OrderMgr.getOrder( paymentsDetailsResponse.merchantReference, orderToken, @@ -14,18 +14,16 @@ function getSignature(paymentsDetailsResponse, orderToken) { const paymentInstruments = order.getPaymentInstruments( AdyenHelper.getOrderMainPaymentInstrumentType(order), ); - - const signature = AdyenHelper.createSignature( + const redirectUrl = AdyenHelper.createRedirectUrl( paymentInstruments[0], - order.getUUID(), paymentsDetailsResponse.merchantReference, + orderToken, ); - Transaction.wrap(() => { paymentInstruments[0].paymentTransaction.custom.Adyen_authResult = JSON.stringify(paymentsDetailsResponse); }); - return signature; + return redirectUrl; } return undefined; } @@ -49,10 +47,11 @@ function paymentsDetails(req, res, next) { const response = AdyenHelper.createAdyenCheckoutResponse( paymentsDetailsResponse, ); - // Create signature to verify returnUrl - const signature = getSignature(paymentsDetailsResponse, request.orderToken); - + const redirectUrl = getRedirectUrl( + paymentsDetailsResponse, + request.orderToken, + ); if (isAmazonpay) { response.fullResponse = { pspReference: paymentsDetailsResponse.pspReference, @@ -60,16 +59,8 @@ function paymentsDetails(req, res, next) { resultCode: paymentsDetailsResponse.resultCode, }; } - if (signature !== null) { - response.redirectUrl = URLUtils.https( - 'Adyen-ShowConfirmation', - 'merchantReference', - response.merchantReference, - 'signature', - signature, - 'orderToken', - request.orderToken, - ).toString(); + if (redirectUrl) { + response.redirectUrl = redirectUrl; } res.json(response); diff --git a/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/payments/paypalHelper.js b/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/payments/paypalHelper.js deleted file mode 100644 index 4fef811e2..000000000 --- a/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/payments/paypalHelper.js +++ /dev/null @@ -1,53 +0,0 @@ -/** - * ###### - * ###### - * ############ ####( ###### #####. ###### ############ ############ - * ############# #####( ###### #####. ###### ############# ############# - * ###### #####( ###### #####. ###### ##### ###### ##### ###### - * ###### ###### #####( ###### #####. ###### ##### ##### ##### ###### - * ###### ###### #####( ###### #####. ###### ##### ##### ###### - * ############# ############# ############# ############# ##### ###### - * ############ ############ ############# ############ ##### ###### - * ###### - * ############# - * ############ - * Adyen Salesforce Commerce Cloud - * Copyright (c) 2021 Adyen B.V. - * This file is open source and available under the MIT license. - * See the LICENSE file for more info. - * - * Add all product and shipping line items to request - */ - -const LineItemHelper = require('*/cartridge/adyen/utils/lineItemHelper'); - -const PAYPAL_ITEM_CATEGORY = ['PHYSICAL_GOODS', 'DIGITAL_GOODS', 'DONATION']; -function getLineItems({ Order: order, Basket: basket }) { - if (!(order || basket)) return null; - const orderOrBasket = order || basket; - const allLineItems = LineItemHelper.getAllLineItems( - orderOrBasket.getAllLineItems(), - ); - return allLineItems.map((lineItem) => { - const lineItemObject = {}; - const description = LineItemHelper.getDescription(lineItem); - const id = LineItemHelper.getId(lineItem); - const quantity = LineItemHelper.getQuantity(lineItem); - const itemAmount = LineItemHelper.getItemAmount(lineItem).divide(quantity); - const vatAmount = LineItemHelper.getVatAmount(lineItem).divide(quantity); - // eslint-disable-next-line - if (lineItem.hasOwnProperty('category')) { - if (PAYPAL_ITEM_CATEGORY.indexOf(lineItem.category) > -1) { - lineItemObject.itemCategory = lineItem.category; - } - } - lineItemObject.quantity = quantity; - lineItemObject.description = description; - lineItemObject.sku = id; - lineItemObject.amountExcludingTax = itemAmount.getValue().toFixed(); - lineItemObject.taxAmount = vatAmount.getValue().toFixed(); - return lineItemObject; - }); -} - -module.exports.getLineItems = getLineItems; diff --git a/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/showConfirmation/handlePaymentFromComponent.js b/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/showConfirmation/handlePaymentFromComponent.js index 30785110a..51bd060ab 100644 --- a/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/showConfirmation/handlePaymentFromComponent.js +++ b/src/cartridges/int_adyen_SFRA/cartridge/adyen/scripts/showConfirmation/handlePaymentFromComponent.js @@ -49,11 +49,14 @@ function handleAuthorisedPayment( ) { // custom fraudDetection const fraudDetectionStatus = { status: 'success' }; + const isPayPalExpress = order.custom.Adyen_paypalExpressResponse; - // Places the order - const placeOrderResult = COHelpers.placeOrder(order, fraudDetectionStatus); - if (placeOrderResult.error) { - return handlePaymentError(order, adyenPaymentInstrument, { res, next }); + // Places the order, for PayPal express the order is placed from makeExpressPaymentDetailsCall.js + if (!isPayPalExpress) { + const placeOrderResult = COHelpers.placeOrder(order, fraudDetectionStatus); + if (placeOrderResult.error) { + return handlePaymentError(order, adyenPaymentInstrument, { res, next }); + } } Transaction.wrap(() => { @@ -101,6 +104,10 @@ function handlePaymentResult(result, order, adyenPaymentInstrument, options) { Transaction.wrap(() => { order.custom.Adyen_pspReference = result.pspReference; order.custom.Adyen_eventCode = result.resultCode; + order.custom.Adyen_paypalExpressResponse = null; + adyenPaymentInstrument.custom.adyenPaymentData = null; + session.privacy.paypalExpressOrderNo = null; + session.privacy.pspReference = null; }); return handlePaymentError(order, adyenPaymentInstrument, options); } @@ -135,15 +142,21 @@ function handlePayment(stateData, order, options) { return handlePaymentError(order, adyenPaymentInstrument, options); } } - - const detailsCall = hasStateData - ? handlePaymentsDetailsCall(stateData, adyenPaymentInstrument) - : null; - - Transaction.wrap(() => { - adyenPaymentInstrument.custom.adyenPaymentData = null; - }); - finalResult = finalResult || detailsCall?.result; + const paymentData = JSON.parse( + adyenPaymentInstrument.custom.adyenPaymentData, + ); + const isPayPalExpress = AdyenHelper.isPayPalExpress( + paymentData.paymentMethod, + ); + const detailsCall = + hasStateData && !isPayPalExpress + ? handlePaymentsDetailsCall(stateData, adyenPaymentInstrument) + : null; + if (isPayPalExpress) { + finalResult = JSON.parse(order.custom.Adyen_paypalExpressResponse); + } else { + finalResult = finalResult || detailsCall?.result; + } return handlePaymentResult( finalResult, diff --git a/src/cartridges/int_adyen_SFRA/cartridge/adyen/utils/__tests__/adyenHelper.test.js b/src/cartridges/int_adyen_SFRA/cartridge/adyen/utils/__tests__/adyenHelper.test.js index e178dd8c6..4ecc6f9ce 100644 --- a/src/cartridges/int_adyen_SFRA/cartridge/adyen/utils/__tests__/adyenHelper.test.js +++ b/src/cartridges/int_adyen_SFRA/cartridge/adyen/utils/__tests__/adyenHelper.test.js @@ -1,4 +1,6 @@ /* eslint-disable global-require */ +const Money = require('../../../../../../../jest/__mocks__/dw/value/Money'); +const { getApplicableShippingMethods } = require('../adyenHelper'); const savePaymentDetails = require('../adyenHelper').savePaymentDetails; describe('savePaymentDetails', () => { let paymentInstrument; @@ -67,3 +69,50 @@ describe('savePaymentDetails', () => { expect(paymentInstrument.paymentTransaction.custom.Adyen_donationToken).toBe('donation-token-123'); }); }); + +describe('getApplicableShippingMethods', () => { + let shippingMethod, shipment, address; + beforeEach(() => { + shippingMethod = { + description: 'Order received within 7-10 business days', + displayName: 'Ground', + ID: '001', + custom: { + estimatedArrivalTime: '7-10 Business Days' + }, + getTaxClassID: jest.fn(), + }; + shipment = { + UUID: 'mock_UUID', + shippingAddress: { + setCity: jest.fn(), + setPostalCode: jest.fn(), + setStateCode: jest.fn(), + setCountryCode: jest.fn(), + }, + getProductLineItems: jest.fn(() => ({ + toArray: jest.fn(() =>[{ + getProduct: jest.fn(() => ({ + getPriceModel: jest.fn(() => ({ + getPrice: jest.fn(() => Money()) + })) + })), + getQuantity: jest.fn() + }]) + })) + }; + address = {} + }); + it('should return applicable shipping methods for shipment and address', () => { + const shippingMethods = getApplicableShippingMethods(shipment, address); + expect(shippingMethods).toStrictEqual([{"shipmentUUID": "mock_UUID", "shippingCost": {"currencyCode": "USD", "value": "10.99"}}, {"shipmentUUID": "mock_UUID", "shippingCost": {"currencyCode": "USD", "value": "10.99"}}]); + }) + it('should return applicable shipping methods when address is not provided', () => { + const shippingMethods = getApplicableShippingMethods(shipment); + expect(shippingMethods).toStrictEqual([{"shipmentUUID": "mock_UUID", "shippingCost": {"currencyCode": "USD", "value": "10.99"}}, {"shipmentUUID": "mock_UUID", "shippingCost": {"currencyCode": "USD", "value": "10.99"}}]); + }) + it('should return no shipping methods when shipment is not provided', () => { + const shippingMethods = getApplicableShippingMethods(); + expect(shippingMethods).toBeNull(); + }) +}) diff --git a/src/cartridges/int_adyen_SFRA/cartridge/adyen/utils/__tests__/paypalHelper.test.js b/src/cartridges/int_adyen_SFRA/cartridge/adyen/utils/__tests__/paypalHelper.test.js new file mode 100644 index 000000000..e64d03a1f --- /dev/null +++ b/src/cartridges/int_adyen_SFRA/cartridge/adyen/utils/__tests__/paypalHelper.test.js @@ -0,0 +1,186 @@ +/* eslint-disable global-require */ +const BasketMgr = require('dw/order/BasketMgr'); +jest.mock('dw/value/Money', () => jest.fn()); +jest.mock('*/cartridge/adyen/utils/adyenHelper', () => { + return { + getCurrencyValueForApi: jest.fn(() => { + return { + value: 1000 + } + }) + } +}) + +const paypalHelper = require('../paypalHelper') +const Money = require('dw/value/Money'); +const { createBillingAddress } = require("../../../../../../../jest/__mocks__/dw/order/BasketMgr"); +describe('paypalHelper', () => { + describe('getLineItems', () => { + let args,lineItem, result + beforeEach(() => { + jest.clearAllMocks(); + args = (item) => ({ + Order: { + getAllLineItems: jest.fn(() => ([item])) + } + }) + + lineItem = { + productName: 'test', + productID: '123', + quantityValue: '1', + getAdjustedTax: '1000', + adjustedNetPrice: '10000', + category: 'PHYSICAL_GOODS', + } + + result = { + quantity: '1', + description: 'test', + itemCategory: 'PHYSICAL_GOODS', + sku: '123', + amountExcludingTax: '10000', + taxAmount: '1000' + } + }) + + afterEach(() => { + jest.resetModules(); + }); + + it('should return lineItems for paypal', () => { + const paypalLineItems = paypalHelper.getLineItems(args(lineItem)) + expect(paypalLineItems[0]).toStrictEqual(result) + }) + + it('should return lineItems for paypal with default itemCategory when category is not as per paypal', () => { + const paypalLineItems = paypalHelper.getLineItems(args({...lineItem, category: 'TEST_GOODS'})) + expect(paypalLineItems[0]).toStrictEqual({ + quantity: '1', + description: 'test', + sku: '123', + amountExcludingTax: '10000', + taxAmount: '1000' + }) + }) + + it('should return no lineItems for paypal if order or basket is not defined', () => { + + const paypalLineItems = paypalHelper.getLineItems({}) + expect(paypalLineItems).toBeNull() + }) + }) + describe('createPaypalUpdateOrderRequest', () => { + let pspReference, currentBasket, currentShippingMethods, paymentData, result; + beforeEach(() => { + jest.clearAllMocks(); + pspReference = 'test'; + paymentData = 'test'; + currentShippingMethods = [{ + ID: '001', + displayName: 'test', + shippingCost: { + currencyCode: 'USD', + value: '10.00', + }, + selected: true, + }] + currentBasket = { + currencyCode: 'USD', + getAdjustedShippingTotalGrossPrice: jest.fn(), + getAdjustedMerchandizeTotalGrossPrice: jest.fn(), + } + + result = { + pspReference: 'test', + paymentData: 'test', + amount: { + currency: 'USD', + value: 2000 + }, + deliveryMethods:[{ + reference: '001', + description: 'test', + type: 'Shipping', + amount: { + currency: 'USD', + value: 1000, + }, + selected: true, + }], + } + + }) + + it('should return UpdateOrderRequest object for paypal', () => { + Money.mockReturnValue(() => {return {value: 10, currency: 'TEST'}}) + const paypalUpdateOrderRequest = paypalHelper.createPaypalUpdateOrderRequest(pspReference, currentBasket, currentShippingMethods, paymentData) + expect(paypalUpdateOrderRequest).toStrictEqual(result) + }) + }) + describe('setBillingAndShippingAddress', () => { + let shopperDetails, billingAddress, shippingAddress; + beforeEach(() => { + jest.clearAllMocks(); + billingAddress = require('../../../../../../../jest/__mocks__/dw/order/BasketMgr'); + shopperDetails = { + shopperName:{ + firstName: 'John', + lastName: 'Doe' + }, + billingAddress:{ + street: '123 Main St', + city: 'City', + postalCode: '12345', + stateOrProvince: 'State', + country: 'United States', + }, + shippingAddress:{ + street: '123 Main St', + city: 'City', + postalCode: '12345', + stateOrProvince: 'State', + country: 'United States', + }, + telephoneNumber: '+1234567890', + shopperEmail: 'john@example.com' + } + session.privacy.shopperDetails = JSON.stringify(shopperDetails); + }); + afterEach(() => { + jest.resetModules(); + }); + it('should update billing and shipping address for current basket', () => { + const currentBasket = BasketMgr.getCurrentBasket(); + paypalHelper.setBillingAndShippingAddress(currentBasket); + expect(currentBasket.billingAddress.setFirstName).toHaveBeenCalledWith('John'); + }) + it('should set billing and shipping address if current basket has no billing Address', () => { + const currentBasket = BasketMgr.getCurrentBasket(); + currentBasket.billingAddress= ''; + paypalHelper.setBillingAndShippingAddress(currentBasket); + expect(currentBasket.createBillingAddress).toHaveBeenCalled(); + }) + it('should set billing and shipping address if current basket has no shipping Address', () => { + const currentBasket = BasketMgr.getCurrentBasket(); + const createShippingAddress = jest.fn(() => ({ + setPostalCode: jest.fn(), + setAddress1: jest.fn(), + setAddress2: jest.fn(), + setCountryCode: jest.fn(), + setCity: jest.fn(), + setFirstName: jest.fn(), + setLastName: jest.fn(), + setPhone: jest.fn(), + setStateCode: jest.fn(), + })); + currentBasket.getDefaultShipment= jest.fn(() => ({ + createShippingAddress: createShippingAddress + })); + paypalHelper.setBillingAndShippingAddress(currentBasket); + expect(currentBasket.getDefaultShipment).toHaveBeenCalled(); + expect(createShippingAddress).toHaveBeenCalled(); + }) + }) +}) + diff --git a/src/cartridges/int_adyen_SFRA/cartridge/adyen/utils/adyenConfigs.js b/src/cartridges/int_adyen_SFRA/cartridge/adyen/utils/adyenConfigs.js index 2d42940c5..574935192 100644 --- a/src/cartridges/int_adyen_SFRA/cartridge/adyen/utils/adyenConfigs.js +++ b/src/cartridges/int_adyen_SFRA/cartridge/adyen/utils/adyenConfigs.js @@ -147,6 +147,14 @@ const adyenConfigsObj = { return getCustomPreference('AmazonPayExpress_Enabled'); }, + isPayPalExpressEnabled() { + return getCustomPreference('PayPalExpress_Enabled'); + }, + + isPayPalExpressReviewPageEnabled() { + return getCustomPreference('PayPalExpress_ReviewPage_Enabled'); + }, + getExpressPaymentsOrder() { return getCustomPreference('ExpressPayments_order'); }, diff --git a/src/cartridges/int_adyen_SFRA/cartridge/adyen/utils/adyenHelper.js b/src/cartridges/int_adyen_SFRA/cartridge/adyen/utils/adyenHelper.js index 147af825b..844b7fc23 100644 --- a/src/cartridges/int_adyen_SFRA/cartridge/adyen/utils/adyenHelper.js +++ b/src/cartridges/int_adyen_SFRA/cartridge/adyen/utils/adyenHelper.js @@ -23,18 +23,20 @@ const Bytes = require('dw/util/Bytes'); const MessageDigest = require('dw/crypto/MessageDigest'); const Encoding = require('dw/crypto/Encoding'); const CustomerMgr = require('dw/customer/CustomerMgr'); -const constants = require('*/cartridge/adyen/config/constants'); -const AdyenConfigs = require('*/cartridge/adyen/utils/adyenConfigs'); const Transaction = require('dw/system/Transaction'); const UUIDUtils = require('dw/util/UUIDUtils'); -const collections = require('*/cartridge/scripts/util/collections'); const ShippingMgr = require('dw/order/ShippingMgr'); -const ShippingMethodModel = require('*/cartridge/models/shipping/shippingMethod'); const PaymentInstrument = require('dw/order/PaymentInstrument'); const StringUtils = require('dw/util/StringUtils'); +const Money = require('dw/value/Money'); +const TaxMgr = require('dw/order/TaxMgr'); +const ShippingLocation = require('dw/order/ShippingLocation'); //script includes +const ShippingMethodModel = require('*/cartridge/models/shipping/shippingMethod'); +const collections = require('*/cartridge/scripts/util/collections'); +const constants = require('*/cartridge/adyen/config/constants'); +const AdyenConfigs = require('*/cartridge/adyen/utils/adyenConfigs'); const AdyenLogs = require('*/cartridge/adyen/logs/adyenCustomLogs'); -const BasketMgr = require('dw/order/BasketMgr'); /* eslint no-var: off */ let adyenHelperObj = { @@ -77,15 +79,50 @@ let adyenHelperObj = { return null; }, + /** + * Returns shippingCost including taxes for a specific Shipment / ShippingMethod pair including the product level shipping cost if any + * @param {dw.order.ShippingMethod} shippingMethod - the default shipment of the current basket + * @param {dw.order.Shipment} shipment - a shipment of the current basket + * @returns {{currencyCode: String, value: String}} - Shipping Cost including taxes + */ getShippingCost(shippingMethod, shipment) { + const { shippingAddress } = shipment const shipmentShippingModel = ShippingMgr.getShipmentShippingModel(shipment); - const shippingCost = shipmentShippingModel.getShippingCost(shippingMethod); + let shippingCost = shipmentShippingModel.getShippingCost(shippingMethod).getAmount(); + collections.forEach(shipment.getProductLineItems(), (lineItem) => { + const product = lineItem.getProduct(); + const productQuantity = lineItem.getQuantity(); + const productShippingModel = ShippingMgr.getProductShippingModel(product); + let productShippingCost = productShippingModel.getShippingCost(shippingMethod) + ? productShippingModel.getShippingCost(shippingMethod).getAmount().multiply(productQuantity) + : new Money(0, product.getPriceModel().getPrice().getCurrencyCode()); + shippingCost = shippingCost.add(productShippingCost); + }) + shippingCost = shippingAddress ? shippingCost.addRate(adyenHelperObj.getShippingTaxRate(shippingMethod, shippingAddress)) : shippingCost; return { - value: shippingCost.amount.value, - currencyCode: shippingCost.amount.currencyCode, + value: shippingCost.getValue(), + currencyCode: shippingCost.getCurrencyCode(), }; }, + /** + * Returns tax rate for specific Shipment / ShippingMethod pair. + * @param {dw.order.ShippingMethod} shippingMethod - the default shipment of the current basket + * @param {dw.order.shippingAddress} shippingAddress - shippingAddress for the default shipment + * @returns {Number} - tax rate in decimals.(eg.: 0.02 for 2%) + */ + getShippingTaxRate(shippingMethod, shippingAddress) { + const taxClassID = shippingMethod.getTaxClassID(); + const taxJurisdictionID = TaxMgr.getTaxJurisdictionID(new ShippingLocation(shippingAddress)); + return TaxMgr.getTaxRate(taxClassID, taxJurisdictionID); + }, + + /** + * Returns applicable shipping methods for specific Shipment / ShippingAddress pair. + * @param {dw.order.OrderAddress} address - the shipping address of the default shipment of the current basket + * @param {dw.order.Shipment} shipment - a shipment of the current basket + * @returns {dw.util.ArrayList | null} - list of applicable shipping methods or null + */ getShippingMethods(shipment, address) { if (!shipment) return null; @@ -103,13 +140,35 @@ let adyenHelperObj = { return shippingMethods; }, + /** + * Returns shipment UUID for the shipment. + * @param {dw.order.Shipment} shipment - a shipment of the current basket + * @returns {String | null} - shipment UUID or null + */ getShipmentUUID(shipment) { if (!shipment) return null; return shipment.UUID; }, + /** + * @typedef {object} ApplicableShippingMethodModel + * @property {string|null} ID + * @property {string|null} displayName + * @property {string|null} estimatedArrivalTime + * @property {boolean|null} default + * @property {boolean|null} [selected] + * @property {{currencyCode: String, value: String}} shippingCost + * @property {string|null} shipmentUUID + */ + + /** + * Returns applicable shipping methods(excluding store pickup methods) for specific Shipment / ShippingAddress pair. + * @param {dw.order.OrderAddress} address - the shipping address of the default shipment of the current basket + * @param {dw.order.Shipment} shipment - a shipment of the current basket + * @returns {dw.util.ArrayList | null} - list of applicable shipping methods or null + */ getApplicableShippingMethods(shipment, address) { - const shippingMethods = this.getShippingMethods(shipment, address); + const shippingMethods = adyenHelperObj.getShippingMethods(shipment, address); if (!shippingMethods) { return null; } @@ -122,8 +181,8 @@ let adyenHelperObj = { shippingMethod, shipment, ); - const shippingCost = this.getShippingCost(shippingMethod, shipment); - const shipmentUUID = this.getShipmentUUID(shipment); + const shippingCost = adyenHelperObj.getShippingCost(shippingMethod, shipment); + const shipmentUUID = adyenHelperObj.getShipmentUUID(shipment); filteredMethods.push({ ...shippingMethodModel, shippingCost, @@ -135,26 +194,6 @@ let adyenHelperObj = { return filteredMethods; }, - callGetShippingMethods(shippingAddress) { - let address; - try { - address = { - city: shippingAddress.city, - countryCode: shippingAddress.countryCode, - stateCode: shippingAddress.stateOrRegion, - }; - const currentBasket = BasketMgr.getCurrentBasket(); - const currentShippingMethodsModels = this.getApplicableShippingMethods( - currentBasket.getDefaultShipment(), - address, - ); - return currentShippingMethodsModels; - } catch (error) { - AdyenLogs.error_log('Failed to fetch shipping methods'); - AdyenLogs.error_log(error); - } - }, - getAdyenGivingConfig(order) { if (!order.getPaymentInstruments( adyenHelperObj.getOrderMainPaymentInstrumentType(order), @@ -293,7 +332,7 @@ let adyenHelperObj = { return returnValue; }, - // determines whether Adyen Giving is available based on the donation token + // determines whether Adyen Giving is available based on the donation token isAdyenGivingAvailable(paymentInstrument) { return paymentInstrument.paymentTransaction.custom.Adyen_donationToken; }, @@ -337,6 +376,13 @@ let adyenHelperObj = { return false; }, + isPayPalExpress(paymentMethod){ + if (paymentMethod.type === 'paypal' && paymentMethod.subtype === 'express'){ + return true; + } + return false; + }, + // Get stored card token of customer saved card based on matched cardUUID getCardToken(cardUUID, customer) { let token = ''; @@ -489,29 +535,12 @@ let adyenHelperObj = { }, // creates a request object to send to the Adyen Checkout API - createAdyenRequestObject(order, paymentInstrument) { + createAdyenRequestObject(orderNo, orderToken, paymentInstrument) { const jsonObject = JSON.parse(paymentInstrument.custom.adyenPaymentData); const filteredJson = adyenHelperObj.validateStateData(jsonObject); const { stateData } = filteredJson; - let reference = 'recurringPayment-account'; - let orderToken = 'recurringPayment-token'; - if (order && order.getOrderNo()) { - reference = order.getOrderNo(); - orderToken = order.getOrderToken(); - } - - let signature = ''; - //Create signature to verify returnUrl if there is an order - if (order && order.getUUID()) { - signature = adyenHelperObj.createSignature( - paymentInstrument, - order.getUUID(), - reference, - ); - } - // Add recurringProcessingModel in case shopper wants to save the card from checkout if (stateData.storePaymentMethod){ stateData.recurringProcessingModel = constants.RECURRING_PROCESSING_MODEL.CARD_ON_FILE; @@ -525,22 +554,21 @@ let adyenHelperObj = { } stateData.merchantAccount = AdyenConfigs.getAdyenMerchantAccount(); - stateData.reference = reference; - stateData.returnUrl = URLUtils.https( - 'Adyen-ShowConfirmation', - 'merchantReference', - reference, - 'signature', - signature, - 'orderToken', - orderToken, - ).toString(); + stateData.reference = orderNo; + stateData.returnUrl = adyenHelperObj.createRedirectUrl(paymentInstrument, orderNo, orderToken) stateData.applicationInfo = adyenHelperObj.getApplicationInfo(); stateData.additionalData = {}; return stateData; }, + /** + * Returns unique hashed signature. + * @param {dw.order.OrderPaymentInstrument} paymentInstrument - paymentInstrument for the current order or current basket. + * @param {String} value - UUID to be hashed for creating signature. + * @param {String} salt - order number for the current order or from createOrderNo() used as Salt for hash. + * @returns {String} - returns hashed signature. + */ createSignature(paymentInstrument, value, salt) { const newSignature = adyenHelperObj.getAdyenHash(value, salt); Transaction.wrap(function () { @@ -549,6 +577,33 @@ let adyenHelperObj = { return newSignature; }, + /** + * Returns redirectURL with 'Adyen-ShowConfirmation' route and query params . + * @param {dw.order.OrderPaymentInstrument} paymentInstrument - paymentInstrument for the current order or current basket + * @param {String} orderNo - order number for the current order or from createOrderNo() + * @param {String} [orderToken] - orderToken for current order if order exists + * @returns {String} - returns String representation of the redirectURL + */ + createRedirectUrl(paymentInstrument, orderNo, orderToken) { + if(!(paymentInstrument instanceof dw.order.OrderPaymentInstrument)) { + return null + } + const signature = adyenHelperObj.createSignature( + paymentInstrument, + UUIDUtils.createUUID(), + orderNo, + ); + return URLUtils.https( + 'Adyen-ShowConfirmation', + 'merchantReference', + orderNo, + 'signature', + signature, + 'orderToken', + orderToken, + ).toString(); + }, + // adds 3DS2 fields to an Adyen Checkout payments Request add3DS2Data(jsonObject) { jsonObject.authenticationData = { @@ -573,6 +628,9 @@ let adyenHelperObj = { case 'amazonpay': methodName = 'Amazon Pay'; break; + case 'paypal': + methodName = 'PayPal'; + break; default: methodName = paymentMethod; } diff --git a/src/cartridges/int_adyen_SFRA/cartridge/adyen/utils/paypalHelper.js b/src/cartridges/int_adyen_SFRA/cartridge/adyen/utils/paypalHelper.js new file mode 100644 index 000000000..bd912dab8 --- /dev/null +++ b/src/cartridges/int_adyen_SFRA/cartridge/adyen/utils/paypalHelper.js @@ -0,0 +1,177 @@ +/** + * ###### + * ###### + * ############ ####( ###### #####. ###### ############ ############ + * ############# #####( ###### #####. ###### ############# ############# + * ###### #####( ###### #####. ###### ##### ###### ##### ###### + * ###### ###### #####( ###### #####. ###### ##### ##### ##### ###### + * ###### ###### #####( ###### #####. ###### ##### ##### ###### + * ############# ############# ############# ############# ##### ###### + * ############ ############ ############# ############ ##### ###### + * ###### + * ############# + * ############ + * Adyen Salesforce Commerce Cloud + * Copyright (c) 2021 Adyen B.V. + * This file is open source and available under the MIT license. + * See the LICENSE file for more info. + * + * Add all product and shipping line items to request + */ +const Money = require('dw/value/Money'); +const Transaction = require('dw/system/Transaction'); +const LineItemHelper = require('*/cartridge/adyen/utils/lineItemHelper'); +const AdyenHelper = require('*/cartridge/adyen/utils/adyenHelper'); + +const PAYPAL_ITEM_CATEGORY = ['PHYSICAL_GOODS', 'DIGITAL_GOODS', 'DONATION']; +function getLineItems({ Order: order, Basket: basket }) { + if (!(order || basket)) return null; + const orderOrBasket = order || basket; + const allLineItems = LineItemHelper.getAllLineItems( + orderOrBasket.getAllLineItems(), + ); + return allLineItems.map((lineItem) => { + const lineItemObject = {}; + const description = LineItemHelper.getDescription(lineItem); + const id = LineItemHelper.getId(lineItem); + const quantity = LineItemHelper.getQuantity(lineItem); + const itemAmount = LineItemHelper.getItemAmount(lineItem).divide(quantity); + const vatAmount = LineItemHelper.getVatAmount(lineItem).divide(quantity); + // eslint-disable-next-line + if (lineItem.hasOwnProperty('category')) { + if (PAYPAL_ITEM_CATEGORY.indexOf(lineItem.category) > -1) { + lineItemObject.itemCategory = lineItem.category; + } + } + lineItemObject.quantity = quantity; + lineItemObject.description = description; + lineItemObject.sku = id; + lineItemObject.amountExcludingTax = itemAmount.getValue().toFixed(); + lineItemObject.taxAmount = vatAmount.getValue().toFixed(); + return lineItemObject; + }); +} + +/** + * @typedef {object} paypalShippingOption + * @property {string} reference - shipping method id + * @property {string} description - shipping method displayName + * @property {('Shipping')} type + * @property {{currencyCode: String, value: String}} amount + * - shipping cost for shipping method including tax + * @property {boolean} selected - - shipping method is selected + */ + +/** + * @typedef {object} paypalUpdateOrderRequest + * @property {String} pspReference - the pspReference returned from adyen /payments endpoint + * @property {String} paymentData - encrypted payment data from paypal component + * @property {{currencyCode: String, value: String}} amount + * - adjustedMerchandizeTotalGrossPrice + adjustedShippingTotalGrossPrice + * @property {dw.util.ArrayList} deliveryMethods + * - list of paypalShippingOption + */ + +/** + * Returns applicable shipping methods(excluding store pickup methods) + * for specific Shipment / ShippingAddress pair. + * @param {String} pspReference - the pspReference returned from adyen /payments endpoint + * @param {dw.order.basket} amount - a shipment of the current basket + * @param {dw.util.ArrayList} currentShippingMethods + * - a shipment of the current basket + * @param {String} paymentData - encrypted payment data from paypal component + * @returns {paypalUpdateOrderRequest} - list of applicable shipping methods or null + */ +function createPaypalUpdateOrderRequest( + pspReference, + currentBasket, + currentShippingMethods, + paymentData, +) { + const adjustedShippingTotalGrossPrice = { + currency: currentBasket.currencyCode, + value: AdyenHelper.getCurrencyValueForApi( + currentBasket.getAdjustedShippingTotalGrossPrice(), + ).value, + }; + const adjustedMerchandizeTotalGrossPrice = { + currency: currentBasket.currencyCode, + value: + AdyenHelper.getCurrencyValueForApi( + currentBasket.getAdjustedMerchandizeTotalGrossPrice(), + ).value + adjustedShippingTotalGrossPrice.value, + }; + const deliveryMethods = currentShippingMethods.map((shippingMethod) => { + const { currencyCode, value } = shippingMethod.shippingCost; + return { + reference: shippingMethod.ID, + description: shippingMethod.displayName, + type: 'Shipping', + amount: { + currency: currencyCode, + value: AdyenHelper.getCurrencyValueForApi( + new Money(value, currencyCode), + ).value, + }, + selected: shippingMethod.selected, + }; + }); + return { + pspReference, + paymentData, + amount: adjustedMerchandizeTotalGrossPrice, + deliveryMethods, + }; +} + +/** + * sets Shipping and Billing address for the basket + * @param {dw.order.Basket} currentBasket - the current basket + * @returns {undefined} + */ +function setBillingAndShippingAddress(currentBasket) { + let { billingAddress } = currentBasket; + let { shippingAddress } = currentBasket.getDefaultShipment(); + Transaction.wrap(() => { + if (!shippingAddress) { + shippingAddress = currentBasket + .getDefaultShipment() + .createShippingAddress(); + } + if (!billingAddress) { + billingAddress = currentBasket.createBillingAddress(); + } + }); + + const shopperDetails = JSON.parse(session.privacy.shopperDetails); + + Transaction.wrap(() => { + billingAddress.setFirstName(shopperDetails.shopperName.firstName); + billingAddress.setLastName(shopperDetails.shopperName.lastName); + billingAddress.setAddress1(shopperDetails.billingAddress.street); + billingAddress.setCity(shopperDetails.billingAddress.city); + billingAddress.setPhone(shopperDetails.telephoneNumber); + billingAddress.setPostalCode(shopperDetails.billingAddress.postalCode); + billingAddress.setStateCode(shopperDetails.billingAddress.stateOrProvince); + billingAddress.setCountryCode(shopperDetails.billingAddress.country); + + shippingAddress.setFirstName(shopperDetails.shopperName.firstName); + shippingAddress.setLastName(shopperDetails.shopperName.lastName); + shippingAddress.setAddress1(shopperDetails.shippingAddress.street); + shippingAddress.setCity(shopperDetails.shippingAddress.city); + shippingAddress.setPhone(shopperDetails.telephoneNumber); + shippingAddress.setPostalCode(shopperDetails.shippingAddress.postalCode); + shippingAddress.setStateCode( + shopperDetails.shippingAddress.stateOrProvince, + ); + shippingAddress.setCountryCode(shopperDetails.shippingAddress.country); + + currentBasket.setCustomerEmail(shopperDetails.shopperEmail); + }); +} + +module.exports = { + createPaypalUpdateOrderRequest, + getLineItems, + setBillingAndShippingAddress, +}; diff --git a/src/cartridges/int_adyen_SFRA/cartridge/controllers/Adyen.js b/src/cartridges/int_adyen_SFRA/cartridge/controllers/Adyen.js index c8295dcbc..c487831a1 100644 --- a/src/cartridges/int_adyen_SFRA/cartridge/controllers/Adyen.js +++ b/src/cartridges/int_adyen_SFRA/cartridge/controllers/Adyen.js @@ -20,12 +20,19 @@ server.post( adyen.paymentsDetails, ); -server.get( +/** + * Save shipping address to currentBasket and + * get applicable shipping methods from an Express component in the SFCC session + */ +server.post( 'ShippingMethods', server.middleware.https, adyen.callGetShippingMethods, ); +/** + * Save selected shipping method to currentBasket from an Express component in the SFCC session + */ server.post( 'SelectShippingMethod', server.middleware.https, @@ -92,6 +99,15 @@ server.get( adyen.getCheckoutPaymentMethods, ); +/** + * Show the review page template. + */ +server.post( + 'CheckoutReview', + server.middleware.https, + adyen.handleCheckoutReview, +); + /** * Called by Adyen to update status of payments. It should always display [accepted] when finished. */ @@ -125,6 +141,28 @@ server.post( */ server.post('partialPayment', server.middleware.https, adyen.partialPayment); +/** + * Called by Adyen to make /payments call for PayPal Express flow + */ +server.post( + 'MakeExpressPaymentsCall', + server.middleware.https, + adyen.makeExpressPaymentsCall, +); + +/** + * Called by Adyen to make /paymentsDetails for PayPal Express flow + */ +server.post( + 'MakeExpressPaymentDetailsCall', + server.middleware.https, + adyen.makeExpressPaymentDetailsCall, +); + +/** + * Called by Adyen to save the shopper data coming from PayPal Express + */ +server.post('SaveShopperData', server.middleware.https, adyen.saveShopperData); /** * Called by Adyen to fetch applied giftcards */ diff --git a/tests/playwright/fixtures/USD.spec.mjs b/tests/playwright/fixtures/USD.spec.mjs index dba1ec5e6..41e3a262b 100644 --- a/tests/playwright/fixtures/USD.spec.mjs +++ b/tests/playwright/fixtures/USD.spec.mjs @@ -80,7 +80,7 @@ for (const environment of environments) { test('PayPal Success @quick', async ({ page }) => { redirectShopper = new RedirectShopper(page); - await redirectShopper.doPayPalPayment(); + await redirectShopper.doPayPalPayment(false, false, true); await checkoutPage.expectSuccess(); }); }); @@ -251,4 +251,48 @@ for (const environment of environments) { await accountPage.expectFailure(); }); }); + + test.describe.parallel(`${environment.name} USD`, () => { + test.beforeEach(async ({ page }) => { + await page.goto(`${environment.urlExtension}`); + }); + + test('PayPal Express @quick', async ({ page }) => { + checkoutPage = new environment.CheckoutPage(page); + await checkoutPage.addProductToCart(); + await checkoutPage.navigateToCart(regionsEnum.US); + redirectShopper = new RedirectShopper(page); + await redirectShopper.doPayPalPayment(true, false, true); + if (environment.name.indexOf('v5') !== -1) { + await page.locator("button[value='place-order']").click(); + await page.locator(".order-thank-you-msg").isVisible({ timeout: 20000 }); + } + else { + await checkoutPage.expectSuccess(); + } + }); + + test('PayPal Express shipping change @quick', async ({ page }) => { + checkoutPage = new environment.CheckoutPage(page); + await checkoutPage.addProductToCart(); + await checkoutPage.navigateToCart(regionsEnum.US); + redirectShopper = new RedirectShopper(page); + await redirectShopper.doPayPalPayment(true, true, true); + if (environment.name.indexOf('v5') !== -1) { + await page.locator("button[value='place-order']").click(); + await page.locator(".order-thank-you-msg").isVisible({ timeout: 20000 }); + } + else { + await checkoutPage.expectSuccess(); + } + }); + + test('PayPal Express Cancellation @quick', async ({ page }) => { + checkoutPage = new environment.CheckoutPage(page); + await checkoutPage.addProductToCart(); + await checkoutPage.navigateToCart(regionsEnum.US); + redirectShopper = new RedirectShopper(page); + await redirectShopper.doPayPalPayment(true, false, false); + }); + }); } diff --git a/tests/playwright/pages/PaymentMethodsPage.mjs b/tests/playwright/pages/PaymentMethodsPage.mjs index c7ce85447..cdf64f94b 100644 --- a/tests/playwright/pages/PaymentMethodsPage.mjs +++ b/tests/playwright/pages/PaymentMethodsPage.mjs @@ -32,16 +32,18 @@ export default class PaymentMethodsPage { await iDealInput.click(); }; - initiatePayPalPayment = async () => { + initiatePayPalPayment = async (expressFlow, shippingChange, success) => { // Paypal button locator on payment methods page const payPalButton = this.page .frameLocator('.adyen-checkout__paypal__button--paypal iframe.visible') .locator('.paypal-button'); // Click PayPal radio button - await this.page.click('#rb_paypal'); - await expect(this.page.locator('.adyen-checkout__paypal__button--paypal iframe.visible'),).toBeVisible({ timeout: 20000 }); - + if (!expressFlow) { + await this.page.click('#rb_paypal'); + } + await expect(this.page.locator('.adyen-checkout__paypal__button--paypal iframe.visible'),).toBeVisible({ timeout: 20000 }); + // Capture popup for interaction const [popup] = await Promise.all([ this.page.waitForEvent('popup'), @@ -60,13 +62,29 @@ export default class PaymentMethodsPage { this.passwordInput = popup.locator('#password'); this.loginButton = popup.locator('#btnLogin'); this.agreeAndPayNowButton = popup.locator('#payment-submit-btn'); + this.shippingMethodsDropdown = popup.locator('#shippingMethodsDropdown'); + this.cancelButton = popup.locator('a[data-testid="cancel-link"]'); await this.emailInput.click(); await this.emailInput.fill(paymentData.PayPal.username); await this.nextButton.click(); await this.passwordInput.fill(paymentData.PayPal.password); await this.loginButton.click(); - await this.agreeAndPayNowButton.click(); + await this.page.waitForTimeout(5000); + + if (shippingChange){ + await this.shippingMethodsDropdown.selectOption({ index: 2 }); // This selects the second option as first one is hidden by default in paypal modale + await this.page.waitForTimeout(5000); + } + + if (success) { + await this.agreeAndPayNowButton.click(); + } + else { + await this.cancelButton.click(); + await this.page.goBack(); + await expect(this.page.locator('.add-to-cart'),).toBeVisible({ timeout: 20000 }); + } }; initiateAmazonPayment = async ( diff --git a/tests/playwright/paymentFlows/redirectShopper.mjs b/tests/playwright/paymentFlows/redirectShopper.mjs index 3561584fd..7b8c84caf 100644 --- a/tests/playwright/paymentFlows/redirectShopper.mjs +++ b/tests/playwright/paymentFlows/redirectShopper.mjs @@ -29,8 +29,8 @@ export class RedirectShopper { await this.paymentMethodsPage.initiateOneyPayment(shopper); }; - doPayPalPayment = async () => { - await this.paymentMethodsPage.initiatePayPalPayment(); + doPayPalPayment = async (expressFlow, shippingChange, success) => { + await this.paymentMethodsPage.initiatePayPalPayment(expressFlow, shippingChange, success); }; doAmazonPayment = async (normalFlow, selectedCard, success) => {