/** @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 = `
Loading...
${_t("Waiting for terminal payment...")}

${_t("Please complete the payment on the terminal device.")}

${_t("Checking status...")}

`; } 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, ); } }, });