diff --git a/ns_app/api/payments.py b/ns_app/api/payments.py new file mode 100644 index 0000000..a8d5740 --- /dev/null +++ b/ns_app/api/payments.py @@ -0,0 +1,119 @@ +import requests +import frappe + + +# Checks to see if customer has Autopay on +@frappe.whitelist() +def check_autopay(customer): + cust = frappe.get_doc("Customer", customer) + + return { + "autopay_enabled": bool(cust.auto_pay), + "autopay_id": cust.auto_pay_id if cust.auto_pay else None + } + + + +# Creates payload to send through API, checks for success, then makes a call to create a payment entry. +@frappe.whitelist() +def run_autopay_payment(invoice, autopay_id, amount): + + payload = { + "autopay_id": autopay_id, + "amount": amount, + "invoice": invoice + } + + response = call_payment_api(payload) + + # Handle gateway failure clearly + if not response.get("success"): + frappe.throw(response.get("error", "Payment failed")) + + # Create ERPNext payment record + payment_entry = create_payment_entry( + invoice=invoice, + amount=amount, + transaction_id=response.get("transaction_id") + ) + + return { + "message": "AutoPay payment successful", + "payment_entry": payment_entry + } + + + +# Call's Crystal CLear's API and Runs an AutoPay transaction using NMI / Crystal Clear using customer_vault_id (autopay_id) + +def call_payment_api(payload): + + url = "https://crystalclear.transactiongateway.com/api/transact.php" + + # Store these in site_config.json or environment variables + 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") + + data = { + "username": api_username, + "password": api_password, + "type": "sale", + "customer_vault_id": payload["autopay_id"], + "amount": payload["amount"], + "orderid": payload["invoice"], + "response": "json" + } + + try: + response = requests.post(url, data=data, timeout=30) + response.raise_for_status() + result = response.json() + + except Exception as e: + frappe.log_error(frappe.get_traceback(), "NMI Payment API Error") + frappe.throw("Payment processor unreachable") + + # NMI success condition + if result.get("response") == "1": + return { + "success": True, + "transaction_id": result.get("transactionid") + } + + # Failure case + return { + "success": False, + "error": result.get("responsetext", "Payment failed") + } + + + +# Auto creates payment entry in ERP Next +def create_payment_entry(invoice, amount, transaction_id=None): + inv = frappe.get_doc("Sales Invoice", invoice) + + pe = frappe.new_doc("Payment Entry") + pe.payment_type = "Receive" + pe.party_type = "Customer" + pe.party = inv.customer + pe.paid_amount = amount + pe.received_amount = amount + pe.reference_no = transaction_id + pe.reference_date = frappe.utils.nowdate() + + pe.append("references", { + "reference_doctype": "Sales Invoice", + "reference_name": invoice, + "allocated_amount": amount + }) + + pe.insert() + pe.submit() + + return pe.name + + + diff --git a/ns_app/hooks.py b/ns_app/hooks.py index 3cfa4f4..d713671 100644 --- a/ns_app/hooks.py +++ b/ns_app/hooks.py @@ -7,4 +7,5 @@ app_license = "MIT" app_include_js = [ "/assets/ns_app/js/custom.js" + "/assets/ns_app/js/sales_invoice.js" ] diff --git a/ns_app/public/js/sales_invoice.js b/ns_app/public/js/sales_invoice.js new file mode 100644 index 0000000..29c7749 --- /dev/null +++ b/ns_app/public/js/sales_invoice.js @@ -0,0 +1,68 @@ +frappe.ui.form.on("Sales Invoice", { + refresh(frm) { + if (frm.doc.docstatus !== 1) return; // submitted only + + frm.add_custom_button("Run Payment", () => { + run_payment_flow(frm); + }); + } +}); + +function run_payment_flow(frm) { + frappe.call({ + method: "ns_app.api.payments.check_autopay", + args: { + customer: frm.doc.customer + }, + callback(r) { + if (!r.message) return; + + if (r.message.autopay_enabled) { + run_autopay(frm, r.message.autopay_id); + } else { + open_manual_payment_form(frm); + } + } + }); + + +function run_autopay(frm, autopay_id) { + frappe.confirm( + `Run AutoPay for $${frm.doc.grand_total}?`, + () => { + frappe.call({ + method: "ns_app.api.payments.run_autopay_payment", + args: { + invoice: frm.doc.name, + autopay_id: autopay_id, + amount: frm.doc.grand_total + }, + callback(r) { + frappe.msgprint(r.message || "Payment processed"); + frm.reload_doc(); + } + }); + } + ); +} + + +function open_manual_payment_form(frm) { + const dialog = new frappe.ui.Dialog({ + title: "Enter Payment", + fields: [ + { + fieldtype: "HTML", + fieldname: "payment_form", + options: `` + } + ] + }); + + dialog.show(); +} + +}