Analyze the NS App and document what it does: NMI card payments on Sales Invoices, AutoPay vaulting, multi-invoice settlement, guided Customer Quick Entry, and branded print formats. - docs/ARCHITECTURE.md — functional overview, component map, payment flow, data model, dependencies, config, known tech debt - docs/DESIGN_SPEC.md — goals, requirements, API contracts, failure modes, acceptance criteria - docs/SECURITY_NOTES.md — posture + hardening list - docs/README.md — docs index; link from root README Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
9.5 KiB
NS App — Architecture & Functional Overview
Custom ERPNext / Frappe application by NS Innovations that extends the standard Sales workflow with an embedded card-payment experience, AutoPay vaulting, a streamlined customer onboarding dialog, and branded print formats.
1. What the app does
NS App layers four capabilities on top of a stock ERPNext v-current install:
| Capability | Where it lives | Summary |
|---|---|---|
| In-form card payments | ns_app/public/js/sales_invoice.js + ns_app/api/payments.py |
Adds a Run Payment button to submitted, unpaid Sales Invoices. Card data is tokenized client-side by NMI Collect.js and charged server-side via the NMI gateway. A Payment Entry is created and submitted automatically on success. |
| AutoPay (card vaulting) | ns_app/api/payments.py, Customer custom fields |
A card can be saved to the NMI Customer Vault. The returned vault ID is stored on the Customer, enabling one-click recurring charges and webhook-driven payments. |
| Multi-invoice payment | sales_invoice.js, get_unpaid_invoices, run_token_payment |
A single card charge can settle several of a customer's outstanding invoices at once, producing one Payment Entry allocated across them. |
| Customer Quick Entry | ns_app/public/js/customer_quick_entry.js, custom.js, ns_app/api/customer.py |
Replaces ERPNext's default "New Customer" quick-entry dialog with a guided form that creates Customer + Contact + Address atomically, with ZIP-based city/state autofill. |
| Branded print formats | ns_app/print_formats/ (shipped as fixtures) |
Double-window envelope layouts for Invoice, Sales Order, Quotation, and Dunning. |
2. Component map
ns_app/
├── hooks.py # App manifest: JS injection points + fixtures
├── api/
│ ├── payments.py # Payment gateway integration (NMI)
│ └── customer.py # Atomic customer creation endpoint
├── public/js/
│ ├── customer_quick_entry.js # Overrides CustomerQuickEntryForm (global)
│ ├── custom.js # Legacy/alt quick-entry enhancer
│ └── sales_invoice.js # Payment UI on the Sales Invoice form
└── print_formats/print_formats/ # HTML/Jinja print templates (fixtures)
Injection points (hooks.py)
app_include_js→customer_quick_entry.jsloads on every desk page (needed because Customer quick-entry can be triggered from many forms).doctype_js["Sales Invoice"]→sales_invoice.jsloads only on the Sales Invoice form.fixtures→ three Print Formats (NS Invoice,NS Sales Order,NS Quotation) are version-controlled and synced on migrate.
Note:
custom.jsis not referenced inhooks.py. It is an earlier iteration of the quick-entry enhancement, superseded bycustomer_quick_entry.js. See Known issues.
3. Payment flow (end-to-end)
┌─ Sales Invoice form (submitted, outstanding > 0) ─────────────────────────┐
│ "Run Payment" button → check_autopay(customer) │
└────────────┬──────────────────────────────────────────────────────────────┘
│
┌───────┴────────┐
│ AutoPay on? │
└───┬────────┬───┘
yes no
│ │
▼ ▼
run_autopay_ open_manual_payment_form() ── Collect.js renders NMI-hosted
payment() │ iframe fields (PCI out of scope)
│ │
│ user enters card → CollectJS.startPaymentRequest() → payment_token
│ │
│ ▼
│ run_token_payment(invoice, token, invoice_names[], …)
│ │
▼ ▼
call_payment POST https://secure.nmi.com/api/transact.php (type=sale)
_api() │
│ response=1 ? ── no ──► error surfaced to UI, no Payment Entry
│ │
│ yes
└───────┬───────┘
▼
create_payment_entry(invoices[], transaction_id, mode_of_payment)
│ (dedup on reference_no == transaction_id)
▼
Payment Entry inserted + submitted → invoice outstanding updates
│
▼ (only if "Save for Auto Pay" checked AND enable_autopay_signup=1)
customer_vault=add_customer → vault_id stored on Customer
The three server entry points to a charge
run_token_payment— interactive, one-time or multi-invoice card charge from the manual dialog. Optionally vaults the card.run_autopay_payment→call_payment_api— charges a previously vaulted card (customer_vault_id) with no card entry.crystalclear_webhook(allow_guest=True) — gateway-initiated confirmation that creates a Payment Entry for the referenced invoice.
All three converge on create_payment_entry, which is idempotent on the
transaction ID (reference_no).
4. Data model (custom fields)
The app relies on custom fields on Customer (created outside this repo — via ERPNext Customize Form / Custom Field, not shipped as fixtures here):
| Field | Type | Purpose |
|---|---|---|
custom_auto_pay_status |
Check | Whether AutoPay is enabled |
custom_auto_pay_id |
Data | NMI Customer Vault ID |
custom_auto_pay_first_name |
Data | Cardholder first name (vault) |
custom_auto_pay_last_name |
Data | Cardholder last name (vault) |
custom_auto_pay_company |
Data | Company on the vault record |
custom_auto_pay_zip |
Data | Billing ZIP on the vault record |
custom_send_via |
Select | Preferred delivery method (mail/email/fax) |
⚠️ These fields are a required dependency that is not tracked in this repository. See Known issues.
5. Customer Quick Entry
customer_quick_entry.js subclasses frappe.ui.form.CustomerQuickEntryForm
and overrides render_dialog() to present a custom dialog instead of
ERPNext's. Key design points:
- Preserves
this.after_insert— the originating link-field callback — so that after creation the new customer is written back into the field that triggered quick entry (e.g. Customer on a Sales Order). - Polls for
frappe.ui.form.make_quick_entryto exist, then patches it to re-assert the override on everyCustomerinvocation (defends against bundle load-order races). - Submits to
ns_app.api.customer.create_customer_full, which creates Customer + Contact + Address inside one DB transaction (begin/commit/rollback). - ZIP autofill via the public
api.zippopotam.usservice populates city/state/country.
6. External dependencies
| Dependency | Used for | Notes |
|---|---|---|
NMI Gateway (secure.nmi.com/api/transact.php) |
Sale + vault transactions | Requires nmi_security_key in site_config.json |
NMI Collect.js (secure.nmi.com/token/Collect.js) |
Client-side card tokenization | Tokenization key is currently hard-coded in sales_invoice.js |
| api.zippopotam.us | ZIP → city/state/country autofill | Public, unauthenticated, US only |
7. Configuration
site_config.json:
{
"nmi_security_key": "your_nmi_security_key",
"enable_autopay_signup": 0
}
nmi_security_key— required for all charge and vault calls.enable_autopay_signup— feature flag. When falsy, the "Save for Auto Pay" checkbox is ignored server-side and no vault entry is created, even if the user checks the box.
Hard-coded values worth noting:
paid_toaccount for card/ACH payments:"ENB Bank Account - NIL"(company-abbreviation specific — seecreate_payment_entry).- Collect.js tokenization key in
sales_invoice.js.
8. Known issues / tech debt
- Undeclared custom-field dependency.
custom_auto_pay_*andcustom_send_viaon Customer are required but not shipped as fixtures. A fresh install will fail until they are created manually. - Duplicate quick-entry logic.
custom.jsandcustomer_quick_entry.jsboth overridemake_quick_entry; only the latter is wired inhooks.py.custom.jsappears to be dead code. - Hard-coded account & keys.
"ENB Bank Account - NIL"and the Collect.js tokenization key are not configurable. - Version drift.
setup.pydeclares0.0.1while__init__.pydeclares0.1.0. - Verbose payment logging.
payments.pywrites request/response snippets viafrappe.log_erroras a debug channel; ensure no PII/PAN leakage and consider a proper logger + log level. - Webhook trust.
crystalclear_webhookisallow_guest=Trueand does not verify a signature/shared secret before creating Payment Entries.
9. Security model
- Card numbers and CVV are entered into NMI-hosted iframes (Collect.js) and
never touch ERPNext's DOM or backend — only a single-use
payment_tokendoes. This keeps PCI scope minimal. - ERPNext stores only the vault ID, never card data.
- The
nmi_security_keylives insite_config.json(server-side only). - All gateway calls are HTTPS.
See SECURITY_NOTES.md for hardening recommendations.