import frappe import requests import urllib.parse 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 } @frappe.whitelist() def run_autopay_payment(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") payload = { "autopay_id": cust.custom_auto_pay_id, "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")) payment_entry = create_payment_entry( invoice=inv.name, amount=payload["amount"], transaction_id=response.get("transaction_id") ) return { "message": "AutoPay payment successful", "payment_entry": payment_entry } 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"] data = { "username": api_username, "password": api_password, "type": "sale", "customer_vault_id": payload["autopay_id"], "amount": payload["amount"], "orderid": invoice } try: response = requests.post(url, data=data, timeout=30) response.raise_for_status() frappe.logger("payments").info( f"NMI PAYMENT | Invoice: {invoice} | Amount: {payload['amount']} | Response: {response.text}" ) if not response.text: frappe.throw("Payment processor returned empty response") # Parse gateway 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": # Detect payment mode if payment_type == "check": mode_of_payment = "ACH" else: mode_of_payment = "Credit Card" create_payment_entry( invoice=invoice, amount=payload["amount"], transaction_id=transaction_id, mode_of_payment=mode_of_payment ) return { "success": True, "transaction_id": transaction_id } return { "success": False, "error": message } @frappe.whitelist() def run_token_payment(invoice, token, cardholder_name=None, billing_zip=None): inv = frappe.get_doc("Sales Invoice", invoice) url = "https://secure.nmi.com/api/transact.php" # Fallback logic customer_name = ( cardholder_name or inv.customer_name or inv.customer or "Customer" ) billing_zip = ( billing_zip or inv.get("billing_zip") or inv.get("pincode") or "" ) # Name Split parts = customer_name.strip().split(" ", 1) first_name = parts[0] last_name = parts[1] if len(parts) > 1 else "." data = { "security_key": frappe.conf.get("nmi_security_key"), "type": "sale", "payment_token": token, "amount": inv.outstanding_amount, "orderid": inv.name, "first_name": first_name, "last_name": last_name, "email": inv.contact_email or "", "zip": billing_zip, } response = requests.post(url, data=data) result = urllib.parse.parse_qs(response.text) frappe.logger("payments").info(f"NMI RESPONSE: {response.text}") success = result.get("response", ["0"])[0] transaction_id = result.get("transactionid", [""])[0] if success == "1": create_payment_entry( invoice=invoice, amount=inv.outstanding_amount, transaction_id=transaction_id, mode_of_payment="Credit Card" ) return {"success": True} return { "success": False, "error": result.get("responsetext", ["Error"])[0] } @frappe.whitelist() def save_to_autopay(customer, token, cardholder_name=None, billing_zip=None): import requests import urllib.parse inv_customer = frappe.get_doc("Customer", customer) # Fallback logic customer_name = ( cardholder_name or inv_customer.customer_name or "Customer" ).strip() billing_zip = ( billing_zip or inv_customer.get("billing_zip") or inv_customer.get("pincode") or "" ) # name split if " " in customer_name: first_name, last_name = customer_name.split(" ", 1) else: first_name = customer_name last_name = "." data = { "security_key": frappe.conf.get("nmi_security_key"), "type": "add_customer", "payment_token": token, "first_name": first_name, "last_name": last_name, "zip": billing_zip, "customer_vault": "add_customer" } try: response = requests.post( "https://secure.nmi.com/api/transact.php", data=data, timeout=30 ) result = urllib.parse.parse_qs(response.text) frappe.logger("payments").info(f"NMI VAULT RESPONSE: {response.text}") success = result.get("response", ["0"])[0] vault_id = result.get("customer_vault_id", [""])[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"} if success == "1" and vault_id: # Save to customer inv_customer.custom_auto_pay_id = vault_id inv_customer.custom_auto_pay_status = 1 inv_customer.save(ignore_permissions=True) return {"success": True} return {"success": False, "error": message} @frappe.whitelist(allow_guest=True) def crystalclear_webhook(): data = frappe.local.form_dict if data.get("response") != "1": return "ignored" 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" create_payment_entry( invoice=invoice, amount=amount, transaction_id=transaction_id, mode_of_payment=mode_of_payment ) return "ok" def create_payment_entry(invoice, amount, transaction_id=None, mode_of_payment=None): # Prevent duplicates if transaction_id and frappe.db.exists( "Payment Entry", {"reference_no": transaction_id} ): return inv = frappe.get_doc("Sales Invoice", invoice) # Account logic if mode_of_payment in ["ACH", "Credit Card"]: paid_to = "ENB Bank Account - NIL" else: paid_to = frappe.db.get_value( "Company", inv.company, "default_cash_account" ) 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() return pe.name