diff --git a/ns_app/api/payments.py b/ns_app/api/payments.py index 734706f..fe763f3 100644 --- a/ns_app/api/payments.py +++ b/ns_app/api/payments.py @@ -1,6 +1,7 @@ import frappe import requests import urllib.parse +import json from frappe import generate_hash from frappe.utils import nowdate @@ -8,760 +9,382 @@ from frappe.utils import nowdate @frappe.whitelist() def check_autopay(customer): - cust = frappe.get_doc("Customer", customer) - return { "autopay_enabled": bool(cust.custom_auto_pay_status), - "autopay_id": ( - cust.custom_auto_pay_id - if cust.custom_auto_pay_status - else None - ) + "autopay_id": cust.custom_auto_pay_id if cust.custom_auto_pay_status else None } +# ── NEW: fetch all unpaid invoices for a customer ──────────────────────────── + +@frappe.whitelist() +def get_unpaid_invoices(customer): + """Return all submitted, unpaid Sales Invoices for this customer.""" + invoices = frappe.get_all( + "Sales Invoice", + filters={ + "customer": customer, + "docstatus": 1, + "outstanding_amount": [">", 0] + }, + fields=["name", "posting_date", "customer_name", "outstanding_amount"], + order_by="posting_date asc" + ) + return invoices + + +# ── AutoPay (unchanged) ─────────────────────────────────────────────────────── + @frappe.whitelist() def run_autopay_payment(invoice): - - inv = frappe.get_doc("Sales Invoice", invoice) - + inv = frappe.get_doc("Sales Invoice", invoice) if inv.outstanding_amount <= 0: frappe.throw("Invoice is already fully paid") cust = frappe.get_doc("Customer", inv.customer) - - if ( - not cust.custom_auto_pay_status - or not cust.custom_auto_pay_id - ): - frappe.throw( - "Customer does not have AutoPay enabled" - ) + if not cust.custom_auto_pay_status or not cust.custom_auto_pay_id: + frappe.throw("Customer does not have AutoPay enabled") payload = { "autopay_id": cust.custom_auto_pay_id, - "amount": float(inv.outstanding_amount), - "invoice": inv.name + "amount": float(inv.outstanding_amount), + "invoice": inv.name } response = call_payment_api(payload) - if not response.get("success"): - - frappe.throw( - response.get("error", "Payment failed") - ) + frappe.throw(response.get("error", "Payment failed")) return { - "success": True, - "message": "AutoPay payment successful", + "success": True, + "message": "AutoPay payment successful", "transaction_id": response.get("transaction_id") } -def call_payment_api(payload): - - url = "https://crystalclear.transactiongateway.com/api/transact.php" - - api_username = frappe.conf.get("nmi_username") - api_password = frappe.conf.get("nmi_password") - - if not api_username or not api_password: - frappe.throw( - "Payment gateway credentials not configured" - ) - - invoice = payload["invoice"] - - order_id = ( - f"{invoice}-{generate_hash(length=6)}" - ) - - data = { - "username": api_username, - "password": api_password, - - "type": "sale", - - "customer_vault_id": payload["autopay_id"], - - "amount": payload["amount"], - - "orderid": order_id - } - - try: - - response = requests.post( - url, - data=data, - timeout=30 - ) - - response.raise_for_status() - - log_response = response.text[:120] - - frappe.logger("payments").info( - f""" -NMI AUTOPAY RESPONSE - -Invoice: {invoice} -Order ID: {order_id} -Amount: {payload['amount']} - -Response: -{log_response} -""" - ) - - if not response.text: - - frappe.throw( - "Payment processor returned empty response" - ) - - result = urllib.parse.parse_qs(response.text) - - success = result.get( - "response", - ["0"] - )[0] - - transaction_id = result.get( - "transactionid", - [""] - )[0] - - message = result.get( - "responsetext", - ["Payment failed"] - )[0] - - payment_type = result.get( - "type", - [""] - )[0] - - except Exception: - - frappe.log_error( - frappe.get_traceback(), - "NMI Payment API Error" - ) - - frappe.throw( - "Payment processor unreachable" - ) - - if success == "1": - - frappe.log_error( - f""" -PAYMENT SUCCESS FAILSAFE - -Invoice: {invoice} -Transaction ID: {transaction_id} -Amount: {payload['amount']} -""", - "PAYMENT SUCCESS FAILSAFE" - ) - - if payment_type == "check": - mode_of_payment = "ACH" - else: - mode_of_payment = "Credit Card" - - existing_pe = frappe.db.exists( - "Payment Entry", - {"reference_no": transaction_id} - ) - - if existing_pe: - - return { - "success": True, - "transaction_id": transaction_id, - "duplicate": True - } - - try: - - create_payment_entry( - invoice=invoice, - amount=payload["amount"], - transaction_id=transaction_id, - mode_of_payment=mode_of_payment - ) - - frappe.db.commit() - - except Exception: - - frappe.log_error( - frappe.get_traceback(), - "PAYMENT ENTRY FAILURE AFTER SUCCESSFUL CHARGE" - ) - - return { - "success": True, - "transaction_id": transaction_id - } - - return { - "success": False, - "error": message - } - +# ── Token payment — now accepts invoice_names list ─────────────────────────── @frappe.whitelist() def run_token_payment( invoice, token, + invoice_names=None, first_name=None, last_name=None, company=None, billing_zip=None, save_autopay=0 ): - if not token: + return {"success": False, "error": "Missing payment token"} - return { - "success": False, - "error": "Missing payment token" - } + # invoice_names arrives as a JSON string from frappe.call args + if isinstance(invoice_names, str): + try: + invoice_names = json.loads(invoice_names) + except Exception: + invoice_names = [invoice] - if not frappe.conf.get( - "enable_autopay_signup" - ): + if not invoice_names: + invoice_names = [invoice] + + if not frappe.conf.get("enable_autopay_signup"): save_autopay = 0 - save_autopay = int(save_autopay or 0) - inv = frappe.get_doc( - "Sales Invoice", - invoice - ) + # Load all selected invoices and validate + invoices = [] + total_amount = 0 - customer = frappe.get_doc( - "Customer", - inv.customer - ) + for inv_name in invoice_names: + inv = frappe.get_doc("Sales Invoice", inv_name) + if inv.docstatus != 1: + frappe.throw(f"Invoice {inv_name} is not submitted") + if inv.outstanding_amount <= 0: + frappe.throw(f"Invoice {inv_name} is already fully paid") + invoices.append(inv) + total_amount += inv.outstanding_amount + + # Use customer from the primary invoice + primary_inv = frappe.get_doc("Sales Invoice", invoice) + customer = frappe.get_doc("Customer", primary_inv.customer) + + first_name = (first_name or customer.customer_name or "Customer").strip() + last_name = (last_name or ".").strip() + company = (company or "").strip() + billing_zip = (billing_zip or customer.get("billing_zip") or customer.get("pincode") or "") + + # Use a combined order ID referencing all invoices + inv_label = invoice if len(invoices) == 1 else f"{invoice}+{len(invoices)-1}more" + order_id = f"{inv_label}-{generate_hash(length=6)}" url = "https://secure.nmi.com/api/transact.php" - first_name = ( - first_name - or customer.customer_name - or "Customer" - ).strip() - - last_name = ( - last_name - or "." - ).strip() - - company = ( - company or "" - ).strip() - - billing_zip = ( - billing_zip - or customer.get("billing_zip") - or customer.get("pincode") - or "" - ) - - order_id = ( - f"{inv.name}-{generate_hash(length=6)}" - ) - sale_data = { - - "security_key": frappe.conf.get( - "nmi_security_key" - ), - - "type": "sale", - + "security_key": frappe.conf.get("nmi_security_key"), + "type": "sale", "payment_token": token, - - "amount": inv.outstanding_amount, - - "orderid": order_id, - - "first_name": first_name, - "last_name": last_name, - "company": company, - - "email": inv.contact_email or "", - - "zip": billing_zip, + "amount": total_amount, + "orderid": order_id, + "first_name": first_name, + "last_name": last_name, + "company": company, + "email": primary_inv.contact_email or "", + "zip": billing_zip, } - # Save to vault DURING sale transaction if save_autopay: - - sale_data["customer_vault"] = ( - "add_customer" - ) - - sale_data["customer_vault_id"] = ( - customer.name.upper() - ) + sale_data["customer_vault"] = "add_customer" + sale_data["customer_vault_id"] = customer.name.upper() frappe.log_error( - f""" -Sending SALE request - -invoice={inv.name} -order_id={order_id} - -customer={customer.name} - -save_autopay={save_autopay} - -amount={inv.outstanding_amount} -""", - "AUTOPAY DEBUG - SALE REQUEST" + f"invoice_names={invoice_names}\ntotal={total_amount}\norder_id={order_id}\nsave_autopay={save_autopay}", + "PAYMENT DEBUG - SALE REQUEST" ) try: - - sale_response = requests.post( - url, - data=sale_data, - timeout=30 - ) - - sale_result = urllib.parse.parse_qs( - sale_response.text - ) - - log_response = sale_response.text[:120] - - frappe.logger("payments").info( - f""" -NMI SALE RESPONSE - -Invoice: {inv.name} -Order ID: {order_id} - -Response: -{log_response} -""" - ) - - frappe.log_error( - log_response, - "AUTOPAY DEBUG - SALE RESPONSE" - ) - + sale_response = requests.post(url, data=sale_data, timeout=30) + sale_result = urllib.parse.parse_qs(sale_response.text) + frappe.log_error(sale_response.text[:120], "PAYMENT DEBUG - SALE RESPONSE") except Exception: + frappe.log_error(frappe.get_traceback(), "PAYMENT DEBUG - SALE EXCEPTION") + return {"success": False, "error": "Payment request failed"} - frappe.log_error( - frappe.get_traceback(), - "AUTOPAY DEBUG - SALE EXCEPTION" - ) - - return { - "success": False, - "error": "Payment request failed" - } - - success = sale_result.get( - "response", - ["0"] - )[0] - - transaction_id = sale_result.get( - "transactionid", - [""] - )[0] - - vault_id = sale_result.get( - "customer_vault_id", - [""] - )[0] + success = sale_result.get("response", ["0"])[0] + transaction_id = sale_result.get("transactionid", [""])[0] + vault_id = sale_result.get("customer_vault_id", [""])[0] if success != "1": - - frappe.log_error( - sale_response.text[:120], - "AUTOPAY DEBUG - SALE FAILED" - ) - + frappe.log_error(sale_response.text[:120], "PAYMENT DEBUG - SALE FAILED") return { "success": False, - "error": sale_result.get( - "responsetext", - ["Error"] - )[0] + "error": sale_result.get("responsetext", ["Error"])[0] } frappe.log_error( - f""" -PAYMENT SUCCESS FAILSAFE - -Invoice: {invoice} -Transaction ID: {transaction_id} -Amount: {inv.outstanding_amount} -""", - "PAYMENT SUCCESS FAILSAFE" + f"transaction_id={transaction_id}\ntotal={total_amount}\ninvoices={invoice_names}", + "PAYMENT SUCCESS" ) - existing_pe = frappe.db.exists( - "Payment Entry", - {"reference_no": transaction_id} - ) + # Duplicate check on transaction ID + if frappe.db.exists("Payment Entry", {"reference_no": transaction_id}): + return {"success": True, "transaction_id": transaction_id, "duplicate": True} - if not existing_pe: + try: + create_payment_entry( + invoices=invoices, + transaction_id=transaction_id, + mode_of_payment="Credit Card" + ) + frappe.db.commit() + except Exception: + frappe.log_error( + frappe.get_traceback(), + "PAYMENT ENTRY FAILURE AFTER SUCCESSFUL CHARGE" + ) - try: - - create_payment_entry( - invoice=invoice, - amount=inv.outstanding_amount, - transaction_id=transaction_id, - mode_of_payment="Credit Card" - ) - - frappe.db.commit() - - except Exception: - - frappe.log_error( - frappe.get_traceback(), - "PAYMENT ENTRY FAILURE AFTER SUCCESSFUL CHARGE" - ) - - # Save AutoPay info locally + # Save AutoPay info if save_autopay and vault_id: - try: - - customer.custom_auto_pay_id = ( - vault_id - ) - - customer.custom_auto_pay_status = 1 - - customer.custom_auto_pay_first_name = ( - first_name - ) - - customer.custom_auto_pay_last_name = ( - last_name - ) - - customer.custom_auto_pay_company = ( - company - ) - - customer.custom_auto_pay_zip = ( - billing_zip - ) - - customer.save( - ignore_permissions=True - ) - + customer.custom_auto_pay_id = vault_id + customer.custom_auto_pay_status = 1 + customer.custom_auto_pay_first_name = first_name + customer.custom_auto_pay_last_name = last_name + customer.custom_auto_pay_company = company + customer.custom_auto_pay_zip = billing_zip + customer.save(ignore_permissions=True) frappe.db.commit() - - frappe.log_error( - f""" -Vault save complete - -customer={customer.name} - -vault_id={vault_id} -""", - "AUTOPAY DEBUG - SAVE COMPLETE" - ) - except Exception: + frappe.log_error(frappe.get_traceback(), "AUTOPAY CUSTOMER SAVE FAILED") - frappe.log_error( - frappe.get_traceback(), - "AUTOPAY CUSTOMER SAVE FAILED" - ) - - return { - "success": True, - "transaction_id": transaction_id, - "vault_id": vault_id - } + return {"success": True, "transaction_id": transaction_id, "vault_id": vault_id} -@frappe.whitelist() -def save_to_autopay( - customer, - token, - first_name=None, - last_name=None, - company=None, - billing_zip=None -): +# ── Payment Entry — now takes a list of invoices ───────────────────────────── - if not token: +def create_payment_entry(invoices, transaction_id=None, mode_of_payment=None): + """ + Create a single Payment Entry covering one or more invoices. + `invoices` is a list of Sales Invoice document objects. + """ + if not invoices: + return - return { - "success": False, - "error": "Missing payment token" - } + if transaction_id and frappe.db.exists("Payment Entry", {"reference_no": transaction_id}): + return - cust = frappe.get_doc( - "Customer", - customer + primary_inv = invoices[0] + total_amount = sum(inv.outstanding_amount for inv in invoices) + + paid_to = ( + "ENB Bank Account - NIL" + if mode_of_payment in ["ACH", "Credit Card"] + else frappe.db.get_value("Company", primary_inv.company, "default_cash_account") ) - first_name = ( - first_name - or cust.customer_name - or "Customer" - ).strip() + if not paid_to: + frappe.throw("No receiving account configured") - last_name = ( - last_name - or "." - ).strip() + pe = frappe.new_doc("Payment Entry") + pe.payment_type = "Receive" + pe.party_type = "Customer" + pe.party = primary_inv.customer + pe.posting_date = nowdate() + pe.mode_of_payment = mode_of_payment or "Credit Card" + pe.paid_amount = total_amount + pe.received_amount = total_amount + pe.paid_to = paid_to + pe.reference_no = transaction_id + pe.reference_date = nowdate() - company = ( - company or "" - ).strip() + for inv in invoices: + pe.append("references", { + "reference_doctype": "Sales Invoice", + "reference_name": inv.name, + "allocated_amount": inv.outstanding_amount + }) - billing_zip = ( - billing_zip - or cust.get("billing_zip") - or cust.get("pincode") - or "" - ) + pe.insert(ignore_permissions=True) + pe.submit() + + +# ── AutoPay via vault (unchanged) ──────────────────────────────────────────── + +def call_payment_api(payload): + url = "https://secure.nmi.com/api/transact.php" + security_key = frappe.conf.get("nmi_security_key") + + if not security_key: + frappe.throw("Payment gateway credentials not configured") + + invoice = payload["invoice"] + order_id = f"{invoice}-{generate_hash(length=6)}" data = { - - "security_key": frappe.conf.get( - "nmi_security_key" - ), - - "type": "add_customer", - - "payment_token": token, - - "customer_vault": "add_customer", - - "customer_vault_id": cust.name.upper(), - - "first_name": first_name, - "last_name": last_name, - "company": company, - - "zip": billing_zip, + "security_key": security_key, + "type": "sale", + "customer_vault_id": payload["autopay_id"], + "amount": payload["amount"], + "orderid": order_id } try: - - response = requests.post( - "https://secure.nmi.com/api/transact.php", - data=data, - timeout=30 + response = requests.post(url, data=data, timeout=30) + frappe.logger("payments").info( + f"NMI AUTOPAY | Invoice: {invoice} | Order: {order_id} | {response.text[:120]}" ) + if not response.text: + frappe.throw("Payment processor returned empty response") - result = urllib.parse.parse_qs( - response.text - ) - - success = result.get( - "response", - ["0"] - )[0] - - returned_vault_id = result.get( - "customer_vault_id", - [cust.name.upper()] - )[0] - - message = result.get( - "responsetext", - ["Failed"] - )[0] + result = urllib.parse.parse_qs(response.text) + success = result.get("response", ["0"])[0] + transaction_id = result.get("transactionid", [""])[0] + message = result.get("responsetext", ["Payment failed"])[0] + payment_type = result.get("type", [""])[0] except Exception: + frappe.log_error(frappe.get_traceback(), "NMI Payment API Error") + frappe.throw("Payment processor unreachable") - frappe.log_error( - frappe.get_traceback(), - "NMI Vault Error" - ) + if success == "1": + mode_of_payment = "ACH" if payment_type == "check" else "Credit Card" - return { - "success": False, - "error": "Vault request failed" - } - - if ( - success == "1" - or "duplicate" in message.lower() - ): + if frappe.db.exists("Payment Entry", {"reference_no": transaction_id}): + return {"success": True, "transaction_id": transaction_id, "duplicate": True} try: - - cust.custom_auto_pay_id = ( - returned_vault_id + inv = frappe.get_doc("Sales Invoice", invoice) + create_payment_entry( + invoices=[inv], + transaction_id=transaction_id, + mode_of_payment=mode_of_payment ) - - cust.custom_auto_pay_status = 1 - - cust.custom_auto_pay_first_name = ( - first_name - ) - - cust.custom_auto_pay_last_name = ( - last_name - ) - - cust.custom_auto_pay_company = ( - company - ) - - cust.custom_auto_pay_zip = ( - billing_zip - ) - - cust.save(ignore_permissions=True) - frappe.db.commit() - except Exception: + frappe.log_error(frappe.get_traceback(), "PAYMENT ENTRY FAILURE AFTER SUCCESSFUL CHARGE") - frappe.log_error( - frappe.get_traceback(), - "AUTOPAY CUSTOMER SAVE FAILED" - ) + return {"success": True, "transaction_id": transaction_id} - return { - "success": True, - "vault_id": returned_vault_id - } + return {"success": False, "error": message} - return { - "success": False, - "error": message - } +# ── Webhook (unchanged except uses new create_payment_entry signature) ──────── @frappe.whitelist(allow_guest=True) def crystalclear_webhook(): - - data = frappe.local.form_dict - + data = frappe.local.form_dict if data.get("response") != "1": return "ignored" - invoice = data.get("orderid") - amount = data.get("amount") + invoice = data.get("orderid") + amount = data.get("amount") transaction_id = data.get("transactionid") - payment_type = data.get("type") - - if payment_type == "check": - mode_of_payment = "ACH" - else: - mode_of_payment = "Credit Card" + payment_type = data.get("type") + mode_of_payment = "ACH" if payment_type == "check" else "Credit Card" try: - + inv = frappe.get_doc("Sales Invoice", invoice) create_payment_entry( - invoice=invoice, - amount=amount, + invoices=[inv], transaction_id=transaction_id, mode_of_payment=mode_of_payment ) - frappe.db.commit() - except Exception: - - frappe.log_error( - frappe.get_traceback(), - "WEBHOOK PAYMENT ENTRY FAILURE" - ) + frappe.log_error(frappe.get_traceback(), "WEBHOOK PAYMENT ENTRY FAILURE") return "ok" -def create_payment_entry( - invoice, - amount, - transaction_id=None, - mode_of_payment=None -): +# ── save_to_autopay (unchanged) ─────────────────────────────────────────────── - if ( - transaction_id - and frappe.db.exists( - "Payment Entry", - {"reference_no": transaction_id} - ) - ): - return +@frappe.whitelist() +def save_to_autopay(customer, token, first_name=None, last_name=None, company=None, billing_zip=None): + if not token: + return {"success": False, "error": "Missing payment token"} - inv = frappe.get_doc( - "Sales Invoice", - invoice - ) + cust = frappe.get_doc("Customer", customer) + first_name = (first_name or cust.customer_name or "Customer").strip() + last_name = (last_name or ".").strip() + company = (company or "").strip() + billing_zip = (billing_zip or cust.get("billing_zip") or cust.get("pincode") or "") - if mode_of_payment in [ - "ACH", - "Credit Card" - ]: + data = { + "security_key": frappe.conf.get("nmi_security_key"), + "type": "add_customer", + "payment_token": token, + "customer_vault": "add_customer", + "customer_vault_id": cust.name.upper(), + "first_name": first_name, + "last_name": last_name, + "company": company, + "zip": billing_zip, + } - paid_to = "ENB Bank Account - NIL" + try: + response = requests.post("https://secure.nmi.com/api/transact.php", data=data, timeout=30) + result = urllib.parse.parse_qs(response.text) + success = result.get("response", ["0"])[0] + returned_vault_id = result.get("customer_vault_id", [cust.name.upper()])[0] + message = result.get("responsetext", ["Failed"])[0] + except Exception: + frappe.log_error(frappe.get_traceback(), "NMI Vault Error") + return {"success": False, "error": "Vault request failed"} - else: + if success == "1" or "duplicate" in message.lower(): + try: + cust.custom_auto_pay_id = returned_vault_id + cust.custom_auto_pay_status = 1 + cust.custom_auto_pay_first_name = first_name + cust.custom_auto_pay_last_name = last_name + cust.custom_auto_pay_company = company + cust.custom_auto_pay_zip = billing_zip + cust.save(ignore_permissions=True) + frappe.db.commit() + except Exception: + frappe.log_error(frappe.get_traceback(), "AUTOPAY CUSTOMER SAVE FAILED") - paid_to = frappe.db.get_value( - "Company", - inv.company, - "default_cash_account" - ) + return {"success": True, "vault_id": returned_vault_id} - if not paid_to: - - frappe.throw( - "No receiving account configured" - ) - - pe = frappe.new_doc("Payment Entry") - - pe.payment_type = "Receive" - - pe.party_type = "Customer" - pe.party = inv.customer - - pe.posting_date = nowdate() - - pe.mode_of_payment = ( - mode_of_payment - or "Credit Card" - ) - - pe.paid_amount = amount - pe.received_amount = amount - - pe.paid_to = paid_to - - pe.reference_no = transaction_id - pe.reference_date = nowdate() - - pe.append( - "references", - { - "reference_doctype": "Sales Invoice", - "reference_name": invoice, - "allocated_amount": amount - } - ) - - pe.insert(ignore_permissions=True) - - pe.submit() \ No newline at end of file + return {"success": False, "error": message} \ No newline at end of file diff --git a/ns_app/public/js/sales_invoice.js b/ns_app/public/js/sales_invoice.js index 0b9c6ed..662010d 100644 --- a/ns_app/public/js/sales_invoice.js +++ b/ns_app/public/js/sales_invoice.js @@ -2,11 +2,9 @@ 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; @@ -14,45 +12,27 @@ frappe.ui.form.on("Sales Invoice", { 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"); - } + 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 - }, - + args: { customer: frm.doc.customer }, callback(r) { - frm.enable_save(); + if (!r.message) return; - if (!r.message) { - return; - } - - if ( - r.message.autopay_enabled - && r.message.autopay_id - ) { - + if (r.message.autopay_enabled && r.message.autopay_id) { run_autopay(frm); - } else { - open_manual_payment_form(frm); - } } }); @@ -60,597 +40,368 @@ function run_payment_flow(frm) { function run_autopay(frm) { - frappe.confirm( `Run AutoPay for ${format_currency(frm.doc.outstanding_amount)}?`, - () => { - frm.remove_custom_button("Run Payment"); - - frm.add_custom_button( - "Processing...", - () => {}, - null - ).prop("disabled", true); + frm.add_custom_button("Processing...", () => {}, null).prop("disabled", true); frappe.call({ method: "ns_app.api.payments.run_autopay_payment", - - args: { - invoice: frm.doc.name - }, - + 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" - ); - + show_payment_failed(frm, "No response from payment processor"); return; } - if (r.message.success) { - - frm.remove_custom_button( - "Run Payment" - ); - - frm.add_custom_button( - "Paid ✓", - () => {} - ).prop("disabled", true); - + 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`, + 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" - ); - + show_payment_failed(frm, r.message.error || "Payment declined"); } } }); - }, - () => {} ); } function show_payment_failed(frm, message) { - frm.remove_custom_button("Processing..."); - - frm.add_custom_button( - "Retry Payment", - () => { - run_payment_flow(frm); - } - ); - - frappe.msgprint({ - title: "Payment Failed", - indicator: "red", - message: message - }); - + frm.add_custom_button("Retry Payment", () => run_payment_flow(frm)); + frappe.msgprint({ title: "Payment Failed", indicator: "red", message }); } function open_manual_payment_form(frm) { - const uid = Date.now(); - window.ns_payment_processing = false; + let all_invoices = []; + let selected = new Set(); + const dialog = new frappe.ui.Dialog({ title: "Secure Payment", - size: "large", - fields: [ { fieldtype: "HTML", - fieldname: "payment_form", - options: ` -