Files
Odoo-Modules/fusion_clover/static/src/interactions/payment_form.js
gsinghpal a2fe1fcbcc changes
2026-04-29 03:35:33 -04:00

501 lines
18 KiB
JavaScript

/** @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 = `
<div class="text-center p-4">
<div class="spinner-border text-primary mb-3" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<h5>${_t("Waiting for terminal payment...")}</h5>
<p class="text-muted">
${_t("Please complete the payment on the terminal device.")}
</p>
<p class="text-muted small" id="clover_terminal_status">
${_t("Checking status...")}
</p>
</div>
`;
}
this._pollTerminalStatus(processingValues, terminalId);
},
async _pollTerminalStatus(processingValues, terminalId, attempt = 0) {
const maxAttempts = 60;
const pollInterval = 3000;
if (attempt >= maxAttempts) {
this._displayErrorDialog(
_t("Timeout"),
_t("Terminal payment timed out. Please check the device."),
);
this._enableButton();
return;
}
try {
const result = await rpc("/payment/clover/terminal_status", {
reference: processingValues.reference,
terminal_id: parseInt(terminalId),
});
const statusEl = document.getElementById("clover_terminal_status");
if (
result.status === "CLOSED" || result.status === "CAPTURED"
|| result.status === "AUTH" || result.status === "AUTHORIZED"
) {
if (statusEl) {
statusEl.textContent = _t("Payment completed! Redirecting...");
}
window.location.href = processingValues.return_url;
return;
}
if (
result.status === "DECLINED" || result.status === "FAILED"
|| result.status === "FAIL"
) {
this._displayErrorDialog(
_t("Payment Declined"),
_t("The payment was declined at the terminal."),
);
this._enableButton();
return;
}
if (statusEl) {
statusEl.textContent = _t("Status: ") + (result.status || _t("Pending"));
}
setTimeout(
() => this._pollTerminalStatus(processingValues, terminalId, attempt + 1),
pollInterval,
);
} catch {
setTimeout(
() => this._pollTerminalStatus(processingValues, terminalId, attempt + 1),
pollInterval,
);
}
},
});