Files
ns_erpnext_app/docs/ARCHITECTURE.md
norman 8bb8954ff6 docs: add architecture, design spec, and security notes
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>
2026-07-02 07:33:32 -04:00

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_jscustomer_quick_entry.js loads on every desk page (needed because Customer quick-entry can be triggered from many forms).
  • doctype_js["Sales Invoice"]sales_invoice.js loads 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.js is not referenced in hooks.py. It is an earlier iteration of the quick-entry enhancement, superseded by customer_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

  1. run_token_payment — interactive, one-time or multi-invoice card charge from the manual dialog. Optionally vaults the card.
  2. run_autopay_paymentcall_payment_api — charges a previously vaulted card (customer_vault_id) with no card entry.
  3. 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_entry to exist, then patches it to re-assert the override on every Customer invocation (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.us service 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_keyrequired 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_to account for card/ACH payments: "ENB Bank Account - NIL" (company-abbreviation specific — see create_payment_entry).
  • Collect.js tokenization key in sales_invoice.js.

8. Known issues / tech debt

  • Undeclared custom-field dependency. custom_auto_pay_* and custom_send_via on Customer are required but not shipped as fixtures. A fresh install will fail until they are created manually.
  • Duplicate quick-entry logic. custom.js and customer_quick_entry.js both override make_quick_entry; only the latter is wired in hooks.py. custom.js appears to be dead code.
  • Hard-coded account & keys. "ENB Bank Account - NIL" and the Collect.js tokenization key are not configurable.
  • Version drift. setup.py declares 0.0.1 while __init__.py declares 0.1.0.
  • Verbose payment logging. payments.py writes request/response snippets via frappe.log_error as a debug channel; ensure no PII/PAN leakage and consider a proper logger + log level.
  • Webhook trust. crystalclear_webhook is allow_guest=True and 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_token does. This keeps PCI scope minimal.
  • ERPNext stores only the vault ID, never card data.
  • The nmi_security_key lives in site_config.json (server-side only).
  • All gateway calls are HTTPS.

See SECURITY_NOTES.md for hardening recommendations.