Merchant Fee for Payment Plan - Shopify Plus


Plugin icon

Shopify+

Integration guide for Zip MFPP on Shopify Plus


All of the files required for integration are stored in the integration subdirectory of this repository.

Checkout integration

Perform the following steps for integration:

  1. Open your store's admin area, and navigate to Online Store > Themes. Click on the Actions drop-down next to the theme you'd like to integrate Zip MFPP with and select "Edit code".

  2. On the left-hand side list, under the "Snippets" section, click "Add a new snippet" and name it zip-checkout-mfpp.

  3. Paste the contents of the zip-checkout-mfpp.liquid file listed below into the code area and click "Save" once you finish.

{% comment %}<!-- Zip MFPP -->{% endcomment %}
{% assign zip_mfpp_url = "https://gateway.quadpay.com/orders/calculate-merchant-fees" -%}
{% if zip_test -%}
{% assign zip_mfpp_url = "https://sandbox.gateway.quadpay.com/orders/calculate-merchant-fees" -%}
{% endif -%}
<script type="application/javascript">
// Zip Checkout MFPP - v1.2.0
{% assign zip_default_money_format = '${{amount}}' -%}
window.QuadPayCheckoutOptions = {
    merchantId: "{{zip_merchant_id}}",
    state: "{{checkout.shipping_address.province_code | default: checkout.billing_address.province_code}}",
    country: "{{checkout.shipping_address.country_code | default: checkout.billing_address.country_code}}",
    currency: "{{shop.currency}}",
    moneyFormat: "{{zip_money_format | default: zip_default_money_format}}",
    quadpayFeeApi: "{{ zip_mfpp_url }}",
    quadpayRegion: "{{ zip_region | default: "US" }}"
};

{% if zip_selector %}
window.QuadPayCheckoutOptions.quadpayGatewaySelector = "{{zip_selector}}";
{% endif %}

{% if order.attributes.zip-mfpp-amount -%}
{% comment %}/* Thank You page and paid with Zip. */{% endcomment %}
window.QuadPayCheckoutOptions.thankYouPageFee = {{ order.attributes.zip-mfpp-amount }};
{% endif -%}

/* global document, window */
window.QuadPayCheckout = {
    selectors: {
        orderSummaryTotalLines: '.total-line-table__tbody',
        paymentMethodSection: '.section--payment-method .section__content',
        total: '.total-line-table__footer .total-line__price .payment-due__price',
        totalRecap: '.total-recap__final-price',
        mainForm: "form[data-payment-form]",
        paymentMethodTransactions: ".payment-method-list__item__amount",
        orderAttributesInputName: "checkout[attributes][zip-mfpp-amount]",
    },
    quadpayFeeTotalLineClass: '.total-line--quadpay-fee',
    init: function(options, step) {
        this.options = options;

        if (step === 'payment_method') {
            this.initPaymentMethodStep();
        } else {
            this.initThankYouPage();
        }
    },
    initPaymentMethodStep: function() {
        const quadpayGatewaySelector = this.options.quadpayGatewaySelector || this.findGatewaySelector();
        if (!quadpayGatewaySelector) {
            const style = "background-color:white;font-weight:bold;";
            console.log(
                "%c[ZIP] Cannot find Zip gateway selector, please refer to documentation at: %chttps://docs.us.zip.co/docs/mfpp-shopify-plus",
                "color:#411361;" + style,
                "color:#6542be;" + style
            );
            return;
        }

        this.shopifyTotal = this.getShopifyTotal();
        this.monitorGatewaySelection(quadpayGatewaySelector);
        this.monitorCurrencySelection();
        this.refresh(true);
    },
    initThankYouPage: function() {
        let fee = this.options.thankYouPageFee;
        if (fee) {
            this.shopifyTotal = this.getShopifyTotal();
            this.setTotalLinePriceIncrease(fee, this.options.currency);

            this.setQuadPayLinePrice(fee);
        }
    },
    findGatewaySelector: function() {
        const identifyingElement = document.querySelector('[data-payment-icon="zip"]') ||
            Array.prototype.find.call(document.querySelectorAll('.radio__label__primary'), function (el) {
                return el.innerText.match(/(Zip|Quadpay|QuadPay)/);
            });

        if (!identifyingElement) {
            return null;
        }

        const wrapperElement = this.findClosestParentElement(identifyingElement, ".radio-wrapper");
        if (!wrapperElement || !wrapperElement.dataset.selectGateway) {
            return null;
        }

        return "#checkout_payment_gateway_" + wrapperElement.dataset.selectGateway;
    },
    findClosestParentElement: function(element, selector) {
        if (element.closest && typeof element.closest === "function") {
            return element.closest(selector);
        }

        const findClosest = function (element, selector) {
            if (element.matches(selector)) {
                return element;
            } else if (!element.parentNode) {
                return null;
            }

            return findClosest(element.parentNode, selector);
        };

        return findClosest(element, selector);
    },
    refresh: function(reloadFee) {
        if (reloadFee === true) {
            this.fee = null;
        }
        this.getQuadPayFee().then((function (fee) {
            this.fee = fee;
            if (this.quadpayIsSelected === true) {
                this.repaintFee(this.fee);
                this.generateOrderAttributesInput(this.fee);
            } else {
                this.hideFee();
                this.removeOrderAttributesInput();
            }
        }).bind(this));
    },
    repaintFee: function(fee) {
        this.setTotalLinePriceIncrease(fee);
        this.setQuadPayLinePrice(fee);
    },
    hideFee: function() {
        this.setTotalLinePriceIncrease(0);
        this.setQuadPayLinePrice(null);
    },
    setTotalLinePriceIncrease: function(fee, currency) {
        const money = this.formatInCurrency(this.shopifyTotal + fee, currency);
        const selector = this.selectors.total + ',' + this.selectors.totalRecap + ',' + this.selectors.paymentMethodTransactions;
        document.querySelectorAll(selector).forEach(function(el) {
            el.innerHTML = money;
        });
    },
    setQuadPayLinePrice: function(fee) {
        let quadPayLine = document.querySelector(this.quadpayFeeTotalLineClass);
        if (quadPayLine) {
            quadPayLine.parentNode.removeChild(quadPayLine);
        }

        if (fee === null || fee < 0.01) {
            return;
        }

        this.generateQuadPayFeeLine(fee);
    },
    monitorGatewaySelection: function(quadpayGatewaySelector) {
        this.quadpayIsSelected = null;
        const gatewaySelected = (function() {
            const radio = document.querySelector(quadpayGatewaySelector);
            const isSelected = radio && radio.checked;
            const shouldRefresh = this.quadpayIsSelected !== null && this.quadpayIsSelected !== isSelected;
            this.quadpayIsSelected = isSelected;
            if (shouldRefresh) {
                this.refresh(false);
            }
        }).bind(this);
        const paymentMethodSection = document.querySelector(this.selectors.paymentMethodSection);
        if (!paymentMethodSection) {
            return;
        }
        paymentMethodSection.addEventListener('click', gatewaySelected);
        gatewaySelected();
    },
    monitorCurrencySelection: function() {
        const currencies = document.querySelector("#currencies");
        if (!currencies) {
            return;
        }

        currencies.addEventListener('change', (function () {
            this.refresh(false);
        }).bind(this));
    },
    generateQuadPayFeeLine: function(fee) {
        let subtotalsElement = document.querySelector(this.selectors.orderSummaryTotalLines);
        if (subtotalsElement) {
            subtotalsElement.insertAdjacentHTML('beforeend', this.generateLine(fee));
        }
    },
    removeOrderAttributesInput: function() {
        document.getElementsByName(this.selectors.orderAttributesInputName).forEach(function (input) {
            try {
                input.parentNode.removeChild(input);
            } catch (_) {}
        });
    },
    generateOrderAttributesInput: function(fee) {
        const orderAttributeInputs = document.getElementsByName(this.selectors.orderAttributesInputName);

        let orderAttributeInput = orderAttributeInputs.length > 0 ? orderAttributeInputs[0] : null;
        if (!orderAttributeInput) {
            const form = document.querySelector(this.selectors.mainForm);
            if (!form) {
                return;
            }

            orderAttributeInput = document.createElement("input");
            orderAttributeInput.setAttribute("type", "hidden");
            orderAttributeInput.setAttribute("name", this.selectors.orderAttributesInputName);
            form.appendChild(orderAttributeInput);
        }

        orderAttributeInput.setAttribute("value", fee);
    },
    generateLine: function(fee) {
        const money = this.formatInCurrency(fee);
        let className = this.quadpayFeeTotalLineClass.replace(/^\./, '');
        return '<tr class="total-line ' + className + '">' +
            '<th class="total-line__name" scope="row">Merchant Fee for Payment Plan</th>' +
            '<td class="total-line__price"><b>' + money + '</b></td>' +
            '</tr>';
    },
    getQuadPayFee: function() {
        if (this.fee !== null) {
            return Promise.resolve(this.fee);
        }
        return this.fetchFee().then(function(data) {
            if (!('merchantFeeForPaymentPlan' in data)) {
                return Promise.resolve(null);
            }
            let fee = parseFloat(data.merchantFeeForPaymentPlan) * 100;
            return Promise.resolve(fee);
        });
    },
    fetchFee: function() {
        let totalForQuadPay = this.shopifyTotal / 100;
        let dataForQuadPay = {
            merchantId: this.options.merchantId,
            currency: this.options.currency,
            customerCountry: this.options.country,
            amount: totalForQuadPay
        };
        if (this.options.state) {
            dataForQuadPay.customerState = this.options.state;
        }
        return window.fetch(this.options.quadpayFeeApi, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'QP-Territory': this.options.quadpayRegion
            },
            body: JSON.stringify(dataForQuadPay)
        }).then(function (response) {
            return response.json();
        });
    },
    getShopifyTotal: function() {
        let totalElement = document.querySelector(this.selectors.total);
        if (!totalElement) {
            return;
        }
        return parseInt(totalElement.getAttribute('data-checkout-payment-due-target'), 10);
    },
    formatInCurrency: function (amount, currency) {
        if (window.Currency) {
            const currentCurrency = currency || window.Currency.cookie.read();
            const total = window.Currency.convert(amount, window.Shopify.Checkout.currency || this.options.currency, currentCurrency);
            const moneyFormat = window.Currency.moneyFormats[currentCurrency][window.Currency.format];
            return window.Currency.formatMoney(total, moneyFormat);
        }

        return this.shopifyFormatMoney(amount);
    },
    shopifyFormatMoney: function(cents) {
        if (typeof cents == 'string') {
            cents = cents.replace('.','');
        }
        let value = '';
        let placeholderRegex = /\{\{\s*(\w+)\s*\}\}/;
        let formatString = this.options.moneyFormat;

        function defaultOption(opt, def) {
            return (typeof opt == 'undefined' ? def : opt);
        }

        function formatWithDelimiters(number, precision, thousands, decimal) {
            precision = defaultOption(precision, 2);
            thousands = defaultOption(thousands, ',');
            decimal   = defaultOption(decimal, '.');

            if (isNaN(number) || number == null) { return 0; }

            number = (number/100.0).toFixed(precision);
            let parts   = number.split('.'),
                dollars = parts[0].replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1' + thousands),
                cents   = parts[1] ? (decimal + parts[1]) : '';

            return dollars + cents;
        }

        switch(formatString.match(placeholderRegex)[1]) {
            case 'amount':
                value = formatWithDelimiters(cents, 2);
                break;
            case 'amount_no_decimals':
                value = formatWithDelimiters(cents, 0);
                break;
            case 'amount_with_comma_separator':
                value = formatWithDelimiters(cents, 2, '.', ',');
                break;
            case 'amount_no_decimals_with_comma_separator':
                value = formatWithDelimiters(cents, 0, '.', ',');
                break;
        }

        return formatString.replace(placeholderRegex, value);
    }
};
(function() {
    let step = window.Shopify.Checkout.step;
    if (!step && window.QuadPayCheckoutOptions.thankYouPageFee) {
        step = 'thank_you';
    }
    if (['payment_method', 'thank_you'].indexOf(step) !== -1) {
        window.QuadPayCheckout.init(window.QuadPayCheckoutOptions, step);
    }
    // re-init on apply gift card and calculating taxes
    if (step === 'payment_method') {
        document.addEventListener('page:change', function () {
            window.setTimeout(function () {
                window.QuadPayCheckout.init(window.QuadPayCheckoutOptions, step);
            }, 500);
        });
    }
})();

</script>
  1. On the left hand side, navigate to the "Layout" section and determine if you have a template file called checkout.liquid. If you do, you should skip the next step.

  2. Click on "Add a new layout" and on the dropdown select "checkout", leave the "called" field empty.

  3. Paste the following snippet that calls the MFPP script before the tag:

{% comment %}<!-- Begin Zip checkout MFPP -->{% endcomment %}
{% assign zip_merchant_id = 'your-merchant-id' -%}
{% include 'zip-checkout-mfpp' %}
{% comment %}<!-- End Zip checkout MFPP -->{% endcomment %}
  1. Change the your_merchant_id value with your Merchant ID.

  2. Save the file and test the results.

📘

Manually setting the zip_selector

If after step 8, the Merchant Fee for Payment Plan line item does not show at checkout when Zip is selected, you can insert the following line of code to the above snippet in step 6:

{% assign zip_selector = '#checkout_payment_gateway_123456' -%}

Then, navigate to your online store and start the checkout process, navigate to the payment methods step, open the developer console, and find the radio button for Zip. Copy the ID of the radio button and set the zip_selector value in checkout.liqud, in the snippet in step 6, to that ID (instead of 123456). Make sure to prepend the ID with #checkoutpayment_gateway".

That finishes the integration for the payment step and order confirmation pages.

501

Order confirmation email integration

Perform the following steps in order to integrate the notification on the confirmation email:

  1. Navigate to Settings > Notifications > Order confirmation.

  2. In the code editor, add the following snippet to the top of the code:

{% comment %} Zip MFPP integration snippet {% endcomment %}
{% assign zip_fee = attributes.zip-mfpp-amount | default: 0 | plus: 0 -%}
{% comment %} END Zip MFPP integration snippet {% endcomment %}
  1. Locate the following line:
<table class="row subtotal-table subtotal-table--total">
  1. Above that line, there should be a tag. Paste the following snippet of code above it:
{% comment %} Zip MFPP integration snippet {% endcomment %}
{% if zip_fee > 0 %}
<tr class="subtotal-line">
  <td class="subtotal-line__title">
    <p>
      <span>Merchant fee for payment plan</span>
    </p>
  </td>
  <td class="subtotal-line__value">
    <strong>{{ zip_fee | money }}</strong>
  </td>
</tr>
{% endif %}
{% comment %} END Zip MFPP integration snippet {% endcomment %}
  1. Find the line that contains the following snippet:
<strong>{{ total_price | money_with_currency }}</strong>

Replace that entire line with the contents of the following snippet:

{% comment %} Zip MFPP integration snippet {% endcomment %}
<strong>{{ total_price | plus: zip_fee | money_with_currency }}</strong>
{% comment %} END Zip MFPP integration snippet {% endcomment %}

  1. Locate all instances of {{ transaction.amount | money }} and modify them so that you add the zip_fee value to them by using Liquid's plus filter. It should look similar to {{ transaction.amount | plus: zip_fee | money }}.


🚧

Note:

You should only perform step number 6 if you installed Zip payment gateway through the Payments app, if you are using the legacy implementation, you must not perform that step.


582