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 get_collect_checkout_url(invoice): inv = frappe.get_doc("Sales Invoice", invoice) return ( "https://crystalclear.transactiongateway.com/collect/checkout" f"?amount={inv.outstanding_amount}&reference={inv.name}" ) @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