import frappe import requests import urllib.parse from frappe import generate_hash 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") ) return { "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": 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 } 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, first_name=None, last_name=None, company=None, billing_zip=None, save_autopay=0 ): if not token: return { "success": False, "error": "Missing payment token" } 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 ) customer = frappe.get_doc( "Customer", inv.customer ) 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", "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, } # Save to vault DURING sale transaction if save_autopay: 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" ) 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" ) except Exception: 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] if success != "1": frappe.log_error( sale_response.text[:120], "AUTOPAY DEBUG - SALE FAILED" ) return { "success": False, "error": sale_result.get( "responsetext", ["Error"] )[0] } existing_pe = frappe.db.exists( "Payment Entry", {"reference_no": transaction_id} ) if not existing_pe: create_payment_entry( invoice=invoice, amount=inv.outstanding_amount, transaction_id=transaction_id, mode_of_payment="Credit Card" ) # Save AutoPay info locally if save_autopay and vault_id: 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" ) 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 ): if not token: return { "success": False, "error": "Missing payment token" } 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 "" ) 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, } 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" } if ( success == "1" or "duplicate" in message.lower() ): 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() return { "success": True, "vault_id": returned_vault_id } 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 ): if ( transaction_id and frappe.db.exists( "Payment Entry", {"reference_no": transaction_id} ) ): return inv = frappe.get_doc( "Sales Invoice", invoice ) 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()