531 lines
19 KiB
JavaScript
531 lines
19 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 { PaymentForm } from '@payment/interactions/payment_form';
|
|
|
|
patch(PaymentForm.prototype, {
|
|
|
|
setup() {
|
|
super.setup();
|
|
this.poyntFormData = {};
|
|
this._detectedCardType = 'other';
|
|
this._selectedCardType = 'other';
|
|
},
|
|
|
|
// #=== DOM MANIPULATION ===#
|
|
|
|
async _prepareInlineForm(providerId, providerCode, paymentOptionId, paymentMethodCode, flow) {
|
|
if (providerCode !== 'poynt') {
|
|
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 poyntContainer = inlineForm.querySelector('[name="o_poynt_payment_container"]');
|
|
|
|
if (!poyntContainer) {
|
|
return;
|
|
}
|
|
|
|
const rawValues = poyntContainer.dataset['poyntInlineFormValues'];
|
|
if (rawValues) {
|
|
this.poyntFormData = JSON.parse(rawValues);
|
|
}
|
|
|
|
this._setupCardFormatting(poyntContainer);
|
|
this._setupTerminalToggle(poyntContainer);
|
|
this._setupSurcharge(poyntContainer);
|
|
this._prefillBillingAddress(poyntContainer);
|
|
},
|
|
|
|
_prefillBillingAddress(container) {
|
|
const billing = this.poyntFormData.billing_details;
|
|
if (!billing || !billing.address) return;
|
|
const addr = billing.address;
|
|
const setVal = (id, val) => {
|
|
const el = container.querySelector(id);
|
|
if (el && val) el.value = val;
|
|
};
|
|
setVal('#poynt_billing_address', addr.line1);
|
|
setVal('#poynt_billing_city', addr.city);
|
|
setVal('#poynt_billing_state', addr.state);
|
|
setVal('#poynt_billing_zip', addr.postal_code);
|
|
setVal('#poynt_billing_country', addr.country);
|
|
},
|
|
|
|
_detectCardBrand(number) {
|
|
const num = (number || '').replace(/\D/g, '');
|
|
if (num.length < 2) return 'other';
|
|
const prefix2 = num.substring(0, 2);
|
|
if (prefix2 === '34' || prefix2 === '37') return 'amex';
|
|
if (num[0] === '4') return 'visa';
|
|
const p2 = parseInt(prefix2, 10);
|
|
if (p2 >= 51 && p2 <= 55) return 'mastercard';
|
|
if (num.length >= 4) {
|
|
const p4 = parseInt(num.substring(0, 4), 10);
|
|
if (p4 >= 2221 && p4 <= 2720) return 'mastercard';
|
|
}
|
|
return 'other';
|
|
},
|
|
|
|
_setupSurcharge(container) {
|
|
const surchargeConfig = this.poyntFormData.surcharge;
|
|
if (!surchargeConfig || !surchargeConfig.enabled) return;
|
|
|
|
const cardTypeSection = container.querySelector('.o_poynt_card_type_section');
|
|
const surchargeNotice = container.querySelector('.o_poynt_surcharge_notice');
|
|
|
|
if (cardTypeSection) {
|
|
cardTypeSection.style.display = 'block';
|
|
}
|
|
|
|
const cardTypeRadios = container.querySelectorAll('input[name="poynt_card_type"]');
|
|
cardTypeRadios.forEach(radio => {
|
|
radio.addEventListener('change', () => {
|
|
this._selectedCardType = radio.value;
|
|
this._updateSurchargeDisplay(container);
|
|
});
|
|
});
|
|
|
|
this._updateSurchargeDisplay(container);
|
|
},
|
|
|
|
_updateSurchargeDisplay(container) {
|
|
const surchargeConfig = this.poyntFormData.surcharge;
|
|
if (!surchargeConfig || !surchargeConfig.enabled) return;
|
|
|
|
const cardType = this._detectedCardType !== 'other'
|
|
? this._detectedCardType
|
|
: this._selectedCardType;
|
|
|
|
const rate = surchargeConfig[cardType] || surchargeConfig['other'] || 0;
|
|
const amount = this.poyntFormData.minor_amount || 0;
|
|
const currencyName = this.poyntFormData.currency_name || 'CAD';
|
|
|
|
const baseAmount = amount / 100;
|
|
const feeAmount = Math.round(baseAmount * rate) / 100;
|
|
|
|
const rateEl = container.querySelector('#poynt_surcharge_rate');
|
|
const amountEl = container.querySelector('#poynt_surcharge_amount');
|
|
const noticeEl = container.querySelector('.o_poynt_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="poynt_card_type"][value="${cardType}"]`
|
|
);
|
|
if (radioToCheck && !radioToCheck.checked) {
|
|
radioToCheck.checked = true;
|
|
}
|
|
},
|
|
|
|
_setupCardFormatting(container) {
|
|
const cardInput = container.querySelector('#poynt_card_number');
|
|
if (cardInput) {
|
|
cardInput.addEventListener('input', (e) => {
|
|
let value = e.target.value.replace(/\D/g, '');
|
|
let formatted = '';
|
|
for (let i = 0; i < value.length && i < 16; i++) {
|
|
if (i > 0 && i % 4 === 0) {
|
|
formatted += ' ';
|
|
}
|
|
formatted += value[i];
|
|
}
|
|
e.target.value = formatted;
|
|
|
|
const detected = this._detectCardBrand(value);
|
|
if (detected !== this._detectedCardType) {
|
|
this._detectedCardType = detected;
|
|
if (detected !== 'other') {
|
|
this._selectedCardType = detected;
|
|
}
|
|
this._updateSurchargeDisplay(
|
|
e.target.closest('.o_poynt_payment_form')
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
const expiryInput = container.querySelector('#poynt_expiry');
|
|
if (expiryInput) {
|
|
expiryInput.addEventListener('input', (e) => {
|
|
let value = e.target.value.replace(/\D/g, '');
|
|
if (value.length >= 2) {
|
|
value = value.substring(0, 2) + '/' + value.substring(2, 4);
|
|
}
|
|
e.target.value = value;
|
|
});
|
|
}
|
|
},
|
|
|
|
_setupTerminalToggle(container) {
|
|
const terminalCheckbox = container.querySelector('#poynt_use_terminal');
|
|
const terminalSelect = container.querySelector('#poynt_terminal_select_wrapper');
|
|
const cardFields = container.querySelectorAll(
|
|
'#poynt_card_number, #poynt_expiry, #poynt_cvv, #poynt_cardholder'
|
|
);
|
|
|
|
if (!terminalCheckbox) {
|
|
return;
|
|
}
|
|
|
|
terminalCheckbox.addEventListener('change', () => {
|
|
if (terminalCheckbox.checked) {
|
|
if (terminalSelect) {
|
|
terminalSelect.style.display = 'block';
|
|
}
|
|
cardFields.forEach(f => {
|
|
f.closest('.mb-3').style.display = 'none';
|
|
f.removeAttribute('required');
|
|
});
|
|
this._loadTerminals(container);
|
|
} else {
|
|
if (terminalSelect) {
|
|
terminalSelect.style.display = 'none';
|
|
}
|
|
cardFields.forEach(f => {
|
|
f.closest('.mb-3').style.display = 'block';
|
|
if (f.id !== 'poynt_cardholder') {
|
|
f.setAttribute('required', 'required');
|
|
}
|
|
});
|
|
}
|
|
});
|
|
},
|
|
|
|
async _loadTerminals(container) {
|
|
const selectEl = container.querySelector('#poynt_terminal_select');
|
|
if (!selectEl || selectEl.options.length > 1) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const terminals = await rpc('/payment/poynt/terminals', {
|
|
provider_id: this.poyntFormData.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 !== 'poynt' || 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('#poynt_use_terminal');
|
|
|
|
if (useTerminal && useTerminal.checked) {
|
|
const terminalId = inlineForm.querySelector('#poynt_terminal_select').value;
|
|
if (!terminalId) {
|
|
this._displayErrorDialog(
|
|
_t("Terminal Required"),
|
|
_t("Please select a terminal device."),
|
|
);
|
|
this._enableButton();
|
|
return;
|
|
}
|
|
} else {
|
|
const validationError = this._validateCardInputs(inlineForm);
|
|
if (validationError) {
|
|
this._displayErrorDialog(
|
|
_t("Invalid Card Details"),
|
|
validationError,
|
|
);
|
|
this._enableButton();
|
|
return;
|
|
}
|
|
}
|
|
|
|
await super._initiatePaymentFlow(...arguments);
|
|
},
|
|
|
|
_validateCardInputs(inlineForm) {
|
|
const cardNumber = inlineForm.querySelector('#poynt_card_number');
|
|
const expiry = inlineForm.querySelector('#poynt_expiry');
|
|
const cvv = inlineForm.querySelector('#poynt_cvv');
|
|
|
|
const cardDigits = cardNumber.value.replace(/\D/g, '');
|
|
if (cardDigits.length < 13 || cardDigits.length > 19) {
|
|
return _t("Please enter a valid card number.");
|
|
}
|
|
|
|
const expiryValue = expiry.value;
|
|
if (!/^\d{2}\/\d{2}$/.test(expiryValue)) {
|
|
return _t("Please enter a valid expiry date (MM/YY).");
|
|
}
|
|
|
|
const [month, year] = expiryValue.split('/').map(Number);
|
|
if (month < 1 || month > 12) {
|
|
return _t("Invalid expiry month.");
|
|
}
|
|
|
|
const now = new Date();
|
|
const expiryDate = new Date(2000 + year, month);
|
|
if (expiryDate <= now) {
|
|
return _t("Card has expired.");
|
|
}
|
|
|
|
const cvvValue = cvv.value.replace(/\D/g, '');
|
|
if (cvvValue.length < 3 || cvvValue.length > 4) {
|
|
return _t("Please enter a valid CVV.");
|
|
}
|
|
|
|
return null;
|
|
},
|
|
|
|
async _processDirectFlow(providerCode, paymentOptionId, paymentMethodCode, processingValues) {
|
|
if (providerCode !== 'poynt') {
|
|
await super._processDirectFlow(...arguments);
|
|
return;
|
|
}
|
|
|
|
const radio = document.querySelector('input[name="o_payment_radio"]:checked');
|
|
const inlineForm = this._getInlineForm(radio);
|
|
const useTerminal = inlineForm.querySelector('#poynt_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="poynt_card_type"]:checked');
|
|
return checked ? checked.value : 'other';
|
|
},
|
|
|
|
_showProcessingOverlay(container) {
|
|
const overlay = container.querySelector('.o_poynt_processing_overlay');
|
|
if (overlay) {
|
|
// Hide all form field sections
|
|
Array.from(container.children).forEach(child => {
|
|
if (!child.classList.contains('o_poynt_processing_overlay')) {
|
|
child.style.display = 'none';
|
|
}
|
|
});
|
|
overlay.style.display = 'block';
|
|
}
|
|
},
|
|
|
|
_hideProcessingOverlay(container) {
|
|
const overlay = container.querySelector('.o_poynt_processing_overlay');
|
|
if (overlay) {
|
|
overlay.style.display = 'none';
|
|
Array.from(container.children).forEach(child => {
|
|
if (!child.classList.contains('o_poynt_processing_overlay')) {
|
|
child.style.display = '';
|
|
}
|
|
});
|
|
}
|
|
},
|
|
|
|
_updateProcessingMessage(container, message) {
|
|
const msgEl = container.querySelector('.o_poynt_processing_message');
|
|
if (msgEl) msgEl.textContent = message;
|
|
},
|
|
|
|
async _processCardPayment(processingValues, inlineForm) {
|
|
const cardNumber = inlineForm.querySelector('#poynt_card_number').value.replace(/\D/g, '');
|
|
const expiry = inlineForm.querySelector('#poynt_expiry').value;
|
|
const cvv = inlineForm.querySelector('#poynt_cvv').value;
|
|
const cardholder = inlineForm.querySelector('#poynt_cardholder').value;
|
|
const cardType = this._detectedCardType !== 'other'
|
|
? this._detectedCardType
|
|
: this._getSelectedCardType(inlineForm);
|
|
|
|
const [expMonth, expYear] = expiry.split('/').map(Number);
|
|
|
|
const formContainer = inlineForm.closest('.o_poynt_payment_form')
|
|
|| inlineForm.querySelector('.o_poynt_payment_form')
|
|
|| inlineForm;
|
|
|
|
// Show processing animation
|
|
this._showProcessingOverlay(formContainer);
|
|
|
|
try {
|
|
const result = await rpc('/payment/poynt/process_card', {
|
|
reference: processingValues.reference,
|
|
poynt_order_id: processingValues.poynt_order_id,
|
|
card_number: cardNumber,
|
|
exp_month: expMonth,
|
|
exp_year: 2000 + expYear,
|
|
cvv: cvv,
|
|
cardholder_name: cardholder,
|
|
card_type: cardType,
|
|
billing_address: inlineForm.querySelector('#poynt_billing_address')?.value || '',
|
|
billing_city: inlineForm.querySelector('#poynt_billing_city')?.value || '',
|
|
billing_state: inlineForm.querySelector('#poynt_billing_state')?.value || '',
|
|
billing_zip: inlineForm.querySelector('#poynt_billing_zip')?.value || '',
|
|
billing_country: inlineForm.querySelector('#poynt_billing_country')?.value || '',
|
|
});
|
|
|
|
if (result.error) {
|
|
this._hideProcessingOverlay(formContainer);
|
|
this._displayErrorDialog(
|
|
_t("Payment Failed"),
|
|
result.error,
|
|
);
|
|
this._enableButton();
|
|
return;
|
|
}
|
|
|
|
this._updateProcessingMessage(formContainer, _t("Payment successful! Redirecting..."));
|
|
window.location.href = processingValues.return_url;
|
|
} catch (error) {
|
|
this._hideProcessingOverlay(formContainer);
|
|
this._displayErrorDialog(
|
|
_t("Payment Processing Error"),
|
|
error.message || _t("An unexpected error occurred."),
|
|
);
|
|
this._enableButton();
|
|
}
|
|
},
|
|
|
|
async _processTerminalPayment(processingValues, inlineForm) {
|
|
const terminalId = inlineForm.querySelector('#poynt_terminal_select').value;
|
|
const cardType = this._getSelectedCardType(inlineForm);
|
|
|
|
try {
|
|
const result = await rpc('/payment/poynt/send_to_terminal', {
|
|
reference: processingValues.reference,
|
|
terminal_id: parseInt(terminalId),
|
|
poynt_order_id: processingValues.poynt_order_id,
|
|
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_poynt_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="poynt_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/poynt/terminal_status', {
|
|
reference: processingValues.reference,
|
|
terminal_id: parseInt(terminalId),
|
|
});
|
|
|
|
const statusEl = document.getElementById('poynt_terminal_status');
|
|
|
|
if (result.status === 'CAPTURED' || 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') {
|
|
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,
|
|
);
|
|
}
|
|
},
|
|
|
|
});
|