Includes: API (Express/TypeScript), Astro site, Python workers, document generators, FCC compliance tools, Canada CRTC formation, Ansible infrastructure, and deployment scripts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
335 lines
14 KiB
Markdown
335 lines
14 KiB
Markdown
# Performance West — ERPNext CRM Integration
|
|
|
|
**Last updated:** 2026-04-06
|
|
|
|
## Architecture Overview
|
|
|
|
```
|
|
Browser Astro Site Express API ERPNext
|
|
| | | |
|
|
+- Place order --------->+- POST /api/orders ----->+- Create DocType ----->|
|
|
| | | |
|
|
| | | ERPNext Webhook |
|
|
| | | | |
|
|
| | | +----+----+ |
|
|
| | | | Webhook |------->|
|
|
| | | | triggers| |
|
|
| | | | workers | |
|
|
| | | +----+----+ |
|
|
| | | | |
|
|
| | | +----+----+ |
|
|
| | | | Generate | |
|
|
| | | | Docs | |
|
|
| | | +----+----+ |
|
|
| | | | |
|
|
| | | +----+----+ |
|
|
| | | | Upload | |
|
|
| | | | to MinIO| |
|
|
| | | +----+----+ |
|
|
| | | | |
|
|
| | | Update status ---->|
|
|
| | | |
|
|
<-- email with docs -----| | |
|
|
```
|
|
|
|
**Data flow:**
|
|
|
|
1. Customer submits order on Astro site
|
|
2. Express API creates ERPNext Sales Invoice + Payment Request
|
|
3. Customer pays via Adyen or SHKeeper (payment routed through ERPNext gateways)
|
|
4. ERPNext marks invoice as Paid, webhook fires to Express API
|
|
5. Express API dispatches job to Workers HTTP server (:8090)
|
|
6. Workers generate documents (DOCX templates or LLM-written), convert to PDF
|
|
7. Documents are uploaded to MinIO
|
|
8. Worker updates ERPNext order status and attaches the MinIO URL
|
|
9. Admin reviews in ERPNext; approves or requests revision
|
|
10. Delivery worker emails documents to customer
|
|
|
|
**Key principle:** ERPNext is the single source of truth for all customer data, orders, and tickets. The Express API and Python workers read/write to ERPNext via its REST API. The API's own PostgreSQL database stores only state filing fees, API keys, session data, discount codes, payment surcharges, and commission ledger backup.
|
|
|
|
## ERPNext Setup
|
|
|
|
- **URL:** https://crm.performancewest.net
|
|
- **Admin:** `Administrator` (password in Ansible vault)
|
|
- **API keys:** Set in Express API `.env` as `ERPNEXT_API_KEY` / `ERPNEXT_API_SECRET`
|
|
- **Image:** `performancewest-erpnext:latest` (custom, extends `frappe/erpnext:version-15`)
|
|
- **6 apps installed:** frappe, erpnext, payments, frappe_crypto, frappe_adyen, performancewest_erpnext
|
|
- **Timezone:** America/Chicago
|
|
|
|
## Custom DocTypes
|
|
|
|
### Formation Order
|
|
|
|
Tracks LLC/Corp formation orders through the pipeline.
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| customer | Link (Customer) | ERPNext Customer record |
|
|
| entity_name | Data | Desired company name |
|
|
| entity_type | Select | LLC, Corporation, Nonprofit |
|
|
| state | Data | Two-letter state code |
|
|
| members | Table (Formation Member) | Member names, ownership %, capital |
|
|
| management_type | Select | Member-managed, Manager-managed |
|
|
| registered_agent | Data | RA provider name |
|
|
| add_ons | Table (Formation Add-On) | Operating agreement, EIN, RA service |
|
|
| status | Select | See Order Flow below |
|
|
| minio_path | Data | Path to generated documents in MinIO |
|
|
| filing_number | Data | State filing confirmation number |
|
|
| order_total | Currency | Total amount charged |
|
|
|
|
### Compliance Calendar
|
|
|
|
Tracks recurring compliance deadlines per entity.
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| customer | Link (Customer) | ERPNext Customer |
|
|
| entity_name | Data | Company name |
|
|
| state | Data | State code |
|
|
| event_type | Select | Annual Report, Franchise Tax, RA Renewal, etc. |
|
|
| due_date | Date | Next due date |
|
|
| recurrence | Select | Annual, Quarterly, Monthly |
|
|
| status | Select | Upcoming, Due Soon, Overdue, Completed |
|
|
| reminder_sent | Check | Whether reminder email was sent |
|
|
|
|
### Sensitive ID
|
|
|
|
Stores SSN/EIN with encryption. Uses ERPNext's Password field type, which encrypts values at rest.
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| customer | Link (Customer) | ERPNext Customer |
|
|
| id_type | Select | SSN, EIN, ITIN |
|
|
| id_value | Password | Encrypted value (never exposed in API responses) |
|
|
| entity_name | Data | Associated entity (for EIN) |
|
|
|
|
### Referral Partner
|
|
|
|
Tracks affiliate/referral partners and their commissions.
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| partner_name | Data | Partner name |
|
|
| partner_email | Data | Email |
|
|
| referral_code | Data | Unique referral code |
|
|
| commission_rate | Percent | Commission percentage |
|
|
| total_referrals | Int | Count of referred customers |
|
|
| total_paid | Currency | Total commissions paid |
|
|
|
|
### Compliance Service
|
|
|
|
Tracks compliance consulting service orders (FLSA audit, CCPA audit, etc.).
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| customer | Link (Customer) | ERPNext Customer |
|
|
| service_type | Select | FLSA Audit, CCPA Audit, TCPA Audit, Contractor Assessment, Handbook Review, Breach Response Plan, Privacy Policy |
|
|
| status | Select | See Order Flow below |
|
|
| intake_data | JSON | Service-specific intake form data |
|
|
| minio_path | Data | Path to generated documents in MinIO |
|
|
| order_total | Currency | Total amount charged |
|
|
|
|
### Sales Agent
|
|
|
|
Tracks sales agents and their referral codes.
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| agent_name | Data | Agent full name |
|
|
| agent_email | Data | Email |
|
|
| referral_code | Data | REF-XXXXX format code |
|
|
| commission_tier | Select | Standard, Premium |
|
|
| payout_method | Select | Relay ACH |
|
|
|
|
### Commission Ledger
|
|
|
|
Per-order commission records.
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| agent | Link (Sales Agent) | Referring agent |
|
|
| order | Link (Sales Order) | The order |
|
|
| service_type | Select | Formation, CRTC, Bundle |
|
|
| commission_amount | Currency | Flat commission ($50/$100/$300) |
|
|
| status | Select | Pending, Eligible, Paid |
|
|
| paid_date | Date | When commission was paid |
|
|
|
|
## Custom Fields on Standard DocTypes
|
|
|
|
| DocType | Field | Type | Purpose |
|
|
|---------|-------|------|---------|
|
|
| Sales Order | custom_identity_status | Select | Identity verification status for CRTC orders |
|
|
| Sales Order | custom_order_type | Select | formation, crtc, bundle, compliance |
|
|
| Sales Order | custom_external_order_id | Data | PostgreSQL order ID |
|
|
| Sales Invoice | custom_surcharge_pct | Percent | Payment method surcharge applied |
|
|
| Sales Invoice | custom_payment_gateway | Data | Which gateway processed payment |
|
|
| Payment Request | custom_adyen_session_id | Data | Adyen session reference |
|
|
|
|
## Order Flow
|
|
|
|
All orders (Formation Order and Compliance Service) follow this status pipeline:
|
|
|
|
```
|
|
Ordered --> Queued --> Processing --> Review --> Approved --> Ready --> Delivered
|
|
```
|
|
|
|
| Status | Description |
|
|
|--------|-------------|
|
|
| **Ordered** | Customer has paid; order created in ERPNext |
|
|
| **Queued** | Payment confirmed; order queued for processing |
|
|
| **Processing** | Worker is actively generating documents or filing |
|
|
| **Review** | Documents generated; awaiting admin review |
|
|
| **Approved** | Admin approved; documents ready for delivery |
|
|
| **Ready** | All reviews passed; documents ready for delivery |
|
|
| **Delivered** | Documents emailed to customer; order complete |
|
|
|
|
**Error states:**
|
|
|
|
| Status | Description |
|
|
|--------|-------------|
|
|
| **Failed** | Worker encountered an error; needs manual intervention |
|
|
| **Revision** | Reviewer requested changes; returns to Processing |
|
|
| **Refunded** | Order cancelled and refunded |
|
|
|
|
## Document Generation Pipeline
|
|
|
|
Two paths depending on the service:
|
|
|
|
### Template-Based (Formation Orders)
|
|
|
|
Used for: Operating agreements, invoices, privacy policies.
|
|
|
|
1. Worker fetches the DOCX template from MinIO (`templates/operating-agreement.docx`)
|
|
2. `DocxBuilder` fills Jinja2 placeholders with order data from ERPNext
|
|
3. DocServer converts DOCX to PDF (or LibreOffice headless as fallback)
|
|
4. Both files uploaded to MinIO at `orders/{order_id}/`
|
|
5. ERPNext order updated with `minio_path`
|
|
|
|
### LLM-Based (Compliance Services)
|
|
|
|
Used for: FLSA audits, CCPA audits, TCPA audits, contractor assessments, handbook reviews, breach response plans.
|
|
|
|
1. Worker fetches the DOCX template from MinIO (provides structure/formatting)
|
|
2. Worker calls Ollama (qwen2.5:7b) with a service-specific prompt + intake data
|
|
3. LLM generates analysis text for each section
|
|
4. `DocxBuilder` fills template placeholders and inserts LLM-generated sections
|
|
5. DocServer converts to PDF (LibreOffice fallback)
|
|
6. Both files uploaded to MinIO
|
|
7. Order status set to **Review** (always requires human review for LLM output)
|
|
|
|
### Hybrid
|
|
|
|
Some documents use both approaches: template for structure/boilerplate, LLM for analysis sections. Example: FLSA audit uses a template for the report structure, table formatting, and disclaimer, but LLM writes the executive summary, classification analysis, and remediation plan.
|
|
|
|
## MinIO Storage Structure
|
|
|
|
```
|
|
performancewest/ # Bucket
|
|
+-- templates/ # DOCX templates (9 uploaded)
|
|
| +-- operating-agreement.docx
|
|
| +-- privacy-policy.docx
|
|
| +-- breach-response-plan.docx
|
|
| +-- flsa-audit-report.docx
|
|
| +-- contractor-assessment.docx
|
|
| +-- handbook-review.docx
|
|
| +-- ccpa-audit-report.docx
|
|
| +-- tcpa-audit-report.docx
|
|
| +-- invoice.docx
|
|
+-- orders/ # Generated documents per order
|
|
| +-- FO-2026-0001/ # Formation Order
|
|
| | +-- operating-agreement.docx
|
|
| | +-- operating-agreement.pdf
|
|
| | +-- articles-of-organization.pdf
|
|
| | +-- ein-confirmation.pdf
|
|
| +-- CS-2026-0042/ # Compliance Service
|
|
| | +-- flsa-audit-report.docx
|
|
| | +-- flsa-audit-report.pdf
|
|
| | +-- remediation-checklist.pdf
|
|
| +-- ...
|
|
+-- invoices/ # Generated invoices
|
|
| +-- INV-2026-0001.pdf
|
|
| +-- ...
|
|
+-- backups/ # Encrypted database backups
|
|
+-- erpnext-2026-03-19.sql.gpg
|
|
+-- ...
|
|
```
|
|
|
|
**Access control:** MinIO is internal only (not exposed to the internet). Documents are served to customers via pre-signed URLs generated by the Express API, valid for 1 hour.
|
|
|
|
## Listmonk Integration
|
|
|
|
Listmonk (`lists.performancewest.net`) handles email marketing campaigns. It does **not**
|
|
store customer orders or tickets — ERPNext is the CRM source of truth.
|
|
|
|
**Admin:** `admin` / `F6IHwDFeMjaDGDPR1OQcKtUA86BGfs2`
|
|
|
|
**Sync flow:**
|
|
|
|
1. When a new Customer is created in ERPNext, the Express API pushes the subscriber to Listmonk via its REST API
|
|
2. Listmonk manages email campaigns, drip sequences, and newsletter sends
|
|
3. Listmonk tracks email opens/clicks and manages subscriber lists
|
|
4. Bounce processing via POP3 from Carbonio (`co.carrierone.com`)
|
|
|
|
**Mass email:** SMTP2GO is used for Listmonk campaign sends (not Carbonio — Carbonio is for transactional email only).
|
|
|
|
**Campaigns:**
|
|
|
|
| Campaign | Trigger | Content |
|
|
|----------|---------|---------|
|
|
| Welcome drip | New customer created | 3-email series: welcome, services overview, compliance tips |
|
|
| Formation follow-up | Formation delivered | Annual report reminder, RA renewal, compliance calendar |
|
|
| CRTC follow-up | CRTC carrier delivered | Banking setup, BITS filing, ATS survey prep |
|
|
| Compliance upsell | 30 days post-formation | FLSA audit, handbook review, privacy policy offers |
|
|
| Renewal reminder | 30 days before RA renewal | Registered agent renewal notice |
|
|
|
|
**Important:** Listmonk subscribers are synced one-way from ERPNext. ERPNext remains the source of truth. If a subscriber unsubscribes in Listmonk, the `email_opted_out` flag is synced back to ERPNext via webhook.
|
|
|
|
## ERPNext Helpdesk
|
|
|
|
ERPNext's built-in Helpdesk module handles all customer support:
|
|
|
|
| Feature | ERPNext Helpdesk |
|
|
|---------|-----------------|
|
|
| Ticket creation | Via API (contact form), email, or ERPNext UI |
|
|
| Assignment | Auto-assign rules based on ticket type |
|
|
| SLA tracking | Built-in SLA configuration per priority |
|
|
| Knowledge base | ERPNext Knowledge Base module |
|
|
| Customer portal | ERPNext portal (customers can view their tickets) |
|
|
| Email integration | ERPNext Email Account (incoming/outgoing) |
|
|
|
|
This eliminates the need for a separate ticketing system. All customer data stays in one place with native linking between tickets and orders.
|
|
|
|
## Security
|
|
|
|
### Encrypted Backups
|
|
|
|
- ERPNext database is backed up daily via `bench backup`
|
|
- Backups are GPG-encrypted before upload to MinIO `backups/` prefix
|
|
- Retention: 30 daily, 12 monthly, indefinite yearly
|
|
- Restore tested monthly
|
|
|
|
### RBAC (Role-Based Access Control)
|
|
|
|
ERPNext provides granular role permissions:
|
|
|
|
| Role | Access |
|
|
|------|--------|
|
|
| **Admin** | Full access to all DocTypes |
|
|
| **Operations** | Formation Orders, Compliance Services (read/write), Customers (read) |
|
|
| **Support** | Helpdesk tickets (read/write), Customers (read) |
|
|
| **API User** | REST API access with specific DocType permissions |
|
|
| **Sales Agent** | Own referrals and commission ledger (read) |
|
|
|
|
### Audit Logging
|
|
|
|
- ERPNext Version (audit log) tracks all changes to all DocTypes
|
|
- Every create/update/delete is logged with user, timestamp, and field-level diff
|
|
- Audit logs are included in encrypted backups
|
|
- Access logs retained for 1 year
|
|
|
|
### Sensitive Data (SSN/EIN)
|
|
|
|
- Stored in ERPNext Password fields (AES-256 encrypted at rest)
|
|
- Never returned in API list responses; only accessible via explicit single-record GET with appropriate role
|
|
- Masked in the ERPNext UI (shown as `***-**-1234`)
|
|
- Workers decrypt only when needed for IRS EIN application, then discard from memory
|