diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 4b3c473d..5c7d0e5b 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,6 +1,10 @@ CHANGELOG ========= +v4.4.0 +------ +* Feat: In Page checkout + v4.3.1 ------ * Feat: add Pay Now with credit card with Alma. diff --git a/cartridges/int_alma/cartridge/controllers/Alma.js b/cartridges/int_alma/cartridge/controllers/Alma.js index 90ea75f9..8cb6d4b0 100644 --- a/cartridges/int_alma/cartridge/controllers/Alma.js +++ b/cartridges/int_alma/cartridge/controllers/Alma.js @@ -90,11 +90,11 @@ function affectOrder(paymentObj, order) { } /** - * Request order from OrderMgr + * Request order from OrderMgr by Alma payment ID * @param {string} pid payment id * @returns {Object} order */ -function buildOrder(pid) { +function getOrderByAlmaPaymentId(pid) { var OrderMgr = require('dw/order/OrderMgr'); return OrderMgr.queryOrder( 'custom.almaPaymentId={0}', @@ -126,7 +126,7 @@ server.get('PaymentSuccess', function (req, res, next) { }); return next(); } - var order = buildOrder(req.querystring.pid); + var order = getOrderByAlmaPaymentId(req.querystring.pid); if (!order) { order = paymentHelper.createOrderFromBasket(req.querystring.alma_payment_method); @@ -195,7 +195,7 @@ server.get('IPN', function (req, res, next) { }); return next(); } - var order = buildOrder(req.querystring.pid); + var order = getOrderByAlmaPaymentId(req.querystring.pid); if (!order) { var basketUuid = paymentObj.custom_data.basket_id; @@ -320,44 +320,44 @@ server.post( ); server.get( - 'FragmentCheckout', + 'InpageCheckout', server.middleware.https, function (req, res, next) { var almaPaymentHelper = require('*/cartridge/scripts/helpers/almaPaymentHelper'); - var BasketMgr = require('dw/order/BasketMgr'); + var getLocale = require('*/cartridge/scripts/helpers/almaHelpers').getLocale; + var paymentData = almaPaymentHelper.buildPaymentData( + req.querystring.installments, + req.querystring.deferred_days, + getLocale(req) + ); try { - var basketAmount = Math.round(BasketMgr.getCurrentBasket().totalGrossPrice.multiply(100).value); - var paymentFormAmount = parseInt(req.querystring.amount, 10); - if (basketAmount !== paymentFormAmount) { - var mismatchErrorContext = { - basketAmount: basketAmount, - paymentFormAmount: paymentFormAmount - }; - - logger.warn('Mismatch error | {0}', [JSON.stringify(mismatchErrorContext)]); - res.setStatusCode(400); - res.json({ - error: 'The amount of the shopping cart was changed.' - }); - } - - var order = buildOrder(req.querystring.pid); + var almaPayment = almaPaymentHelper.createPayment(paymentData); + var order = getOrderByAlmaPaymentId(almaPayment.id); if (!order) { order = almaPaymentHelper.createOrderFromBasket(req.querystring.alma_payment_method); - syncOrderAndPaymentDetails(req.querystring.pid, order); + syncOrderAndPaymentDetails(almaPayment.id, order); } - + res.setStatusCode(200); res.json({ - order: JSON.stringify(order) + order: order, + payment_id: almaPayment.id }); } catch (e) { res.setStatusCode(500); - res.json({ - error: e.message - }); + + if (e.name === 'create_payment_error') { + res.json({ + error: 'Could not create payment on Alma side' + }); + } else { + res.json({ + error: e.message + }); + } } + return next(); }); diff --git a/cartridges/int_alma/cartridge/controllers/Checkout.js b/cartridges/int_alma/cartridge/controllers/Checkout.js index 0d89fcfa..1e74db39 100644 --- a/cartridges/int_alma/cartridge/controllers/Checkout.js +++ b/cartridges/int_alma/cartridge/controllers/Checkout.js @@ -32,7 +32,7 @@ function getAlmaUrls() { data_url: URLUtils.http('Alma-BasketData').toString(), create_payment_url: URLUtils.https('Alma-CreatePaymentUrl').toString(), order_amount_url: URLUtils.http('Alma-OrderAmount').toString(), - checkout_fragment_url: URLUtils.http('Alma-FragmentCheckout').toString(), + inpage_checkout_url: URLUtils.http('Alma-InpageCheckout').toString(), plans_url: URLUtils.http('Alma-Plans').toString() }; } @@ -68,8 +68,9 @@ server.append('Begin', function (req, res, next) { currencyCode: currentBasket.currencyCode, purchase_amount: Math.round(currentBasket.totalGrossPrice.multiply(100).value), plans: almaPlanHelper.getPlansForCheckout(getLocale(req), currentBasket), - fragment_on_close_message: Resource.msg('alma.fragment_on_close_message', 'alma', null), - fragment_on_failure_message: Resource.msg('alma.fragment_on_failure_message', 'alma', null) + inpage_on_close_message: Resource.msg('alma.inpage_on_close_message', 'alma', null), + inpage_on_failure_message: Resource.msg('alma.inpage_on_failure_message', 'alma', null), + locale: getLocale(req) } ); diff --git a/cartridges/int_alma/cartridge/scripts/helpers/almaCheckoutHelper.js b/cartridges/int_alma/cartridge/scripts/helpers/almaCheckoutHelper.js index c9527e98..f6a432cd 100644 --- a/cartridges/int_alma/cartridge/scripts/helpers/almaCheckoutHelper.js +++ b/cartridges/int_alma/cartridge/scripts/helpers/almaCheckoutHelper.js @@ -104,7 +104,7 @@ function getInstallmentCountAfterFirst(plan) { } /** - * Returns the property displaying installments before calling fragment + * Returns the property displaying installments before calling inpage * @param {Object} plan any alma plan * @param {string} currencyCode e.g. 'EUR' * @returns {string} the message to display for a payment option @@ -158,21 +158,22 @@ function getPropertiesForPlan(plan, currencyCode) { /** * Returns if the current payment option is pnx 2,3 or 4 - * @param {Object} plan any alma plan - * @returns {boolean} true means we can use fragment + * @param {int} installmentsCount installments count + * @param {int} deferredDays deferred days + * @returns {boolean} true means we can use inpage */ -function isPnx(plan) { - return plan.installments_count <= 4; +function isAvailableForInpage(installmentsCount, deferredDays) { + return installmentsCount <= 4 && deferredDays <= 0; } /** * Returns true if the merchant want in-page payment - * @returns {boolean} if we can use fragment + * @returns {boolean} if we can use inpage */ -function isFragmentActivated() { +function isInpageActivated() { var Site = require('dw/system/Site'); - return Site.getCurrent().getCustomPreferenceValue('ALMA_Fragment_Payment'); + return Site.getCurrent().getCustomPreferenceValue('ALMA_Inpage_Payment'); } /** @@ -223,7 +224,7 @@ function formatPlanForCheckout(plan, currencyCode) { var formatPlan = {}; if (plan.installments_count < 5 && planIsActivated(PaymentMgr.getPaymentMethod(ALMA_PNX_ID), plan)) { formatPlan = { - in_page: isPnx(plan) && isFragmentActivated(), + in_page: isAvailableForInpage(plan.installments_count, plan.deferred_days) && isInpageActivated(), selector: getSelectorNameFromPlan(plan), installments_count: plan.installments_count, deferred_days: plan.deferred_days, @@ -236,7 +237,7 @@ function formatPlanForCheckout(plan, currencyCode) { } if (plan.installments_count >= 5 && planIsActivated(PaymentMgr.getPaymentMethod(ALMA_CREDIT_ID), plan)) { formatPlan = { - in_page: isPnx(plan) && isFragmentActivated(), + in_page: isAvailableForInpage(plan.installments_count, plan.deferred_days) && isInpageActivated(), selector: getSelectorNameFromPlan(plan), installments_count: plan.installments_count, deferred_days: plan.deferred_days, @@ -249,7 +250,7 @@ function formatPlanForCheckout(plan, currencyCode) { } if (plan.deferred_days > 0 && planIsActivated(PaymentMgr.getPaymentMethod(ALMA_DEFERRED_ID), plan)) { formatPlan = { - in_page: isPnx(plan) && isFragmentActivated(), + in_page: isAvailableForInpage(plan.installments_count, plan.deferred_days) && isInpageActivated(), selector: getSelectorNameFromPlan(plan), installments_count: plan.installments_count, deferred_days: plan.deferred_days, @@ -262,7 +263,7 @@ function formatPlanForCheckout(plan, currencyCode) { } if (plan.installments_count === 1 && plan.deferred_days === 0 && planIsActivated(PaymentMgr.getPaymentMethod(ALMA_PAY_NOW_ID), plan)) { formatPlan = { - in_page: false, + in_page: isAvailableForInpage(plan.installments_count, plan.deferred_days) && isInpageActivated(), selector: getSelectorNameFromPlan(plan), installments_count: plan.installments_count, deferred_days: plan.deferred_days, @@ -278,5 +279,7 @@ function formatPlanForCheckout(plan, currencyCode) { module.exports = { formatPlanForCheckout: formatPlanForCheckout, - getPlanPaymentMethodID: getPlanPaymentMethodID + getPlanPaymentMethodID: getPlanPaymentMethodID, + isAvailableForInpage: isAvailableForInpage, + isInpageActivated: isInpageActivated }; diff --git a/cartridges/int_alma/cartridge/scripts/helpers/almaHelpers.js b/cartridges/int_alma/cartridge/scripts/helpers/almaHelpers.js index 31cd886d..9e0fc6ee 100644 --- a/cartridges/int_alma/cartridge/scripts/helpers/almaHelpers.js +++ b/cartridges/int_alma/cartridge/scripts/helpers/almaHelpers.js @@ -146,9 +146,12 @@ function haveExcludedCategory(productIds) { }); } }); - logger.warn('categoriesID {0}', [JSON.stringify(categoriesID)]); - var categoriesExcluded = Site.getCurrent().getCustomPreferenceValue('categoryExclusion').trim().split(' | '); + var categoriesExcluded = []; + + if (Site.getCurrent().getCustomPreferenceValue('categoryExclusion')) { + categoriesExcluded = Site.getCurrent().getCustomPreferenceValue('categoryExclusion').trim().split(' | '); + } var haveExcludedCategoryReturn = false; diff --git a/cartridges/int_alma/cartridge/scripts/helpers/almaPaymentHelper.js b/cartridges/int_alma/cartridge/scripts/helpers/almaPaymentHelper.js index bf358fe3..6609edb7 100644 --- a/cartridges/int_alma/cartridge/scripts/helpers/almaPaymentHelper.js +++ b/cartridges/int_alma/cartridge/scripts/helpers/almaPaymentHelper.js @@ -271,10 +271,12 @@ function createOrderFromBasket(almaPaymentMethod) { */ function createPayment(param) { var service = require('*/cartridge/scripts/services/alma'); - var httpResult = service.createPayment().call(param); + if (httpResult.msg !== 'OK') { - throw new Error('API error : ' + httpResult.status); + var e = new Error('API error : ' + httpResult.status); + e.name = 'create_payment_error'; + throw e; } return JSON.parse(httpResult.getObject().text); } @@ -290,11 +292,17 @@ function buildPaymentData(installmentsCount, deferredDays, locale) { var BasketMgr = require('dw/order/BasketMgr'); var URLUtils = require('dw/web/URLUtils'); var almaHelper = require('*/cartridge/scripts/helpers/almaHelpers'); + var almaCheckoutHelper = require('*/cartridge/scripts/helpers/almaCheckoutHelper'); var formatAddress = require('*/cartridge/scripts/helpers/almaAddressHelper').formatAddress; var isOnShipmentPaymentEnabled = require('*/cartridge/scripts/helpers/almaOnShipmentHelper').isOnShipmentPaymentEnabled; var formatCustomerData = require('*/cartridge/scripts/helpers/almaHelpers').formatCustomerData; + var origin = 'online'; + if (almaCheckoutHelper.isAvailableForInpage(installmentsCount, deferredDays) && almaCheckoutHelper.isInpageActivated()) { + origin = 'online_in_page'; + } + var currentBasket = BasketMgr.getCurrentBasket(); var isEnableOnShipment = isOnShipmentPaymentEnabled(installmentsCount); @@ -308,7 +316,7 @@ function buildPaymentData(installmentsCount, deferredDays, locale) { ipn_callback_url: URLUtils.http('Alma-IPN').toString(), customer_cancel_url: URLUtils.https('Alma-CustomerCancel').toString(), locale: locale, - origin: 'online', + origin: origin, shipping_address: formatAddress(currentBasket.getDefaultShipment().shippingAddress), billing_address: formatAddress(currentBasket.getBillingAddress()), deferred: isEnableOnShipment ? 'trigger' : '', diff --git a/cartridges/int_alma/cartridge/static/default/js/almaCheckout.js b/cartridges/int_alma/cartridge/static/default/js/almaCheckout.js index e388d2cd..f4d0b9ab 100644 --- a/cartridges/int_alma/cartridge/static/default/js/almaCheckout.js +++ b/cartridges/int_alma/cartridge/static/default/js/almaCheckout.js @@ -1,7 +1,7 @@ window.addEventListener('DOMContentLoaded', function () { - var purchase_amount = Number(almaContext.payment.purchaseAmount); + var purchase_amount = Number(almaContext.payment.purchaseAmount); function assignAlmaElementsValues(plan) { for (const [id, property] of Object.entries(plan.properties)) { @@ -29,84 +29,85 @@ window.addEventListener('DOMContentLoaded', } /* Uses jQuery here because context.updateCheckoutViewEvent is triggered with jQuery */ - jQuery('body').on(almaContext.updateCheckoutViewEvent, async function() { - var checkoutBtn = document.querySelector(almaContext.selector.submitPayment); - var nextStepButton = checkoutBtn.parentElement.parentElement; - nextStepButton.classList.add('next-step-button'); - checkoutBtn.setAttribute('type', 'submit'); - - + jQuery('body') + .on(almaContext.updateCheckoutViewEvent, async function () { + var checkoutBtn = document.querySelector(almaContext.selector.submitPayment); + var nextStepButton = checkoutBtn.parentElement.parentElement; + nextStepButton.classList.add('next-step-button'); + checkoutBtn.setAttribute('type', 'submit'); + + + var responseOrderAmount = await fetch(almaContext.almaUrl.orderAmountUrl); + var dataOrderAmount = await responseOrderAmount.json(); + purchase_amount = dataOrderAmount.purchase_amount; + + var response = await fetch(almaContext.almaUrl.plans_url); + var data = await response.json(); + almaPaymentMethods = data.plans; + + for (const [indexPaymentMethod, almaPaymentMethod] of Object.entries(almaPaymentMethods)) { + var name = almaPaymentMethod.name; + var plans = almaPaymentMethod.plans; + + for (const [indexPlan, plan] of Object.entries(plans)) { + var icons = document.querySelectorAll(".alma-payment-method .fa"); + [].forEach.call(icons, function (icon) { + icon.classList.remove("fa-chevron-down"); + }); - var responseOrderAmount = await fetch(almaContext.almaUrl.orderAmountUrl); - var dataOrderAmount = await responseOrderAmount.json() - purchase_amount = dataOrderAmount.purchase_amount; + document.getElementById(`${plan.key + '-inpage'}`).innerHTML = ""; - var response = await fetch(almaContext.almaUrl.plans_url); - var data = await response.json() - almaPaymentMethods = data.plans; + if (plan.payment_plans) { + if (document.getElementById(plan.key).hasAttribute("hidden")){ + document.getElementById(plan.key) + .removeAttribute('hidden'); + document.getElementById(`${'alma-tab-' + plan.key + '-img'}`) + .removeAttribute('hidden'); + } - for (const [indexPaymentMethod, almaPaymentMethod] of Object.entries(almaPaymentMethods)) { - var name = almaPaymentMethod.name; - var plans = almaPaymentMethod.plans; - for (const [indexPlan, plan] of Object.entries(plans)) { - var icons = document.querySelectorAll(".alma-payment-method .fa"); - [].forEach.call(icons, function (icon) { - icon.classList.remove("fa-chevron-down"); - }); + assignAlmaElementsValues(plan); + continue; + } + document.getElementById(plan.key) + .setAttribute('hidden', 'hidden'); + document.getElementById(`${'alma-tab-' + plan.key + '-img'}`) + .setAttribute('hidden', 'hidden'); - document.getElementById(`${plan.key + '_fragment'}`).innerHTML = ""; + } - if (plan.payment_plans) { - document.getElementById(plan.key) + if (almaPaymentMethod.hasEligiblePaymentMethod) { + document.getElementById(`${'alma-tab-' + name}`) .removeAttribute('hidden'); - if (document.getElementById(`${'alma-tab-' + plan.key + '-img'}`)) { - document.getElementById(`${'alma-tab-' + plan.key + '-img'}`) - .removeAttribute('hidden'); - } - - assignAlmaElementsValues(plan); continue; } - document.getElementById(plan.key) - .setAttribute('hidden', 'hidden'); - document.getElementById(`${'alma-tab-' + plan.key + '-img'}`) + document.getElementById(`${'alma-tab-' + name}`) .setAttribute('hidden', 'hidden'); } - if (almaPaymentMethod.hasEligiblePaymentMethod) { - document.getElementById(`${'alma-tab-' + name}`) - .removeAttribute('hidden'); - continue; - } - document.getElementById(`${'alma-tab-' + name}`) - .setAttribute('hidden', 'hidden'); - - } - - }); + }); - var checkoutFragmentCallInProgress = false; + var checkoutInpageCallInProgress = false; var checkoutEvents = []; function addCheckoutEvent(event) { document .querySelector(almaContext.selector.submitPayment) - .addEventListener('click', event) + .addEventListener('click', event); } function removeCheckoutEvents() { var lastEvent; - var event = checkoutEvents.shift() + var event = checkoutEvents.shift(); while (event) { lastEvent = event; document .querySelector(almaContext.selector.submitPayment) - .removeEventListener('click', event) + .removeEventListener('click', event); - event = checkoutEvents.shift() + event = checkoutEvents.shift(); } if (lastEvent) { @@ -117,93 +118,9 @@ window.addEventListener('DOMContentLoaded', var paymentOptions = document.querySelectorAll(almaContext.selector.paymentOptions); paymentOptions.forEach(function (paymentOption) { - paymentOption.addEventListener('click', removeCheckoutEvents) + paymentOption.addEventListener('click', removeCheckoutEvents); }); - /** - * Returns the data formatted for in-page payment - * @param {Object} data an alma plan - * @param {number} installments_count number of installments - * @param {number} deferred_days number of days before the 1st payment - * @returns {Object} - */ - function getPaymentData(data, installments_count, deferred_days) { - return { - payment: { - purchase_amount: purchase_amount, - installments_count: installments_count, - deferred_days: deferred_days, - deferred_months: 0, - return_url: almaContext.payment.returnUrl, - ipn_callback_url: almaContext.payment.ipnCallbackUrl, - customer_cancel_url: almaContext.payment.customerCancelUrl, - locale: data.locale.split("_")[0], - shipping_address: data.shipping_address, - deferred: data.isEnableOnShipment ? "trigger" : "", - deferred_description: data.isEnableOnShipment ? decodeHtml(almaContext.payment.deferredDescription) : "", - custom_data: { - cms_name: data.cms_name, - cms_version: data.cms_version, - alma_plugin_version: data.alma_plugin_version - } - }, - customer: data.customer - }; - } - - /** - * Build an in-page payment form to allow the customer to pay - * This option is only available when installments_count is 2 or 3 - * @param {string} container the div elem where the form will be built - * @param {number} installments_count number of installments - * @param {number} deferred_days number of days before the 1st payment - */ - async function renderInPage( - container, - installments_count, - deferred_days - ) { - var response = await fetch(almaContext.almaUrl.dataUrl + '?installment=' + installments_count); - var data = await response.json(); - - var paymentData = getPaymentData(data, installments_count, deferred_days) - - var fragments = new Alma.Fragments(almaContext.merchantId, { - mode: almaContext.almaMode === 'LIVE' ? Alma.ApiMode.LIVE : Alma.ApiMode.TEST - }); - - var paymentForm = fragments.createPaymentForm(paymentData, { - showPayButton: false, - onSuccess: function (returnedData) { - window.location = returnedData.return_url; - }, - onFailure: function () { - addCheckoutEvent(checkoutEvents.at(-1)); - checkoutFragmentCallInProgress = false; - displayAlmaErrors(almaContext.fragmentOnFailureMessage, 'fragment-on-failure') - }, - onPopupClose: function () { - addCheckoutEvent(checkoutEvents.at(-1)); - checkoutFragmentCallInProgress = false; - displayAlmaErrors(almaContext.fragmentOnCloseMessage, 'fragment-on-close') - } - }) - - await paymentForm.mount(document.getElementById(container)); - return paymentForm; - } - - /** - * SFCC resource needs to be decoded to be given to Alma Fragment - * @param {string} ressource the message to decode - * @returns {string} the decoded string - */ - function decodeHtml(ressource) { - var txt = document.createElement("textarea"); - txt.innerHTML = ressource; - return txt.value; - } - /* * Redirect to Alma website to allow the customer to pay * @param {number} installments_count number of installments @@ -218,11 +135,34 @@ window.addEventListener('DOMContentLoaded', window.location = body.url; } + async function inPageInitialize(inPageContainer, installments_count) { + return Alma.InPage.initialize( + { + merchantId: almaContext.merchantId, + amountInCents: purchase_amount, + installmentsCount: installments_count, + selector: "#" + inPageContainer, + locale: almaContext.locale.slice(0, 2), + environment: almaContext.almaMode + } + ); + } + /** * Open a payment option and hide the others * @param {Object} t a JS selector */ async function toggle(t) { + var inPages = document.querySelectorAll('[id$="-inpage"]'); + inPages.forEach(function (inPage) { + if (inPage.firstChild) { + inPage + .firstChild + .remove(); + } + }); + + removeCheckoutEvents(); var activeElt = document.querySelector("#" + t.id + " .fa"); var isAlreadyOpen = activeElt.classList.contains("fa-chevron-down"); @@ -240,39 +180,48 @@ window.addEventListener('DOMContentLoaded', var alma_payment_method = t.getAttribute('data-alma-payment-method'); var in_page = t.getAttribute('data-in-page') === 'true'; - document.body.style.cursor = 'wait'; if (in_page) { - await renderInPage(t.id + "_fragment", installments_count, deferred_days) - .then(function (paymentForm) { - var checkoutFragmentCall = async function () { - if (checkoutFragmentCallInProgress) { + await inPageInitialize( + t.id + "-inpage", + installments_count + ) + .then(function (inPage) { + var checkoutInpageCall = async function () { + if (checkoutInpageCallInProgress) { return; } - checkoutFragmentCallInProgress = true; - var ajaxResponse = await fetch(almaContext.almaUrl.checkoutFragmentUrl + '?pid=' + paymentForm.currentPayment.id + '&amount=' + paymentForm.currentPayment.purchase_amount + '&alma_payment_method=' + alma_payment_method); - var orderFragment = await ajaxResponse.json(); - switch (ajaxResponse.status) { + checkoutInpageCallInProgress = true; + var ajaxInPageResponse = await fetch(almaContext.almaUrl.inPageCheckoutUrl + '?alma_payment_method=' + alma_payment_method + '&deferred_days=' + deferred_days + '&installments=' + installments_count); + var inPagePaymentResponse = await ajaxInPageResponse.json(); + switch (ajaxInPageResponse.status) { case 200: - removeCheckoutEvents(); - paymentForm.pay(); + $.spinner() + .start(); + inPage.startPayment({ + paymentId: inPagePaymentResponse.payment_id, + onUserCloseModal: () => { + $.spinner() + .stop(); + } + }); + checkoutInpageCallInProgress = false; break; case 400: - displayMismatchMessage(orderFragment) - checkoutFragmentCallInProgress = false; + displayMismatchMessage(inPagePaymentResponse); + checkoutInpageCallInProgress = false; break; case 500: - displayAlmaErrors(orderFragment.error, 'payment-method-not-found-message') - checkoutFragmentCallInProgress = false; + displayAlmaErrors(inPagePaymentResponse.error, 'payment-method-not-found-message'); + checkoutInpageCallInProgress = false; break; default: - displayAlmaErrors(ajaxResponse.status, 'payment-error') - checkoutFragmentCallInProgress = false; + displayAlmaErrors(ajaxInPageResponse.status, 'payment-error'); + checkoutInpageCallInProgress = false; } - } - - checkoutEvents.push(checkoutFragmentCall); + }; - addCheckoutEvent(checkoutFragmentCall); + checkoutEvents.push(checkoutInpageCall); + addCheckoutEvent(checkoutInpageCall); }); } else { activeElt.parentNode.innerHTML = '