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 dialog = new frappe.ui.Dialog({ title: "Secure Payment", size: "large", fields: [ { fieldtype: "HTML", fieldname: "payment_form", options: `
` } ], primary_action_label: "Close", primary_action() { dialog.hide(); } }); dialog.show(); // Load NMI Collect.js 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"); CollectJS.configure({ variant: "inline", styleSniffer: true, fields: { ccnumber: { selector: "#cc_number", placeholder: "Card Number" }, ccexp: { selector: "#cc_exp", placeholder: "MM / YY" }, cvv: { selector: "#cc_cvv", placeholder: "CVV" } }, callback: function (response) { console.log("Token response:", response); if (response.token) { run_token_payment(frm, response.token, dialog); } else { frappe.msgprint("Payment failed to tokenize"); } } }); console.log("Fields:", document.querySelector("#cc_number")); setTimeout(() => { const btn = dialog.$wrapper.find("#pay_btn")[0]; if (!btn) { console.error("Pay button not found"); return; } btn.onclick = function () { console.log("Pay clicked"); frappe.show_alert({ message: "Processing payment...", indicator: "blue" }); CollectJS.startPaymentRequest(); }; }, 300); }; document.body.appendChild(script); } function run_token_payment(frm, token, dialog) { frappe.call({ method: "ns_app.api.payments.run_token_payment", args: { invoice: frm.doc.name, token: token }, callback(r) { if (r.message?.success) { dialog.hide(); frm.reload_doc(); frappe.show_alert({ message: "Payment successful", indicator: "green" }); } else { frappe.msgprint(r.message?.error || "Payment failed"); } } }); }