Changed the opt in for auto pay to be a check box on thecustom form. Improves work flow and is more professional. Added this as checkbox under the zip code entry field. Did no testing on the logic yet.

This commit is contained in:
Ty Reynolds
2026-04-08 08:02:42 -04:00
parent d082c59bc4
commit cc44683972
2 changed files with 130 additions and 78 deletions

View File

@@ -118,8 +118,10 @@ def call_payment_api(payload):
@frappe.whitelist()
def run_token_payment(invoice, token, cardholder_name=None, billing_zip=None):
def run_token_payment(invoice, token, cardholder_name=None, billing_zip=None, save_autopay=0):
inv = frappe.get_doc("Sales Invoice", invoice)
customer = frappe.get_doc("Customer", inv.customer)
url = "https://secure.nmi.com/api/transact.php"
@@ -138,12 +140,13 @@ def run_token_payment(invoice, token, cardholder_name=None, billing_zip=None):
or ""
)
# Name Split
# Name split
parts = customer_name.strip().split(" ", 1)
first_name = parts[0]
last_name = parts[1] if len(parts) > 1 else "."
data = {
sale_data = {
"security_key": frappe.conf.get("nmi_security_key"),
"type": "sale",
"payment_token": token,
@@ -153,72 +156,110 @@ def run_token_payment(invoice, token, cardholder_name=None, billing_zip=None):
"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)
sale_response = requests.post(url, data=sale_data)
sale_result = urllib.parse.parse_qs(sale_response.text)
frappe.logger("payments").info(f"NMI RESPONSE: {response.text}")
frappe.logger("payments").info(f"NMI SALE RESPONSE: {sale_response.text}")
success = result.get("response", ["0"])[0]
transaction_id = result.get("transactionid", [""])[0]
success = sale_result.get("response", ["0"])[0]
transaction_id = sale_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"
)
if success != "1":
return {
"success": False,
"error": sale_result.get("responsetext", ["Error"])[0]
}
return {"success": True}
# Create payment entry
create_payment_entry(
invoice=invoice,
amount=inv.outstanding_amount,
transaction_id=transaction_id,
mode_of_payment="Credit Card"
)
vault_id = None
# Vault (if checked)
if cint(save_autopay):
vault_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:
vault_response = requests.post(url, data=vault_data, timeout=30)
vault_result = urllib.parse.parse_qs(vault_response.text)
frappe.logger("payments").info(f"NMI VAULT RESPONSE: {vault_response.text}")
vault_success = vault_result.get("response", ["0"])[0]
vault_id = vault_result.get("customer_vault_id", [""])[0]
if vault_success == "1" and vault_id:
customer.custom_auto_pay_id = vault_id
customer.custom_auto_pay_status = 1
customer.save(ignore_permissions=True)
except Exception:
frappe.log_error(frappe.get_traceback(), "Vault Creation Failed")
return {
"success": False,
"error": result.get("responsetext", ["Error"])[0]
"success": True,
"vault_id": vault_id
}
@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)
cust = frappe.get_doc("Customer", customer)
# Fallback logic
customer_name = (
# Priority: Form input → Customer record → fallback
final_name = (
cardholder_name
or inv_customer.customer_name
or cust.customer_name
or cust.name
or "Customer"
).strip()
billing_zip = (
# ZIP fallback chain
final_zip = (
billing_zip
or inv_customer.get("billing_zip")
or inv_customer.get("pincode")
or cust.get("billing_zip")
or cust.get("pincode")
or ""
)
# name split
if " " in customer_name:
first_name, last_name = customer_name.split(" ", 1)
else:
first_name = customer_name
last_name = "."
# Optional but recommended fields
email = cust.get("email_id") or ""
# Safe name split
parts = final_name.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": "add_customer",
"payment_token": token,
"customer_vault": "add_customer",
"first_name": first_name,
"last_name": last_name,
"email": email,
"zip": billing_zip,
"customer_vault": "add_customer"
"zip": final_zip,
}
try:
@@ -230,7 +271,10 @@ def save_to_autopay(customer, token, cardholder_name=None, billing_zip=None):
result = urllib.parse.parse_qs(response.text)
frappe.logger("payments").info(f"NMI VAULT RESPONSE: {response.text}")
frappe.logger("payments").info({
"vault_request": data,
"vault_response": response.text
})
success = result.get("response", ["0"])[0]
vault_id = result.get("customer_vault_id", [""])[0]
@@ -241,14 +285,25 @@ def save_to_autopay(customer, token, cardholder_name=None, billing_zip=None):
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}
# Save vault ID to ERP Customer
cust.custom_auto_pay_id = vault_id
cust.custom_auto_pay_status = 1
return {"success": False, "error": message}
cust.custom_auto_pay_name = final_name
cust.custom_auto_pay_zip = final_zip
cust.save(ignore_permissions=True)
return {
"success": True,
"vault_id": vault_id
}
return {
"success": False,
"error": message
}
@frappe.whitelist(allow_guest=True)

View File

@@ -141,6 +141,13 @@ function open_manual_payment_form(frm) {
<input type="text" id="billing_zip_${uid}" class="form-control"/>
</div>
<div class="mt-3">
<label>
<input type="checkbox" id="save_autopay_${uid}" />
Save for Auto Pay
</label>
</div>
<div id="cc_number_${uid}" class="mt-3"></div>
<div id="cc_exp_${uid}" class="mt-2"></div>
<div id="cc_cvv_${uid}" class="mt-2"></div>
@@ -254,15 +261,22 @@ function open_manual_payment_form(frm) {
callback: function (response) {
if (response.token) {
// Get name and ZIP
const enteredName = document.getElementById(`cardholder_name_${uid}`)?.value;
const enteredZip = document.getElementById(`billing_zip_${uid}`)?.value;
// Save name and ZIP
const finalName = enteredName || frm.doc.customer_name;
const finalZip = enteredZip || frm.doc.billing_zip || frm.doc.pincode;
// Check autopay enrollment choice
const save_autopay = document.getElementById(`save_autopay_${uid}`)?.checked;
run_token_payment(frm, response.token, dialog, {
cardholder_name: finalName,
billing_zip: finalZip
billing_zip: finalZip,
save_autopay: save_autopay
});
} else {
@@ -294,49 +308,32 @@ function open_manual_payment_form(frm) {
function run_token_payment(frm, token, dialog, extra_data = {}) {
const save_autopay = extra_data.save_autopay ? 1 : 0;
frappe.call({
method: "ns_app.api.payments.run_token_payment",
args: {
invoice: frm.doc.name,
token: token,
cardholder_name: extra_data.cardholder_name,
billing_zip: extra_data.billing_zip
billing_zip: extra_data.billing_zip,
save_autopay: save_autopay
},
callback(r) {
if (r.message?.success) {
frappe.confirm(
"Payment successful. Save this card for AutoPay?",
() => {
// YES → save to vault
frappe.call({
method: "ns_app.api.payments.save_to_autopay",
args: {
customer: frm.doc.customer,
token: token,
cardholder_name: extra_data.cardholder_name,
billing_zip: extra_data.billing_zip
},
callback(res) {
if (res.message?.success) {
frappe.show_alert({
message: "AutoPay enabled",
indicator: "green"
});
} else {
frappe.msgprint(res.message?.error || "Failed to save AutoPay");
}
}
});
},
() => {
// NO
frappe.show_alert({
message: "Payment completed (not saved)",
indicator: "blue"
});
}
);
if (save_autopay && r.message.vault_id) {
frappe.show_alert({
message: "Payment successful + AutoPay enabled",
indicator: "green"
});
} else {
frappe.show_alert({
message: "Payment successful",
indicator: "green"
});
}
dialog.hide();
frm.reload_doc();