frappe.ui.form.on("Sales Invoice", { refresh(frm) { frm.clear_custom_buttons(); // Only on submitted invoices if (frm.doc.docstatus !== 1) return; if (!frm.doc.customer) return; // Already paid if (frm.doc.outstanding_amount <= 0) { frm.dashboard.add_indicator("Paid", "green"); return; } frm.dashboard.add_indicator("Unpaid", "red"); if (frm.doc.outstanding_amount > 0 && frm.doc.docstatus === 1) { frm.add_custom_button("Run Payment", () => { run_payment_flow(frm); }, "Actions"); } } }); function run_payment_flow(frm) { frm.disable_save(); frappe.call({ method: "ns_app.api.payments.check_autopay", args: { customer: frm.doc.customer }, callback(r) { if (!r.message) { frm.enable_save(); return; } if (r.message.autopay_enabled && r.message.autopay_id) { run_autopay(frm); } else { open_manual_payment_form(frm); } } }); } function run_autopay(frm) { frappe.confirm( `Run AutoPay for ${format_currency(frm.doc.outstanding_amount)}?`, () => { // Change button to processing frm.remove_custom_button("Run Payment"); frm.add_custom_button("Processing...", () => {}, null).prop("disabled", true); frappe.call({ method: "ns_app.api.payments.run_autopay_payment", args: { invoice: frm.doc.name }, freeze: true, freeze_message: "Processing payment...", callback(r) { if (!r.message) { show_payment_failed(frm, "No response from payment processor"); return; } if (r.message.success) { // Success UI frm.remove_custom_button("Run Payment"); frm.add_custom_button("Paid ✓", () => {}) .prop("disabled", true); frappe.show_alert({ message: `Payment of ${format_currency(frm.doc.outstanding_amount)} received`, indicator: "green" }); frm.reload_doc(); } else { show_payment_failed(frm, r.message.error || "Payment declined"); } } }); }, () => {} ); } function show_payment_failed(frm, message) { // Remove processing button frm.remove_custom_button("Processing..."); // Add retry button frm.add_custom_button("Retry Payment", () => { run_payment_flow(frm); }); frappe.msgprint({ title: "Payment Failed", indicator: "red", message: message }); } function open_manual_payment_form(frm) { const uid = Date.now(); // unique per open const dialog = new frappe.ui.Dialog({ title: "Secure Payment", size: "large", fields: [ { fieldtype: "HTML", fieldname: "payment_form", options: `
` } ], // Destroy CollectJS instance if close button is clicked primary_action_label: "Close", primary_action() { dialog.hide(); } }); dialog.show(); // Destroy CollectJS Instance if closed with the X button dialog.$wrapper.on("hidden.bs.modal", function () { // Remove leftover backdrop (fix dark screen) document.querySelectorAll(".modal-backdrop").forEach(el => el.remove()); // Restore body scroll + click document.body.classList.remove("modal-open"); document.body.style.overflow = ""; // safely remove dialog dialog.$wrapper.remove(); // WClean CollectJS if (window.CollectJS) { try { delete window.CollectJS; } catch (e) {} } }); // Prefill setTimeout(() => { const nameEl = document.getElementById(`cardholder_name_${uid}`); const zipEl = document.getElementById(`billing_zip_${uid}`); if (nameEl) nameEl.value = frm.doc.customer_name || ""; if (zipEl) zipEl.value = frm.doc.billing_zip || frm.doc.pincode || ""; }, 50); // Load CollectJS once function loadCollectJS(callback) { // Remove existing script if it exists const existingScript = document.querySelector('script[src*="Collect.js"]'); if (existingScript) { existingScript.remove(); } // Reset global if (window.CollectJS) { delete window.CollectJS; } // Create fresh script every instance const script = document.createElement("script"); script.src = "https://secure.nmi.com/token/Collect.js"; script.setAttribute( "data-tokenization-key", "HKx4XR-G549wT-8bZ2YJ-3kbG28" ); script.onload = () => { console.log("CollectJS loaded fresh"); callback(); }; document.body.appendChild(script); } loadCollectJS(() => { console.log("CollectJS ready"); setTimeout(() => { CollectJS.configure({ variant: "inline", styleSniffer: true, fields: { ccnumber: { selector: `#cc_number_${uid}`, placeholder: "Card Number" }, ccexp: { selector: `#cc_exp_${uid}`, placeholder: "MM / YY" }, cvv: { selector: `#cc_cvv_${uid}`, placeholder: "CVV" } }, callback: function (response) { if (response.token) { // Get name and ZIP const enteredName = document.getElementById(`cardholder_name_${uid}`)?.value; const enteredZip = document.getElementById(`billing_zip_${uid}`)?.value; // Save name and ZIP const finalName = enteredName || frm.doc.customer_name; const finalZip = enteredZip || frm.doc.billing_zip || frm.doc.pincode; // Check autopay enrollment choice const AUTOPAY_ENABLED = false; const checkbox = document.getElementById(`save_autopay_${uid}`); const save_autopay = AUTOPAY_ENABLED ? checkbox?.checked : 0; run_token_payment(frm, response.token, dialog, { cardholder_name: finalName, billing_zip: finalZip, save_autopay: save_autopay }); } else { frappe.msgprint("Payment failed to tokenize"); } } }); const btn = document.getElementById(`pay_btn_${uid}`); if (!btn) { console.error("Pay button not found"); return; } btn.onclick = function () { frappe.show_alert({ message: "Processing payment...", indicator: "blue" }); CollectJS.startPaymentRequest(); }; }, 100); }); } function run_token_payment(frm, token, dialog, extra_data = {}) { const save_autopay = extra_data.save_autopay ? 1 : 0; frappe.call({ method: "ns_app.api.payments.run_token_payment", args: { invoice: frm.doc.name, token: token, cardholder_name: extra_data.cardholder_name, billing_zip: extra_data.billing_zip, save_autopay: save_autopay }, callback(r) { if (r.message?.success) { if (save_autopay && r.message.vault_id) { frappe.show_alert({ message: "Payment successful + AutoPay enabled", indicator: "green" }); } else { frappe.show_alert({ message: "Payment successful", indicator: "green" }); } dialog.hide(); frm.reload_doc(); } else { frappe.msgprint(r.message?.error || "Payment failed"); } } }); }