/** @odoo-module **/ import { _t } from "@web/core/l10n/translation"; import { patch } from "@web/core/utils/patch"; import { rpc } from "@web/core/network/rpc"; import { loadJS } from "@web/core/assets"; import { PaymentForm } from "@payment/interactions/payment_form"; const CLOVER_SDK_URL_TEST = "https://checkout.sandbox.dev.clover.com/sdk.js"; const CLOVER_SDK_URL_PROD = "https://checkout.clover.com/sdk.js"; patch(PaymentForm.prototype, { setup() { super.setup(); this.cloverFormData = {}; this.cloverInstance = null; this.cloverElements = null; this.cloverMountedElements = {}; this._detectedCardType = "other"; this._selectedCardType = "other"; }, // #=== DOM MANIPULATION ===# async _prepareInlineForm(providerId, providerCode, paymentOptionId, paymentMethodCode, flow) { if (providerCode !== "clover") { await super._prepareInlineForm(...arguments); return; } if (flow === "token") { return; } this._setPaymentFlow("direct"); const radio = document.querySelector('input[name="o_payment_radio"]:checked'); const inlineForm = this._getInlineForm(radio); const cloverContainer = inlineForm.querySelector('[name="o_clover_payment_container"]'); if (!cloverContainer) { return; } const rawValues = cloverContainer.dataset["cloverInlineFormValues"]; if (rawValues) { this.cloverFormData = JSON.parse(rawValues); } this._setupTerminalToggle(cloverContainer); this._setupSurcharge(cloverContainer); try { await this._loadCloverSDK(this.cloverFormData.is_test); this._initialiseCloverIframe(cloverContainer); } catch (error) { this._showCloverSdkError( cloverContainer, error?.message || _t("Could not initialise the Clover payment form."), ); } }, async _loadCloverSDK(isTest) { const url = isTest ? CLOVER_SDK_URL_TEST : CLOVER_SDK_URL_PROD; await loadJS(url); if (typeof window.Clover === "undefined") { throw new Error(_t("Clover SDK failed to load.")); } }, _initialiseCloverIframe(container) { const data = this.cloverFormData; const apiAccessKey = data.public_key; const merchantId = data.merchant_id; if (!apiAccessKey) { this._showCloverSdkError( container, _t("This Clover provider has no Public API Key (PAKMS) configured. Add it on the payment provider record before customers can pay online."), ); return; } const cloverConfig = { merchantId }; if (data.locale) { cloverConfig.locale = data.locale; } // eslint-disable-next-line no-undef this.cloverInstance = new Clover(apiAccessKey, cloverConfig); this.cloverElements = this.cloverInstance.elements(); const styles = { input: { fontSize: "16px", fontFamily: "inherit", color: "#212529", padding: "10px 12px", }, ".invalid": { color: "#dc3545" }, }; const mounts = [ ["CARD_NUMBER", "#clover-card-number", "#clover-card-number-errors"], ["CARD_DATE", "#clover-card-date", "#clover-card-date-errors"], ["CARD_CVV", "#clover-card-cvv", "#clover-card-cvv-errors"], ["CARD_POSTAL_CODE", "#clover-card-postal-code", "#clover-card-postal-code-errors"], ]; for (const [type, mountSelector, errorSelector] of mounts) { const element = this.cloverElements.create(type, styles); element.mount(mountSelector); element.addEventListener("change", (event) => { const errorEl = container.querySelector(errorSelector); if (!errorEl) return; if (event && event[type] && event[type].error) { errorEl.textContent = event[type].error; } else { errorEl.textContent = ""; } if (type === "CARD_NUMBER" && event && event.CARD_NUMBER) { const brand = (event.CARD_NUMBER.brand || "").toLowerCase(); if (brand) { const mapped = this._mapCloverBrandToOdoo(brand); if (mapped !== this._detectedCardType) { this._detectedCardType = mapped; if (mapped !== "other") { this._selectedCardType = mapped; } this._updateSurchargeDisplay(container); } } } }); this.cloverMountedElements[type] = element; } }, _mapCloverBrandToOdoo(brand) { const map = { visa: "visa", mastercard: "mastercard", mc: "mastercard", amex: "amex", "american_express": "amex", discover: "other", "diners_club": "other", jcb: "other", unionpay: "other", unknown: "other", }; return map[brand] || "other"; }, _showCloverSdkError(container, message) { const banner = container.querySelector("#clover-sdk-error"); const messageEl = container.querySelector("#clover-sdk-error-message"); if (banner) banner.classList.remove("d-none"); if (messageEl) messageEl.textContent = message; }, _setupSurcharge(container) { const surchargeConfig = this.cloverFormData.surcharge; if (!surchargeConfig || !surchargeConfig.enabled) return; const cardTypeSection = container.querySelector(".o_clover_card_type_section"); if (cardTypeSection) { cardTypeSection.style.display = "block"; } const cardTypeRadios = container.querySelectorAll('input[name="clover_card_type"]'); cardTypeRadios.forEach((radio) => { radio.addEventListener("change", () => { this._selectedCardType = radio.value; this._updateSurchargeDisplay(container); }); }); this._updateSurchargeDisplay(container); }, _updateSurchargeDisplay(container) { const surchargeConfig = this.cloverFormData.surcharge; if (!surchargeConfig || !surchargeConfig.enabled) return; const cardType = this._detectedCardType !== "other" ? this._detectedCardType : this._selectedCardType; const rate = surchargeConfig[cardType] || surchargeConfig["other"] || 0; const minorAmount = this.cloverFormData.minor_amount || 0; const baseAmount = minorAmount / 100; const feeAmount = Math.round(baseAmount * rate) / 100; const rateEl = container.querySelector("#clover_surcharge_rate"); const amountEl = container.querySelector("#clover_surcharge_amount"); const noticeEl = container.querySelector(".o_clover_surcharge_notice"); if (rateEl) rateEl.textContent = rate.toFixed(2); if (amountEl) amountEl.textContent = `$${feeAmount.toFixed(2)}`; if (noticeEl) { noticeEl.style.display = rate > 0 ? "block" : "none"; } const radioToCheck = container.querySelector( `input[name="clover_card_type"][value="${cardType}"]`, ); if (radioToCheck && !radioToCheck.checked) { radioToCheck.checked = true; } }, _setupTerminalToggle(container) { const terminalCheckbox = container.querySelector("#clover_use_terminal"); const terminalSelect = container.querySelector("#clover_terminal_select_wrapper"); const iframeForm = container.querySelector(".o_clover_iframe_form"); if (!terminalCheckbox) { return; } terminalCheckbox.addEventListener("change", () => { if (terminalCheckbox.checked) { if (terminalSelect) terminalSelect.style.display = "block"; if (iframeForm) iframeForm.style.display = "none"; this._loadTerminals(container); } else { if (terminalSelect) terminalSelect.style.display = "none"; if (iframeForm) iframeForm.style.display = "block"; } }); }, async _loadTerminals(container) { const selectEl = container.querySelector("#clover_terminal_select"); if (!selectEl || selectEl.options.length > 1) { return; } try { const terminals = await rpc("/payment/clover/terminals", { provider_id: this.cloverFormData.provider_id, }); selectEl.innerHTML = ""; if (terminals && terminals.length > 0) { terminals.forEach((t) => { const option = document.createElement("option"); option.value = t.id; option.textContent = `${t.name} (${t.status})`; selectEl.appendChild(option); }); } else { const option = document.createElement("option"); option.value = ""; option.textContent = _t("No terminals available"); selectEl.appendChild(option); } } catch { const option = document.createElement("option"); option.value = ""; option.textContent = _t("Failed to load terminals"); selectEl.appendChild(option); } }, // #=== PAYMENT FLOW ===# async _initiatePaymentFlow(providerCode, paymentOptionId, paymentMethodCode, flow) { if (providerCode !== "clover" || flow === "token") { await super._initiatePaymentFlow(...arguments); return; } const radio = document.querySelector('input[name="o_payment_radio"]:checked'); const inlineForm = this._getInlineForm(radio); const useTerminal = inlineForm.querySelector("#clover_use_terminal"); if (useTerminal && useTerminal.checked) { const terminalSelect = inlineForm.querySelector("#clover_terminal_select"); if (!terminalSelect || !terminalSelect.value) { this._displayErrorDialog( _t("Terminal Required"), _t("Please select a terminal device."), ); this._enableButton(); return; } } else if (!this.cloverInstance) { this._displayErrorDialog( _t("Payment form not ready"), _t("The Clover payment form has not finished loading. Please wait a moment and try again."), ); this._enableButton(); return; } await super._initiatePaymentFlow(...arguments); }, async _processDirectFlow(providerCode, paymentOptionId, paymentMethodCode, processingValues) { if (providerCode !== "clover") { await super._processDirectFlow(...arguments); return; } const radio = document.querySelector('input[name="o_payment_radio"]:checked'); const inlineForm = this._getInlineForm(radio); const useTerminal = inlineForm.querySelector("#clover_use_terminal"); if (useTerminal && useTerminal.checked) { await this._processTerminalPayment(processingValues, inlineForm); } else { await this._processCardPayment(processingValues, inlineForm); } }, _getSelectedCardType(inlineForm) { const checked = inlineForm.querySelector('input[name="clover_card_type"]:checked'); return checked ? checked.value : "other"; }, async _processCardPayment(processingValues, inlineForm) { if (!this.cloverInstance) { this._displayErrorDialog( _t("Payment form error"), _t("Clover SDK has not been initialised. Please reload the page."), ); this._enableButton(); return; } const cardType = this._detectedCardType !== "other" ? this._detectedCardType : this._getSelectedCardType(inlineForm); let result; try { result = await this.cloverInstance.createToken(); } catch (error) { this._displayErrorDialog( _t("Card validation error"), error?.message || _t("Could not validate the card details."), ); this._enableButton(); return; } if (!result || result.errors) { const messages = []; if (result && result.errors) { Object.values(result.errors).forEach((value) => { if (typeof value === "string") { messages.push(value); } else if (value && value.error) { messages.push(value.error); } }); } this._displayErrorDialog( _t("Invalid Card Details"), messages.join(" ") || _t("Please check the card details and try again."), ); this._enableButton(); return; } const token = result.token; if (!token) { this._displayErrorDialog( _t("Tokenization Failed"), _t("Clover did not return a card token. Please try again."), ); this._enableButton(); return; } try { const response = await rpc("/payment/clover/process_card", { reference: processingValues.reference, card_token: token, card_type: cardType, }); if (response.error) { this._displayErrorDialog(_t("Payment Failed"), response.error); this._enableButton(); return; } window.location.href = processingValues.return_url; } catch (error) { this._displayErrorDialog( _t("Payment Processing Error"), error.message || _t("An unexpected error occurred."), ); this._enableButton(); } }, async _processTerminalPayment(processingValues, inlineForm) { const terminalId = inlineForm.querySelector("#clover_terminal_select").value; const cardType = this._getSelectedCardType(inlineForm); try { const result = await rpc("/payment/clover/send_to_terminal", { reference: processingValues.reference, terminal_id: parseInt(terminalId), card_type: cardType, }); if (result.error) { this._displayErrorDialog(_t("Terminal Payment Failed"), result.error); this._enableButton(); return; } this._showTerminalWaitingScreen(processingValues, terminalId); } catch (error) { this._displayErrorDialog( _t("Terminal Error"), error.message || _t("Failed to send payment to terminal."), ); this._enableButton(); } }, _showTerminalWaitingScreen(processingValues, terminalId) { const container = document.querySelector(".o_clover_payment_form"); if (container) { container.innerHTML = `
${_t("Please complete the payment on the terminal device.")}
${_t("Checking status...")}