Statement generation raised a fresh LPF invoice every month, so a customer who never paid accumulated a pile of small invoices, each carrying the flat dunning_fee again. Interest was also recomputed from each invoice's due date every run, re-billing periods already charged for. Now a customer gets one fee invoice per collections episode: - While an earlier fee invoice still carries a balance, the next run amends it and appends the new period's interest as a further line item, rather than creating a second invoice. - Payment Entries allocated to a partly paid fee invoice are unlinked by the cancellation and re-applied to the amended invoice via reconcile_against_document (the primitive Payment Reconciliation uses), so the outstanding amount and Payment Ledger stay correct. - Original posting and due dates are carried over. Re-dating to today would reset the invoice to Current in the statement's aging buckets and hide how long the balance has been owed. - Interest accrues from the last run, tracked by a new custom_late_fee_billed_upto field on Sales Invoice, so no period is billed twice. Fee invoices predating the field fall back to their posting date, which is when they were billed, so no migration patch is needed. - The flat dunning_fee is charged once, when a fee invoice is first raised, not again on every top-up. - Unpaid fee invoices are in the interest base on the same terms as any other overdue receivable, so interest compounds onto the fee balance. Amending means cancelling, which is only reversible for links we can restore. If the open fee invoice has a Journal Entry or credit note applied, a negative payment allocation, or a posting date in a frozen period, it is left alone, the charge goes on a new invoice, and the reason is recorded on the customer's timeline. Verified against nsi.local with two rolled-back integration probes covering the amend + re-link path (including two consecutive amendments) and the blocked-amend fallback. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
NS App – ERPNext Custom Integrations
Overview
NS App is a custom ERPNext application that:
Adding a manual payment form using Collect.js Processing secure card payments through NMI Optionally saving payment methods to the NMI Customer Vault Storing the returned vault ID on the ERPNext Customer for future autopay use
This part of the app is designed to integrate directly into the Sales Invoice workflow.
Features Secure card entry using NMI Collect.js (tokenization) Manual payment dialog inside ERPNext Optional “Subscribe to Autopay” checkbox Conditional vault creation in NMI Storage of customer_vault_id in ERPNext Customer Configurable enable/disable of autopay via site_config.json
Requirements ERPNext / Frappe Bench environment NMI gateway account Collect.js enabled in NMI Access to modify site_config.json
Installation
- Get the app cd ~/frappe-bench bench get-app git@git.nsinnovations.net:nsinnovations/ns_erpnext_app.git --skip-assets
- Install on site bench --site "site name" install-app ns_app
- Build and restart bench build bench restart
Configuration
Edit site configuration: nano ~/frappe-bench/sites/site-name/site_config.json
Add:
{ "nmi_security_key": "your_nmi_security_key", "enable_autopay_signup": 0 } Notes nmi_security_key is required for all payment and vault operations enable_autopay_signup controls whether the checkbox actually triggers vault creation Frontend Behavior
The payment dialog includes:
Cardholder Name Billing ZIP Secure card fields via Collect.js “Subscribe to Autopay” checkbox
If the checkbox is selected and autopay is enabled in config, the backend will attempt to create a vault entry.
Backend Flow: Payment Collect.js generates a payment token Token is sent to backend API Backend sends transaction to NMI Payment is processed Autopay (Optional)
If enabled and selected:
Backend sends customer_vault=add_customer request to NMI Uses the same payment token Parses response for customer_vault_id Saves ID to ERPNext Customer Example Vault Request vault_data = { "security_key": frappe.conf.get("nmi_security_key"), "customer_vault": "add_customer", "payment_token": token, "first_name": first_name, "last_name": last_name, "zip": billing_zip } Error Handling
Vault creation is wrapped in a try/except block to prevent payment failures:
try: if int(save_autopay or 0): # vault logic pass except Exception: frappe.log_error(frappe.get_traceback(), "Autopay Vault Error") Testing Strategy
Before enabling in production:
Set in site_config.json:
"enable_autopay_signup": 0 Verify: Payments process correctly No vault entries are created
Enable autopay:
"enable_autopay_signup": 1 Test: Checkbox unchecked → no vault Checkbox checked → vault created and saved Deployment Notes Do not expose autopay functionality until fully tested Keep enable_autopay_signup disabled in production until ready Always run: bench build bench restart
Troubleshooting:
- App not found
Ensure app is in apps.txt:
nano ~/frappe-bench/sites/apps.txt
Add:
ns_app
- ModuleNotFoundError
Ensure app is installed in environment:
./env/bin/pip install -e apps/ns_app Asset build errors
Skip during install:
bench get-app --skip-assets
Then fix frontend paths before running bench build.
Vault not saving
Check logs:
frappe.log_error(response.text, "Vault Response")
Verify:
customer_vault_id is returned Security key is correct File Structure apps/ns_app/ ├── ns_app/ │ ├── api/ │ │ └── payments.py │ ├── public/ │ │ └── js/ │ └── hooks.py ├── setup.py ├── pyproject.toml └── MANIFEST.in
Security Notes: Never store raw card data Always use Collect.js tokenization Keep nmi_security_key private Use HTTPS for all requests