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>
210 lines
9.5 KiB
Markdown
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.
|