4.2 KiB
Author: Ty Reynolds Description: Full flow of what happens now from the moment a user opens the payment form through vault creation and payment entry generation.
- User Opens “Pay Invoice” Form
Frontend JS runs:
open_manual_payment_form(frm)
This creates a custom Frappe dialog containing:
Cardholder Name Billing ZIP Secure card fields from Collect.js “Subscribe to Auto Pay” checkbox Pay button
At this point, no card data touches ERPNext, Collect.js owns the secure fields
- Collect.js Initializes Secure Fields
These divs:
are replaced by NMI-hosted iframe fields.
This is important because card numbers never enter our JS, CVV never reaches ERPNext, PCI scope stays much lower and we don't have worry.
- User Enters Card Info
User types:
card number expiration CVV ZIP cardholder name
Optionally checks:
Subscribe to Auto Pay
- User Clicks “Pay”
JS gathers "const save_autopay = checkbox.checked ? 1 : 0;". Then Collect.js tokenizes the card. Instead of returning card data, NMI returns "payment_token". This token represents the card securely.
- Frontend Calls Backend API
Frontend sends:
frappe.call({ method: "ns_app.api.payments.run_token_payment", args: { invoice, token, cardholder_name, billing_zip, save_autopay } })
ERPNext now receives:
token invoice checkbox value customer metadata
But not the raw card data.
- Backend Starts run_token_payment()
Python method begins:
run_token_payment(...)
First debug logs run:
AUTOPAY DEBUG - START
This is to confirm invoice arrived and checkbox value arrived
- Feature Flag Protection Runs
This executes:
if not frappe.conf.get("enable_autopay_signup"): save_autopay = 0
Meaning checkbox can exist but vaulting can be globally disabled
Then "save_autopay = int(save_autopay or 0)" normalizes the value safely.
- Invoice + Customer Are Loaded
Backend loads:
inv = frappe.get_doc("Sales Invoice", invoice) customer = frappe.get_doc("Customer", inv.customer)
Now the system knows exact invoice, ERP customer and outstanding balance
- Name + ZIP Are Prepared
Fallback logic determines first_name, last_name and billing_zip
This is used both for payment and vault creation
- SALE Transaction Runs
Backend sends:
sale_data = { "type": "sale", "payment_token": token, ... }
to:
https://secure.nmi.com/api/transact.php
NMI processes the payment.
- NMI Returns Payment Response
NMI returns query-string style data: response=1 transactionid=123456
Python then parses it.
- Payment Success Check
If response == "1", then payment succeeded.
If not:
Error returned to frontend, payment entry NOT created
- ERPNext Payment Entry Is Created
Code runs:
create_payment_entry(...)
This creates Payment Entry, allocates against invoice, submits it automatically and invoice balance updates immediately.
- Vault Logic Begins (Only If Checked)
This section runs ONLY if save_autopay == 1, which means the box is checked, and feature flag enabled.
Debug log:
AUTOPAY DEBUG - VAULT ENTRY
- Vault Request Sent to NMI
Backend sends customer_vault=add_customer and payment_token
This tells NMI to store this payment method securely
- NMI Creates Customer Vault Entry
NMI returns response=1 customer_vault_id=******
This vault ID is the reusable payment profile.
No card data is stored in ERPNext.
Only the vault reference.
- ERPNext Customer Is Updated
If successful:
customer.custom_auto_pay_id = vault_id and customer.custom_auto_pay_status = 1
Now the ERP Customer is marked as enrolled in autopay and linked to NMI vault profile
- Autopay Transaction Flow
Autopay requests send:
"type": "sale", "customer_vault_id": vault_id
NMI charges the saved method directly.
- Webhook Safety
If NMI later posts payment confirmations crystalclear_webhook() can also create payment entries automatically.
Security Model:
Our system is currently designed to -> Never have ERPNext store card numbers. CVV never touches our backend. Collect.js handles PCI-sensitive data. NMI stores vault securely. ERPNext only stores vault ID
root@erpnext:/home/norman/frappe-bench#