This document provides a detailed comparison between FlexPay cartridge files and the base SFRA cartridge (app_storefront_base).
- Overview
- SFRA 6.x/7.x File Changes
- Detailed File Comparisons
- SFRA 5.x Specific Differences
- New Files (Not in Base SFRA)
The FlexPay cartridge extends SFRA by:
- Modifying existing templates and JavaScript to add FlexPay payment option
- Extending controllers using SFCC's
server.prepend()pattern - Adding new FlexPay-specific files for the payment flow and marketing offers
Version: 0.9.2
| FlexPay File | Base File | Change Type |
|---|---|---|
int_flexpay/.../paymentOptions.isml |
app_storefront_base/.../paymentOptions.isml |
Modified |
int_flexpay/.../paymentOptionsTabs.isml |
app_storefront_base/.../paymentOptionsTabs.isml |
Modified |
int_flexpay/.../paymentOptionsContent.isml |
app_storefront_base/.../paymentOptionsContent.isml |
Modified |
int_flexpay/.../paymentOptionsSummary.isml |
app_storefront_base/.../paymentOptionsSummary.isml |
Modified |
int_flexpay/.../creditCardTab.isml |
app_storefront_base/.../creditCardTab.isml |
Modified |
int_flexpay/.../product/productDetails.isml |
app_storefront_base/.../product/productDetails.isml |
Modified |
int_flexpay/.../checkout/orderTotalSummary.isml |
app_storefront_base/.../checkout/orderTotalSummary.isml |
Modified |
int_flexpay/.../common/scripts.isml |
app_storefront_base/.../common/scripts.isml |
Modified |
int_flexpay/.../flexpay/flexPayMethodTab.isml |
N/A | New |
int_flexpay/.../flexpay/flexPaymentContent.isml |
N/A | New |
int_flexpay/.../flexpay/flexPaySummary.isml |
N/A | New |
int_flexpay/.../flexpay/flexpayOfferWidget.isml |
N/A | New |
int_flexpay/.../flexpay/flexpayInfoModal.isml |
N/A | New |
| FlexPay File | Base File | Change Type |
|---|---|---|
int_flexpay/controllers/CheckoutServices.js |
app_storefront_base/controllers/CheckoutServices.js |
Extended (prepend) |
int_flexpay/controllers/FlexpayOrder.js |
N/A | New |
| FlexPay File | Base File | Change Type |
|---|---|---|
int_flexpay_sfra/.../js/checkout/checkout.js |
app_storefront_base/.../js/checkout/checkout.js |
Modified |
int_flexpay_sfra/.../js/checkout/billing.js |
app_storefront_base/.../js/checkout/billing.js |
Requires base |
int_flexpay_sfra/.../js/flexpaycheckout.js |
N/A | New |
int_flexpay_sfra/.../js/flexpayOfferWidgets.js |
N/A | New (marketing offers) |
int_flexpay_sfra5/.../js/flexpayOfferWidgets.js |
N/A | New (SFRA 5.x version) |
| FlexPay File | Base File | Change Type |
|---|---|---|
int_flexpay/.../scripts/flexpayAPI.js |
N/A | New (added getOffers() method) |
int_flexpay/.../scripts/flexpayConfig.js |
N/A | New (added offer preference methods) |
int_flexpay/.../scripts/flexpayOffers.js |
N/A | New (marketing offer business logic) |
| Test File | Purpose |
|---|---|
test/unit/int_flexpay/scripts/flexpayOffersTest.js |
Unit tests for flexpayOffers.js |
test/unit/int_flexpay_sfra/js/flexpayOfferWidgetsTest.js |
Unit tests for flexpayOfferWidgets.js |
Path: templates/default/checkout/billing/paymentOptions.isml
<div class="form-nav billing-nav payment-information"
data-payment-method-id="CREDIT_CARD"
data-is-new-payment="${pdict.customer.registeredUser && pdict.customer.customerPaymentInstruments.length ? false : true}"
>
<ul class="nav nav-tabs nav-fill payment-options" role="tablist">
<isinclude template="checkout/billing/paymentOptions/paymentOptionsTabs" />
</ul>
</div>
<div class="credit-card-selection-new" >
<div class="tab-content">
<isinclude template="checkout/billing/paymentOptions/paymentOptionsContent" />
</div>
</div>
<div class="form-nav billing-nav payment-information"
data-payment-method-id="${pdict.selectedPaymentMethod}"
data-is-new-payment="${pdict.customer.registeredUser && pdict.customer.customerPaymentInstruments.length ? false : true}"
>
<ul class="nav nav-tabs nav-fill payment-options" role="tablist">
<isinclude template="checkout/billing/paymentOptions/paymentOptionsTabs" />
</ul>
</div>
<div class="credit-card-selection-new" >
<div class="tab-content">
<isinclude template="checkout/billing/paymentOptions/paymentOptionsContent" />
</div>
</div>
<div class="form-nav billing-nav payment-information"
- data-payment-method-id="CREDIT_CARD"
+ data-payment-method-id="${pdict.selectedPaymentMethod}"
data-is-new-payment="${pdict.customer.registeredUser && pdict.customer.customerPaymentInstruments.length ? false : true}"
>Change Summary: Dynamic payment method ID instead of hardcoded "CREDIT_CARD"
Path: templates/default/checkout/billing/paymentOptions/paymentOptionsTabs.isml
<isloop items="${pdict.order.billing.payment.applicablePaymentMethods}" var="paymentOption">
<isif condition="${paymentOption.ID === 'CREDIT_CARD'}">
<isinclude template="checkout/billing/paymentOptions/creditCardTab" />
</isif>
</isloop>
<isloop items="${pdict.order.billing.payment.applicablePaymentMethods}" var="paymentOption">
<isif condition="${paymentOption.ID === 'FLEXPAY' && dw.system.Site.getCurrent().getCustomPreferenceValue('flexPayEnabled')}">
<isinclude template="flexpay/flexPayMethodTab" />
</isif>
<isif condition="${paymentOption.ID === 'CREDIT_CARD'}">
<isinclude template="checkout/billing/paymentOptions/creditCardTab" />
</isif>
</isloop>
<isloop items="${pdict.order.billing.payment.applicablePaymentMethods}" var="paymentOption">
+ <isif condition="${paymentOption.ID === 'FLEXPAY' && dw.system.Site.getCurrent().getCustomPreferenceValue('flexPayEnabled')}">
+ <isinclude template="flexpay/flexPayMethodTab" />
+ </isif>
<isif condition="${paymentOption.ID === 'CREDIT_CARD'}">
<isinclude template="checkout/billing/paymentOptions/creditCardTab" />
</isif>
</isloop>Change Summary: Added FlexPay tab with site preference check before credit card tab
Path: templates/default/checkout/billing/paymentOptions/paymentOptionsContent.isml
<isloop items="${pdict.order.billing.payment.applicablePaymentMethods}" var="paymentOption">
<isif condition="${paymentOption.ID === 'CREDIT_CARD'}">
<isinclude template="checkout/billing/paymentOptions/creditCardContent" />
</isif>
</isloop>
<isloop items="${pdict.order.billing.payment.applicablePaymentMethods}" var="paymentOption">
<isif condition="${paymentOption.ID === 'FLEXPAY' }">
<isinclude template="flexpay/flexPaymentContent" />
</isif>
<isif condition="${paymentOption.ID === 'CREDIT_CARD'}">
<isinclude template="checkout/billing/paymentOptions/creditCardContent" />
</isif>
</isloop>
<isloop items="${pdict.order.billing.payment.applicablePaymentMethods}" var="paymentOption">
+ <isif condition="${paymentOption.ID === 'FLEXPAY' }">
+ <isinclude template="flexpay/flexPaymentContent" />
+ </isif>
<isif condition="${paymentOption.ID === 'CREDIT_CARD'}">
<isinclude template="checkout/billing/paymentOptions/creditCardContent" />
</isif>
</isloop>Change Summary: Added FlexPay content panel inclusion
Path: templates/default/checkout/billing/paymentOptions/paymentOptionsSummary.isml
<div class="payment-details">
<isloop items="${pdict.order.billing.payment.selectedPaymentInstruments}" var="payment">
<isif condition="${payment.paymentMethod === 'CREDIT_CARD'}">
<isinclude template="checkout/billing/paymentOptions/creditCardSummary" />
</isif>
</isloop>
</div>
<div class="payment-details">
<isloop items="${pdict.order.billing.payment.selectedPaymentInstruments}" var="payment">
<isif condition="${payment.paymentMethod === 'CREDIT_CARD'}">
<isinclude template="checkout/billing/paymentOptions/creditCardSummary" />
</isif>
<isif condition="${payment.paymentMethod === 'FLEXPAY'}">
<isinclude template="flexpay/flexPaySummary" />
</isif>
</isloop>
</div>
<div class="payment-details">
<isloop items="${pdict.order.billing.payment.selectedPaymentInstruments}" var="payment">
<isif condition="${payment.paymentMethod === 'CREDIT_CARD'}">
<isinclude template="checkout/billing/paymentOptions/creditCardSummary" />
</isif>
+ <isif condition="${payment.paymentMethod === 'FLEXPAY'}">
+ <isinclude template="flexpay/flexPaySummary" />
+ </isif>
</isloop>
</div>Change Summary: Added FlexPay summary display for order confirmation
Path: templates/default/product/productDetails.isml
<isscript>
var assets = require('*/cartridge/scripts/assets');
assets.addJs('/js/productDetail.js');
assets.addCss('/css/product/detail.css');
</isscript>
<isscript>
var assets = require('*/cartridge/scripts/assets');
assets.addJs('/js/productDetail.js');
assets.addJs('/js/flexpayOfferWidgets.js');
assets.addCss('/css/product/detail.css');
assets.addCss('/css/flexpay.css');
</isscript>
<isscript>
var assets = require('*/cartridge/scripts/assets');
assets.addJs('/js/productDetail.js');
+ assets.addJs('/js/flexpayOfferWidgets.js');
assets.addCss('/css/product/detail.css');
+ assets.addCss('/css/flexpay.css');
</isscript><div class="prices-add-to-cart-actions">
<div class="row">
<div class="col-12">
<!-- Prices -->
<div class="prices">
<isset name="price" value="${product.price}" scope="page" />
<isinclude template="product/components/pricing/main" />
</div>
</div>
</div>
<!-- Cart and [Optionally] Apple Pay -->
<isinclude template="product/components/addToCartProduct" />
</div>
<div class="prices-add-to-cart-actions">
<div class="row">
<div class="col-12">
<!-- Prices -->
<div class="prices">
<isset name="price" value="${product.price}" scope="page" />
<isinclude template="product/components/pricing/main" />
</div>
</div>
</div>
<!-- FlexPay Offer Widget -->
<isscript>
var flexpayConfig = require('*/cartridge/scripts/flexpayConfig');
var offerEnabled = flexpayConfig.isMarketingOfferEnabled();
var showPdpPromo = flexpayConfig.showPdpMarketingOffer();
</isscript>
<isif condition="${offerEnabled && showPdpPromo && product.price && product.price.sales && product.price.sales.value}">
<div class="row flexpay-pdp-offer-container">
<div class="col-12">
<isset name="offerAmount" value="${product.price.sales.value}" scope="page" />
<isset name="offerCurrency" value="${product.price.sales.currency || 'USD'}" scope="page" />
<isset name="offerContext" value="pdp" scope="page" />
<isset name="offerCssClass" value="flexpay-offer-pdp" scope="page" />
<isinclude template="flexpay/flexpayOfferWidget" />
</div>
</div>
</isif>
<!-- Cart and [Optionally] Apple Pay -->
<isinclude template="product/components/addToCartProduct" />
</div>
<div class="prices-add-to-cart-actions">
<div class="row">
<div class="col-12">
<!-- Prices -->
<div class="prices">
<isset name="price" value="${product.price}" scope="page" />
<isinclude template="product/components/pricing/main" />
</div>
</div>
</div>
+ <!-- FlexPay Offer Widget -->
+ <isscript>
+ var flexpayConfig = require('*/cartridge/scripts/flexpayConfig');
+ var offerEnabled = flexpayConfig.isMarketingOfferEnabled();
+ var showPdpPromo = flexpayConfig.showPdpMarketingOffer();
+ </isscript>
+
+ <isif condition="${offerEnabled && showPdpPromo && product.price && product.price.sales && product.price.sales.value}">
+ <div class="row flexpay-pdp-offer-container">
+ <div class="col-12">
+ <isset name="offerAmount" value="${product.price.sales.value}" scope="page" />
+ <isset name="offerCurrency" value="${product.price.sales.currency || 'USD'}" scope="page" />
+ <isset name="offerContext" value="pdp" scope="page" />
+ <isset name="offerCssClass" value="flexpay-offer-pdp" scope="page" />
+
+ <isinclude template="flexpay/flexpayOfferWidget" />
+ </div>
+ </div>
+ </isif>
+
<!-- Cart and [Optionally] Apple Pay -->
<isinclude template="product/components/addToCartProduct" />
</div>Change Summary:
- Added FlexPay offer JavaScript (
flexpayOfferWidgets.js) and CSS (flexpay.css) to page assets - Inserted FlexPay promotional offer widget between product pricing and "Add to Cart" button
- Widget displays conditionally based on three requirements:
flexPayMarketingOfferEnabledsite preference istrueflexPayShowPdpMarketingOffersite preference istrue- Product has a valid sale price (
product.price.sales.value)
- Passes product sale price, currency, context ('pdp'), and CSS class to
flexpayOfferWidget.ismltemplate
Path: templates/default/checkout/orderTotalSummary.isml
<!--- Grand Total --->
<div class="row grand-total leading-lines">
<div class="col-6 start-lines">
<p class="order-receipt-label"><span>${Resource.msg('label.order.grand.total','confirmation', null)}</span></p>
</div>
<div class="col-6 end-lines">
<p class="text-right"><span class="grand-total-sum">${pdict.order.totals.grandTotal}</span></p>
</div>
</div>
<!--- Grand Total --->
<div class="row grand-total leading-lines">
<div class="col-6 start-lines">
<p class="order-receipt-label"><span>${Resource.msg('label.order.grand.total','confirmation', null)}</span></p>
</div>
<div class="col-6 end-lines">
<p class="text-right"><span class="grand-total-sum">${pdict.order.totals.grandTotal}</span></p>
</div>
</div>
<!--- FlexPay Offer Widget --->
<isscript>
var BasketMgr = require('dw/order/BasketMgr');
var currentBasket = BasketMgr.getCurrentBasket();
</isscript>
<isif condition="${currentBasket && currentBasket.totalGrossPrice && currentBasket.totalGrossPrice.available}">
<isset name="offerAmount" value="${currentBasket.totalGrossPrice.value}" scope="page" />
<isset name="offerCurrency" value="${currentBasket.currencyCode}" scope="page" />
<isset name="offerContext" value="checkout-summary" scope="page" />
<isset name="offerCssClass" value="flexpay-offer-checkout-summary" scope="page" />
<isinclude template="flexpay/flexpayOfferWidget" />
</isif>
<!--- Grand Total --->
<div class="row grand-total leading-lines">
<div class="col-6 start-lines">
<p class="order-receipt-label"><span>${Resource.msg('label.order.grand.total','confirmation', null)}</span></p>
</div>
<div class="col-6 end-lines">
<p class="text-right"><span class="grand-total-sum">${pdict.order.totals.grandTotal}</span></p>
</div>
</div>
+
+<!--- FlexPay Offer Widget --->
+<isscript>
+ var BasketMgr = require('dw/order/BasketMgr');
+ var currentBasket = BasketMgr.getCurrentBasket();
+</isscript>
+
+<isif condition="${currentBasket && currentBasket.totalGrossPrice && currentBasket.totalGrossPrice.available}">
+ <isset name="offerAmount" value="${currentBasket.totalGrossPrice.value}" scope="page" />
+ <isset name="offerCurrency" value="${currentBasket.currencyCode}" scope="page" />
+ <isset name="offerContext" value="checkout-summary" scope="page" />
+ <isset name="offerCssClass" value="flexpay-offer-checkout-summary" scope="page" />
+
+ <isinclude template="flexpay/flexpayOfferWidget" />
+</isif>Change Summary:
- Inserted FlexPay promotional offer widget below Grand Total in checkout order summary
- Uses
BasketMgr.getCurrentBasket()to get the current basket's gross price directly (more reliable than string parsing) - Widget displays conditionally based on:
- Current basket exists with available gross price
flexPayMarketingOfferEnabledsite preference istrue(checked by widget)
- Passes basket total amount, currency, context ('checkout-summary'), and CSS class to
flexpayOfferWidget.ismltemplate
Path: templates/default/common/scripts.isml
<!-- No FlexPay JavaScript loading -->
<iscomment>Conditionally load FlexPay JavaScript when offers are enabled</iscomment>
<isscript>
var Site = require('dw/system/Site');
var currentSite = Site.getCurrent();
var flexPayEnabled = currentSite.getCustomPreferenceValue('flexPayEnabled');
var flexPayOfferEnabled = currentSite.getCustomPreferenceValue('flexPayMarketingOfferEnabled');
</isscript>
<isif condition="${flexPayEnabled && flexPayOfferEnabled}">
<isscript>
var assets = require('*/cartridge/scripts/assets.js');
assets.addJs('/js/flexpayOfferWidgets.js');
</isscript>
</isif>
+<iscomment>Conditionally load FlexPay JavaScript when offers are enabled</iscomment>
+<isscript>
+ var Site = require('dw/system/Site');
+ var currentSite = Site.getCurrent();
+ var flexPayEnabled = currentSite.getCustomPreferenceValue('flexPayEnabled');
+ var flexPayOfferEnabled = currentSite.getCustomPreferenceValue('flexPayMarketingOfferEnabled');
+</isscript>
+
+<isif condition="${flexPayEnabled && flexPayOfferEnabled}">
+ <isscript>
+ var assets = require('*/cartridge/scripts/assets.js');
+ assets.addJs('/js/flexpayOfferWidgets.js');
+ </isscript>
+</isif>Change Summary:
- Conditionally loads
flexpayOfferWidgets.jsbundle when both FlexPay and marketing offers are enabled - Prevents unnecessary JavaScript from loading when offers are disabled
- Uses site preferences for runtime control without code changes
Path: templates/default/flexpay/flexPayMethodTab.isml
Purpose: Renders the FlexPay payment method tab with conditional offer widget or static logo.
<isscript>
var assets = require('*/cartridge/scripts/assets.js');
assets.addCss('/css/flexpay.css');
var flexpayConfig = require('*/cartridge/scripts/flexpayConfig');
var BasketMgr = require('dw/order/BasketMgr');
var currentBasket = BasketMgr.getCurrentBasket();
var offerEnabled = flexpayConfig.isMarketingOfferEnabled();
var hasBasket = currentBasket && currentBasket.totalGrossPrice;
var offerAmount = hasBasket ? currentBasket.totalGrossPrice.value : 0;
var minAmount = flexpayConfig.getMarketingOfferMinAmount();
var showOfferInTab = offerEnabled && hasBasket && offerAmount >= minAmount;
</isscript>
<li class="nav-item" data-method-id="${paymentOption.ID}">
<a class="nav-link flexpay-tab <isif condition="${paymentOption.ID === pdict.selectedPaymentMethod}">active</isif>"
data-toggle="tab"
href="#flexpay-content"
role="tab">
<isif condition="${showOfferInTab}">
<iscomment>Show offer widget when offers are enabled and eligible</iscomment>
<isset name="offerAmount" value="${currentBasket.totalGrossPrice.value}" scope="page" />
<isset name="offerCurrency" value="${currentBasket.getCurrencyCode()}" scope="page" />
<isset name="offerContext" value="payment-selector" scope="page" />
<isset name="offerCssClass" value="flexpay-offer-inline" scope="page" />
<isinclude template="flexpay/flexpayOfferWidget" />
<iselse/>
<iscomment>Show standard logo when offers are disabled</iscomment>
<img class="flexpay-payment-option"
src="https://www.upgrade.com/img/flex-pay-logo-horizontal.svg"
height="32"
alt="${paymentOption.name}"
title="${paymentOption.name}" />
</isif>
</a>
</li>
<iscomment>Include FlexPay Info Modal outside li element - will be moved to body by JS</iscomment>
<isif condition="${showOfferInTab && !request.custom.flexpayModalIncluded}">
<isset name="flexpayModalIncluded" value="${true}" scope="request" />
<isinclude template="flexpay/flexpayInfoModal" />
</isif>
Key Features:
- Conditionally shows offer widget OR static logo based on configuration
- Calculates offer eligibility from current basket total
- Includes info modal once per page (prevents duplicate modals)
Path: controllers/CheckoutServices.js
server.post(
'SubmitPayment',
server.middleware.https,
csrfProtection.validateAjaxRequest,
function (req, res, next) {
var PaymentManager = require('dw/order/PaymentMgr');
var HookManager = require('dw/system/HookMgr');
var Resource = require('dw/web/Resource');
var COHelpers = require('*/cartridge/scripts/checkout/checkoutHelpers');
var viewData = {};
var paymentForm = server.forms.getForm('billing');
// verify billing form data
var billingFormErrors = COHelpers.validateBillingForm(paymentForm.addressFields);
var contactInfoFormErrors = COHelpers.validateFields(paymentForm.contactInfoFields);
// ... continues with standard payment processing
}
);'use strict';
var server = require('server');
server.extend(module.superModule);
var csrfProtection = require('*/cartridge/scripts/middleware/csrf');
var COHelpers = require('*/cartridge/scripts/checkout/checkoutHelpers');
var flexpayConfig = require('*/cartridge/scripts/flexpayConfig');
server.prepend(
'SubmitPayment',
server.middleware.https,
csrfProtection.validateAjaxRequest,
function (req, res, next) {
var data = res.getViewData();
var BasketMgr = require('dw/order/BasketMgr');
var currentBasket = BasketMgr.getCurrentOrNewBasket();
var paymentForm = server.forms.getForm('billing');
var paymentMethodID = paymentForm.paymentMethod.value;
var Transaction = require('dw/system/Transaction');
// If not FlexPay, remove any existing FlexPay payment instruments and continue
if (paymentMethodID !== flexpayConfig.getFlexpayPaymentMethodID()) {
var paymentMethod = flexpayConfig.getFlexpayPaymentMethodID();
Transaction.wrap(function () {
var payInstr = currentBasket.getPaymentInstruments(paymentMethod);
var iter = payInstr.iterator();
while (iter.hasNext()) {
var pi = iter.next();
currentBasket.removePaymentInstrument(pi);
}
});
next();
return;
}
// FlexPay-specific validation and processing
// ... validates billing form
// ... sets billing address
// ... calls payment processor hook
if (paymentMethodID === flexpayConfig.getFlexpayPaymentMethodID()) {
this.emit('route:Complete', req, res);
}
}
);
module.exports = server.exports();Change Summary:
- Uses
server.extend()andserver.prepend()pattern - Removes FlexPay payment instruments when switching to other payment methods
- Custom billing validation for FlexPay
- SFRA version-aware email handling
- Terminates request processing after FlexPay payment submission
Path: controllers/FlexpayOrder.js
Purpose: Handles FlexPay order creation, confirmation, cancellation, and marketing offers.
| Endpoint | Method | Description |
|---|---|---|
FlexpayOrder-Create |
POST | Creates FlexPay order, returns redirect URL |
FlexpayOrder-Confirm |
GET | Handles return from FlexPay, creates SFCC order |
FlexpayOrder-Cancel |
GET | Handles cancellation, redirects to checkout |
FlexpayOrder-GetOffer |
GET | Retrieves marketing offers for a given amount |
GetOffer Implementation:
server.get('GetOffer', function (req, res, next) {
var flexpayOffers = require('*/cartridge/scripts/flexpayOffers');
try {
// Parse and validate input
var amount = parseFloat(req.querystring.amount);
var currency = req.querystring.currency || 'USD';
if (!amount || isNaN(amount) || amount <= 0) {
res.json({
success: false,
error: 'Invalid amount parameter'
});
return next();
}
// Get offers from business logic module
var offersData = flexpayOffers.getAvailableOffers(amount, currency);
if (offersData.success) {
// Format offers for display
var formattedOffers = offersData.offers.map(function(offer) {
return flexpayOffers.formatOfferForDisplay(offer);
});
res.json({
success: true,
offers: formattedOffers,
disclaimer: offersData.disclaimer
});
} else {
res.json({
success: false,
error: offersData.error
});
}
} catch (e) {
Logger.error('Exception in FlexpayOrder-GetOffer: ' + e.message);
res.json({
success: false,
error: 'Internal server error'
});
}
return next();
});Path: client/default/js/checkout/checkout.js
} else if (stage === 'placeOrder') {
// disable the placeOrder button here
$('body').trigger('checkout:disableButton', '.next-step-button button');
$.ajax({
url: $('.place-order').data('action'),
method: 'POST',
success: function (data) {
// ... handles order placement
},
error: function () {
$('body').trigger('checkout:enableButton', $('.next-step-button button'));
}
});
return defer;
}} else if (stage === 'placeOrder') {
if ($('.payment-information').data('payment-method-id') === 'FLEXPAY') {
return defer; // Skip - let flexpaycheckout.js handle
}
// disable the placeOrder button here
$('body').trigger('checkout:disableButton', '.next-step-button button');
$.ajax({
url: $('.place-order').data('action'),
method: 'POST',
success: function (data) {
// ... same as base SFRA
},
error: function () {
$('body').trigger('checkout:enableButton', $('.next-step-button button'));
}
});
return defer;
} } else if (stage === 'placeOrder') {
+ if ($('.payment-information').data('payment-method-id') === 'FLEXPAY') {
+ return defer; // Skip - let flexpaycheckout.js handle
+ }
// disable the placeOrder button here
$('body').trigger('checkout:disableButton', '.next-step-button button');Change Summary: Added early return for FlexPay to allow flexpaycheckout.js to handle the custom redirect flow
Path: client/default/js/flexpayOfferWidgets.js
Purpose: Manages marketing offers display, AJAX calls, and modal population.
Key Functions:
| Function | Description |
|---|---|
initializeOfferWidgets() |
Finds all [data-flexpay-offer] elements, fetches offer data |
fetchOfferData(amount, currency, $widget) |
AJAX call to FlexpayOrder-GetOffer |
handleOfferSuccess(response, $widget) |
Stores offers data, renders widget content |
renderOffer(offer, disclaimer, $widget) |
Builds inline offer display HTML |
handleOfferError($widget) |
Graceful degradation - keeps widget hidden |
populateInfoModal(offers, selectedOffer, amount) |
Populates modal from API marketingContent |
populateSteps($modal, steps) |
Updates step text from API |
populateFaqs($modal, faqs) |
Builds FAQ tabs and accordion from API |
markdownToHtml(text, preserveNewlines) |
Converts markdown to HTML |
generateDisclosureText(offers, selectedOffer, amount) |
Creates disclosure with dynamic values |
Event Handlers:
// Product variant selection (PDP)
$('body').on('product:afterAttributeSelect', function(e, data) {
// Updates offer when color/size changes
});
// Payment method selection
$('body').on('payment:methodSelected', function(e, data) {
// Re-initializes widgets when FlexPay tab shown
});
// Modal shown event
$('#flexpayInfoModal').on('show.bs.modal', function(e) {
// Populates modal with dynamic content
});
// FAQ button click
$('#flexpayInfoModal').on('click', '.flexpay-modal-faq-btn', function() {
// Shows FAQ view, hides info view
});
// Back button click
$('#flexpayInfoModal').on('click', '.flexpay-modal-back-btn', function() {
// Returns to info view
});
// FAQ accordion toggle
$('#flexpayInfoModal').on('click', '.flexpay-faq-question', function() {
// Expands/collapses FAQ items
});
// FAQ category tabs
$('#flexpayInfoModal').on('click', '.flexpay-faq-tab', function() {
// Switches between FAQ categories
});Path: scripts/flexpayConfig.js
New methods added for marketing offers:
/**
* Check if marketing offer messaging is enabled
* @returns {boolean} True if enabled
*/
this.isMarketingOfferEnabled = function () {
return currentSite.getCustomPreferenceValue('flexPayMarketingOfferEnabled');
};
/**
* Get minimum amount for marketing offer eligibility
* @returns {Number} Minimum amount
*/
this.getMarketingOfferMinAmount = function () {
return currentSite.getCustomPreferenceValue('flexPayMarketingOfferMinAmount') || 50.0;
};
/**
* Check if PDP marketing offer is enabled
* @returns {boolean} true if PDP marketing offer should be displayed
*/
this.showPdpMarketingOffer = function () {
var showPdp = currentSite.getCustomPreferenceValue('flexPayShowPdpMarketingOffer');
return showPdp === true;
};Path: scripts/flexpayAPI.js
New method added for marketing offers:
/**
* Get available marketing offers for a purchase amount
* Uses the FlexPay bulk orders endpoint with dryRun=true to get offers
* @param {number} amount - Purchase amount
* @param {string} currency - Currency code (default: USD)
* @param {string} locale - Locale string (default: en-US)
* @param {string} country - Country code (default: US)
* @returns {Object} Offer data from FlexPay API
*/
getOffers: function (amount, currency, locale, country) {
var httpService = createHttpService('FlexpayService');
var Site = require('dw/system/Site');
var Locale = require('dw/util/Locale');
var currentLocale = locale || Site.getCurrent().getDefaultLocale();
var currentCurrency = currency || 'USD';
var currentCountry = country || Locale.getLocale(currentLocale).getCountry();
var requestPayload = {
integrationId: flexpayConfig.getSdkKey(),
orders: [{
localization: {
country: currentCountry,
currency: currentCurrency,
locale: convertLocaleFormat(currentLocale.toString())
},
orderCategory: 'TOTAL',
price: amount,
channel: 'WEB',
externalId: dw.util.UUIDUtils.createUUID(),
orderItems: [{
orderLine: {
name: 'Order Total',
quantity: 1,
sku: 'OFFER_REQUEST',
type: 'DIGITAL',
unitPrice: amount
}
}]
}]
};
httpService.URL = flexpayConfig.getURLPath() + '/marketing/offers';
httpService.setRequestMethod('POST');
var response = httpService.call(requestPayload);
return handleFlexpayResponses(httpService.URL, httpService.requestMethod, response, requestPayload);
}Path: scripts/flexpayOffers.js
Purpose: Business logic for marketing offer eligibility, calculation, and formatting.
Exported Functions:
| Function | Description |
|---|---|
isEligibleForOffers(amount) |
Validates amount against minimum threshold from site preferences |
getAvailableOffers(amount, currency) |
Fetches offers from FlexPay API and transforms to internal format |
formatOfferForDisplay(offer) |
Formats offer object for frontend display with formatted prices |
Key Features:
- Checks
flexPayMarketingOfferEnabledsite preference before API calls - Validates amount meets
flexPayMarketingOfferMinAmountthreshold - Transforms FlexPay API response format to simplified display format
- Converts monthly payment to cents (integer) for precision
- Extracts
marketingContentobject for modal display - Handles API error codes (e.g., order ineligibility)
Data Flow:
1. Controller calls getAvailableOffers(amount, currency)
2. Validates: offer feature enabled && amount >= minimum
3. Calls flexpayAPI.api.getOffers(amount, currency)
4. FlexPay API returns: { orders: [{ offers: [...], errorCodes: [...] }] }
5. Transforms API response to internal format:
{
term: 12,
apr: 0.15,
minApr: 0,
maxApr: 0.36,
monthlyPayment: { value: 5221, currency: 'USD' }, // cents
grandTotal: 626.52,
marketingContent: { header, subheader, steps, disclaimer, faqs }
}
6. formatOfferForDisplay() converts to display format:
{
term: 12,
monthlyPayment: { formatted: '$52.21' },
apr: 0.15,
displayText: 'Pay $52.21/mo for 12 months',
marketingContent: { ... } // passed through
}
Error Handling:
- Returns
{ success: false, error: 'message' }for all failures - Logs errors to SFCC Logger
- Graceful degradation (widget stays hidden on error)
The following files have separate implementations for SFRA 5.x vs 6.x/7.x:
| File | SFRA 5.x Location | SFRA 6.x/7.x Location |
|---|---|---|
flexpayOfferWidgets.js |
int_flexpay_sfra5/cartridge/client/default/js/ |
int_flexpay_sfra/cartridge/client/default/js/ |
flexpay.scss |
int_flexpay_sfra5/cartridge/client/default/scss/ |
int_flexpay_sfra/cartridge/client/default/scss/ |
checkout.js |
int_flexpay_sfra5/cartridge/client/default/js/checkout/ |
int_flexpay_sfra/cartridge/client/default/js/checkout/ |
| SFRA 5.x | SFRA 6.x/7.x |
|---|---|
['shipping', 'payment', 'placeOrder', 'submitted'] |
['customer', 'shipping', 'payment', 'placeOrder', 'submitted'] |
| Feature | SFRA 5.x | SFRA 6.x |
|---|---|---|
| Customer stage | Not included | Includes customer login/registration |
| formData object | No customer property |
Has customer property |
| Imports | No customerHelpers |
Includes customerHelpers |
| Order confirmation | URL params redirect | Form POST redirect |
The FlexPay cartridge detects SFRA version and handles email differently:
// Check SFRA version for email handling
if (flexpayConfig.getSfraMajorVersion() <= 5) {
// SFRA 5.x - email is in contactInfoFields on billing form
viewData.email = {
value: paymentForm.contactInfoFields.email.value
};
}
// Later, when setting customer email
if (flexpayConfig.getSfraMajorVersion() <= 5) {
currentBasket.setCustomerEmail(billingData.email.value);
}
// SFRA 6.x - email already set in customer stage| Template | Purpose |
|---|---|
flexpay/flexPayMethodTab.isml |
Payment tab with conditional offer widget or logo |
flexpay/flexPaymentContent.isml |
Hidden form fields and data attributes |
flexpay/flexPaySummary.isml |
Order confirmation summary ("FLEXPAY LOAN") |
flexpay/flexpayOfferWidget.isml |
Reusable offer widget for all 3 locations (PDP, checkout summary, payment tab) |
flexpay/flexpayInfoModal.isml |
Educational modal with 3-step process and FAQs |
Path: templates/default/flexpay/flexpayOfferWidget.isml
Purpose: Reusable widget template for displaying marketing offers in 3 different contexts.
Required Variables (passed via <isset>):
offerAmount(Number) - Purchase amount for offer calculationofferCurrency(String) - Currency code (e.g., 'USD')offerContext(String) - Context identifier:'pdp','checkout-summary', or'payment-selector'offerCssClass(String) - Context-specific CSS class:'flexpay-offer-pdp','flexpay-offer-checkout-summary', or'flexpay-offer-inline'
Rendering Strategy:
- Checks
flexPayMarketingOfferEnabledsite preference - Validates amount meets minimum threshold (
flexPayMarketingOfferMinAmount) - Renders empty container with data attributes (amount, currency, offer-url)
- JavaScript (
flexpayOfferWidgets.js) initializes widget, fetches offers, populates content
HTML Structure:
<div class="flexpay-offer-widget {offerCssClass}"
data-flexpay-offer
data-amount="{amount}"
data-currency="{currency}"
data-context="{context}"
data-offer-url="{FlexpayOrder-GetOffer URL}">
<div class="flexpay-offer-loading" style="display: none;">
<!-- Loading spinner and text -->
</div>
<div class="flexpay-offer-content" style="display: none;">
<!-- Populated by JavaScript after AJAX call -->
</div>
<div class="flexpay-offer-error" style="display: none;">
<!-- Error state: widget stays hidden -->
</div>
</div>
Context Usage:
- PDP (
pdp): Displayed below product price, above "Add to Cart" button - Checkout Summary (
checkout-summary): Displayed below Grand Total in order summary sidebar - Payment Selector (
payment-selector): Displayed inside FlexPay payment tab (replaces static logo when offers enabled)
Modal Inclusion:
- Template includes
flexpayInfoModal.ismlonce per page (controlled byrequest.custom.flexpayModalIncludedflag) - Modal NOT included for
payment-selectorcontext (modal already included by PDP/checkout summary)
Path: templates/default/flexpay/flexpayInfoModal.isml
Purpose: Educational modal explaining FlexPay payment process with dynamic content from API.
Content Sources:
- Static content: Modal structure, navigation buttons, FlexPay logo URL
- Dynamic content: All text populated by JavaScript from API
marketingContentobject
Modal Sections:
| Section | Dynamic Content Source | Description |
|---|---|---|
| Title | marketingContent.header |
Main promotional headline with monthly payment |
| Subtitle | marketingContent.subheader |
Brief value proposition |
| 3-Step Process | marketingContent.steps |
Visual guide (choose, enter info, enjoy) |
| Disclosure | marketingContent.disclaimer |
Legal text with APR ranges and lending partner links |
| FAQ | marketingContent.faqs |
Categorized questions/answers with accordion UI |
Navigation:
- Info View (default): Shows title, subtitle, steps, disclosure, FAQ button
- FAQ View: Shows FAQ categories and accordion
- Back Button: Returns from FAQ view to info view
JavaScript Integration:
- Modal HTML is static skeleton
- Content populated by
flexpayOfferWidgets.json modal show event - Markdown formatting (
**bold**,[link](url)) converted to HTML client-side
FAQ Structure:
faqs: {
header: "Frequently Asked Questions",
items: [
{
category: "GENERAL",
questions: [
{ question: "How does FlexPay work?", answer: "..." }
]
},
{
category: "PAYMENTS & INSTALLMENTS",
questions: [...]
}
]
}Features:
- Category Tabs: Switch between FAQ categories (GENERAL, PAYMENTS & INSTALLMENTS, etc.)
- Accordion: Expand/collapse individual FAQ items
- Markdown Support: API text with
**bold**and[link](url)converted to HTML - Responsive: Adapts to mobile and desktop layouts
| File | Purpose |
|---|---|
flexpay.js |
Main module exports (config, api, constants) |
flexpayAPI.js |
API client for OAuth, orders, transactions, offers |
flexpayConfig.js |
Site preference configuration reader |
flexpayConstants.js |
Transaction status and job result constants |
flexpayJobs.js |
Scheduled job implementations (capture, refund, void) |
flexpayOffers.js |
Marketing offers business logic |
processor/flexpayPayment.js |
Payment processor hooks (Handle, Authorize) |
| File | Purpose |
|---|---|
flexpaycheckout.js |
FlexPay checkout flow handler |
flexpayOfferWidgets.js |
Offer widget initialization, AJAX, modal population |
New preferences added for marketing offers:
| Attribute ID | Type | Default | Description |
|---|---|---|---|
flexPayMarketingOfferEnabled |
Boolean | false |
Enable marketing offer messages |
flexPayMarketingOfferMinAmount |
Double | 50.0 |
Minimum amount for offers |
flexPayShowPdpMarketingOffer |
Boolean | true |
Show offers on PDP |
Document Version: 1.1
Last Updated: February 2026
Cartridge Version: 0.9.2