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

210 lines
9.5 KiB
Markdown

# 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.