# 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.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](#8-known-issues--tech-debt). --- ## 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_payment`** → **`call_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](#8-known-issues--tech-debt). --- ## 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`: ```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_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](./SECURITY_NOTES.md) for hardening recommendations.