Files
pn-new-crm/01-CONSOLIDATED-SYSTEM-SPEC.md

274 lines
22 KiB
Markdown
Raw Normal View History

# Port Nimara CRM — Consolidated System Specification
**Compiled:** 2026-03-11
**Sources:** Claude Code audit + Codex audit of `client-portal/`
This document merges both independent audit outputs into a single authoritative reference for the rebuild.
---
## 1. System Architecture
### 1.1 Current Stack
| Layer | Technology | Role |
| -------------- | ------------------------------------------------------ | -------------------------------------------------------------------------------- |
| Framework | Nuxt 3 SPA (`ssr: false`) + Nitro server | Client shell, server API, scheduled tasks |
| Database | NocoDB REST API | System of record for interests, berths, expenses, invoices, settings, audit logs |
| Auth | Keycloak OIDC | SSO, refresh tokens, role/group extraction via JWT `groups` claim |
| File Storage | MinIO (S3-compatible) | Documents, EOI PDFs, expense exports, cached email JSON/attachments |
| E-Signatures | Documenso (self-hosted at `signatures.portnimara.dev`) | EOI generation, 3-party signing, webhook lifecycle, signed PDF retrieval |
| Outbound Email | SMTP via Nodemailer | User mailbox sending + reminder notifications (separate SMTP config) |
| Inbound Email | IMAP | Thread sync, cached mail archive, sales inbox PDF harvesting |
| Currency | Frankfurter API | Exchange rates with local file-based cache fallback |
| Automations | automation.portnimara.com | No-code webhooks for request forms and sales handoffs |
| Deployment | Docker (node:20-alpine) + Gitea CI/CD | Builds `.output`, pushes image on `main` pushes — no tests in pipeline |
### 1.2 Runtime Dependencies (33 production, 6 dev)
Key packages: `@nuxt/ui` ^3.2.0, `@pdfme/common` + `@pdfme/generator` ^5.4.0, `@pinia/nuxt`, `chart.js`, `date-fns`, `imap`, `lodash-es`, `lucide-vue-next`, `mailparser`, `minio`, `node-cron`, `nodemailer`, `nuxt-directus` (legacy, unused), `pdfkit`, `sharp`, `vue-chartjs`, `vue-toastification`, `vuetify-nuxt-module` (legacy).
Notable: `@nuxt/ui` and Vuetify coexist alongside a custom Maritime Design System. The frontend is mid-migration. `nuxt-directus` is installed but auth/data paths use Keycloak and NocoDB.
### 1.3 External Service Configuration
| Service | Config Source | Credential Status |
| ------------------------- | ---------------------------------------------------------- | --------------------------------------------------------- |
| NocoDB | `NUXT_NOCODB_URL`, `NUXT_NOCODB_TOKEN` | Environment variables (OK) |
| Keycloak | Hardcoded realm/client URLs + `KEYCLOAK_CLIENT_SECRET` env | Mixed — URLs hardcoded, secret in env |
| MinIO | Runtime config in `nuxt.config.ts` | **CRITICAL: Access key + secret key hardcoded in source** |
| Documenso | Env-driven base URL/API key/template IDs/webhook secret | Environment variables (OK) |
| SMTP/IMAP (user) | Per-user credentials cached in-memory (encrypted) | In-memory only — lost on restart |
| SMTP/IMAP (sales) | `process-sales-eois.ts` | **CRITICAL: Password hardcoded in source** |
| SMTP (reminders) | `NUXT_REMINDER_SMTP_*` env vars | Environment variables (OK) |
| automation.portnimara.com | Hardcoded webhook URLs in 3 handlers | No auth beyond URL secrecy |
### 1.4 Background Tasks
| Task | Schedule | Mechanism |
| ------------------------------- | --------------------------------------------------------- | -------------------------------------- |
| Pending notification processing | Every 30 seconds | Nitro experimental task (active) |
| Signature polling fallback | Every 5 minutes (default) | `setInterval` in server plugin |
| EOI reminders | Every 10 minutes (check), fires at 09:00/16:00 Paris time | `setInterval`**currently disabled** |
| Sales email processing | Manual trigger via internal API | Task file exists but not scheduled |
| Currency rate refresh | Manual/startup | Task file exists |
---
## 2. Data Model
### 2.1 Entity Relationship Map
| Relationship | Cardinality | Storage | Notes |
| -------------------------------- | ------------------- | ----------------------------------------------------------------------------- | ---------------------------------------------------------------- |
| Interests ↔ Berths (committed) | Many-to-many | NocoDB link field `cj7v7bb9pa5eyo3` / reverse `c7q2z2rb27c1cb5` | Core berth assignment. Linking auto-moves berth to `Under Offer` |
| Interests ↔ Berths (recommended) | Many-to-many | NocoDB link field `cgthyq2e95ajc52` | Suggestions separate from committed links |
| Expenses → Invoices | Many-to-one (soft) | `Expenses.invoice_id` + denormalized `Invoices.expense_ids` (comma-separated) | No relational constraint; manual sync required |
| Interests → EOI Documents | Attachment array | `Interests.EOI Document` field | Not modeled as own table |
| Interests → Documenso | Soft one-to-one | `Interests.documensoID` | Remote document can drift from local state |
| Email threads | Object storage only | MinIO `client-emails` bucket as JSON | No NocoDB table stores thread records |
| Users/Roles | External only | Keycloak JWT groups | No app-managed user table |
### 2.2 Core Tables
**Interests** (table: `mbs9hjauug4eseo`) — ~60 fields including:
- Identity: Full Name, Email Address, Phone Number, Address
- Vessel: Yacht Name, Length, Width, Depth, Berth Size Desired
- Pipeline: Sales Process Level (8 stages), Lead Category, Source, Date Added
- EOI State: EOI Status, documensoID, 6 signature link fields, 6 embedded signature link fields, EOI Client/Developer/Oscar Links
- Timing: EOI Time Sent, Time LOI Sent, Request Form Sent
- Milestones: Berth Info Sent Status, Contract Sent Status, Deposit 10% Status, Contract Status
- Notification machinery: ~15 fields for webhook locks, cooldowns, signature timestamps, notification tracking, pending email flags
- Links: Berths (many-to-many), Berth Recommendations (many-to-many)
**Berths** (table: `mczgos9hr3oa9qc`) — Physical marina berths:
- Identity: Mooring Number, Area, Status (Available/Under Offer/Sold)
- Dimensions: Length, Width, Draft, Water Depth, Nominal Boat Size (imperial + metric pairs)
- Infrastructure: Side Pontoon, Power Capacity, Voltage, Mooring Type, Access, Cleat/Bollard Type/Capacity
- Commercial: Price, Bow Facing
- Override: status_override_mode, status_last_modified_at
- Links: Interested Parties (reverse many-to-many)
**Expenses** (table: `mxfcefkk4dqs6uq`) — Operational costs:
- Core: Establishment Name, Price, currency, Payment Method, Category, Payer, Time, Contents
- Receipts: NocoDB attachment field
- Payment: payment_status, payment_date, payment_method, payment_reference, payment_notes
- Link: invoice_id (back-reference)
**Invoices** (table: `mvyvz0lpc30p01s`) — Generated from expense groups:
- Core: invoice_number (INV-YYYYMM-###), client_name, billing_email, due_date, payment_terms, currency, total_amount
- Payment: status, payment_status, payment_date, payment_method, payment_reference
- Links: expense_ids (comma-separated string — integrity risk)
- Output: pdf_path
**Singleton tables:** Reminder Settings (`mfdltoib4bji21u`), Alert Settings (`m5xl992f4i6e9q7`), Audit Logs (`audit_logs`)
### 2.3 Non-Table Entities
- **EOI documents**: Dual storage — MinIO files under `EOIs/` prefix AND Documenso remote documents keyed by `Interests.documensoID`. The `EOI Document` field stores arrays of file metadata, not normalized child rows.
- **Email threads**: JSON objects and attachments in MinIO `client-emails` bucket under `interest-<id>/...`. Not indexed in NocoDB.
- **Users/roles**: No app-managed user table. Identity from `nuxt-oidc-auth` cookie + Keycloak JWT `groups` claim.
### 2.4 Data Integrity Risks
1. All relationships enforced by application code only — NocoDB provides no transaction boundary
2. `Invoices.expense_ids` (comma-separated) and `Expenses.invoice_id` can drift out of sync
3. EOI state spans three systems (NocoDB fields, Documenso documents, MinIO files) — deleting any layer orphans the others
4. Signature/reminder state is 15+ nullable text fields instead of a workflow/history table
5. Audit logging inconsistent: some domains write to `audit_logs`, others only `console.log()`
6. Invoice deletion handler calls `getExpenses()` with wrong filter shape — linked expenses missed and left orphaned
7. `updateSignatureTimestamps()` returns empty update object — timestamps can remain stale
8. File audit logging functions never write to the real `audit_logs` table
---
## 3. Business Workflows
### 3.1 Authentication
- **Primary**: Keycloak OIDC Authorization Code flow via `nuxt-oidc-auth` cookie
- **Session caching**: 3-min TTL + 5-min grace, 1s rate limit between checks, request deduplication
- **Circuit breaker**: 5 failures → open state, 60s reset, 3 retries with exponential backoff
- **Roles**: admin > sales > user (from Keycloak `groups` claim)
- **Internal auth**: Static header `x-tag: 094ut234` for server-to-server calls (weak)
- **Dev bypass**: `NUXT_PUBLIC_DEV_AUTH_BYPASS=true` skips ALL auth — exposed as public runtime config
### 3.2 Interest Lifecycle (Sales Pipeline)
**8 stages**: Open → Details Sent → In Communication → Visited → Signed EOI and NDA → 10% Deposit → Contract → Completed
- Create: Whitelist filter on fields, date format conversion (dd-mm-yyyy → yyyy-mm-dd), audit logged
- Update: 3 retries with exponential backoff on 404, monitored fetch wrapper triggers alerts
- Berth linking: Auto-moves berth status to `Under Offer`; unlinking only resets to `Available` when berth is still `Under Offer`, interest not at high-close state, and no other interested parties remain
- Frontend auto-promotion: Entering yacht dimensions auto-upgrades Sales Process Level from General to Specific Qualified Interest
- Duplicate detection: Blocking by name prefix/email domain/phone prefix, then scoring by same email (1.0), same phone (1.0), similar name+address (0.8). Master selected by completeness + recency.
### 3.3 EOI/Signature Workflow (Documenso)
**3-party sequential signing**: Client (order 1) → Developer (order 2) → Sales/Approver (order 3)
- **Generation**: Requires populated client name, email, yacht name, L×W×D, and ≥1 linked berth. Blocked if manual EOI docs exist. Uses `@pdfme/generator` with Documenso template. Creates document via API, assigns 3 recipients, stores 12+ signing URL fields on Interest.
- **Webhooks**: `DOCUMENT_SIGNED` → deduplication via signature hash + in-memory lock → queues notification flag (developer or sales) → background task sends email every 30s. `DOCUMENT_COMPLETED` → downloads signed PDF → emails all 3 parties → stores in MinIO under `EOIs/{Client_Name}/`.
- **Fallback polling**: Every 5 minutes, checks all interests with documensoID against Documenso API, constructs synthetic webhook payloads.
- **Reminders**: Time-gated (09:00/16:00 Paris), per-interest `reminder_enabled` toggle, system-wide cooldown window. Currently disabled in production.
- **Manual upload**: Bypasses Documenso entirely — immediately sets `EOI Status = Signed` and `Sales Process Level = Signed EOI and NDA`.
### 3.4 Email System
- **Outbound**: Nodemailer SMTP with per-user encrypted credentials (in-memory cache, lost on restart). Sent emails stored in MinIO. HTML templates inlined as string literals in 4+ files.
- **Inbound**: IMAP sync with connection pool. Threads cached as JSON in MinIO `client-emails` bucket. Two parallel implementations exist (pool-based + standalone). V1 and V2 fetch endpoints.
- **Sales inbox**: Dedicated polling endpoint with hardcoded credentials harvests EOI-related PDFs.
### 3.5 Expense/Invoice Workflow
- **Expenses**: CRUD with receipt upload (MinIO), filtering by date/payer/category/payment status, CSV and PDF export. Export computes EUR subtotal + 5% processing fee. N+1 fetch patterns for related records.
- **Invoices**: Created from grouped expenses with auto-generated `INV-YYYYMM-###` numbers. Payment terms: immediate/net10/net15/net30/net45/net60. PDF generation applies 2% discount for net10 terms. Cascading payment status updates attempt manual rollback on failure. Invoice deletion has a bug that can miss linked expenses.
### 3.6 File Management
MinIO-backed file browser with upload/download/preview/rename/delete and folder management. Email attachments surfaced alongside stored documents. Presigned URLs for downloads. Audit logging defined but only prints to console (never writes to `audit_logs` table).
---
## 4. Security & Technical Debt
### 4.1 Critical (fix before any production traffic)
| Issue | Location | Impact |
| --------------------------------- | ------------------------------------------------ | ------------------------------------------------------------ |
| Hardcoded MinIO credentials | `nuxt.config.ts` lines 159161 | Full object storage access to anyone with repo access |
| Hardcoded sales email credentials | `server/api/email/process-sales-eois.ts` line 27 | Full mailbox access exposed in source |
| Dev auth bypass as public config | `nuxt.config.ts` `NUXT_PUBLIC_DEV_AUTH_BYPASS` | Entire CRM accessible without auth if flag set in production |
### 4.2 High
| Issue | Location |
| -------------------------------------------------- | ---------------------------------------------------------------- |
| Trivially guessable internal auth tag (`094ut234`) | Task endpoints, internal API calls |
| TLS certificate verification disabled | `email-utils.ts`, `fetch-thread.ts`, `send.ts` |
| Debug endpoints expose config without admin guard | `server/api/debug/*` |
| Destructive test endpoints in production | `test-eoi-cleanup.ts`, `test-berth-connection.ts` |
| N+1 query patterns on berth endpoints | `get-all-interest-berths.ts`, `get-berth-interested-parties.ts` |
| ~400 lines of duplicate EOI generation code | `generate-eoi-document.ts` + `generate-quick-eoi.ts` |
| No database migration strategy | All table IDs hardcoded as string literals |
| No transaction support (NocoDB REST API) | All multi-step write operations |
| Inconsistent error response shapes | Mixed `throw createError()` and `return { success: false }` |
| Frontend route auth metadata inconsistency | `roles` vs `auth.roles` — middleware only checks `to.meta.roles` |
| Mixed auth systems (Keycloak + leftover Directus) | Multiple components still use `useDirectusUser()` |
| Invoice deletion filter bug | `[id].delete.ts` calls `getExpenses()` with wrong filter shape |
### 4.3 Medium
No rate limiting, dual design system (Vuetify + Maritime), critical state in process memory only (webhook store, IMAP pool, credential cache), disabled scheduled reminders, inconsistent API design patterns, silent failures in core operations, retry without circuit breaker, inconsistent ID types, orphaned Documenso references, no caching + unbounded list fetches, blocking I/O in request handlers, weak TypeScript typing (`any` throughout), dead/unused code, EOI webhook timestamp persistence returning empty objects, file audit logging only printing to console.
### 4.4 Low
Reminder settings default to test mode, excessive console logging, naming inconsistencies (`documeso.ts` missing 'n'), missing input validation, ~16 mockup/abandoned pages in production source.
---
## 5. API Surface
The current system has ~100 endpoints mixing RPC-style (`/api/create-interest`), REST-style (`/api/invoices/[id].get.ts`), and ad-hoc patterns. No consistent pagination, error response shape, or input validation.
### Codex proposed clean API (~40 endpoints)
Resource-oriented surface replacing the current ~100:
- **Auth**: `/auth/login`, `/auth/logout`, `/auth/refresh`, `/auth/session`
- **Interests**: Standard REST + `/interests/{id}/berths`, `/interests/{id}/recommendations`, `/interests/{id}/status-transitions`, `/interests/{id}/eoi-documents/*`
- **Berths**: `GET /berths`, `GET /berths/{id}`, `PATCH /berths/{id}`
- **Expenses**: Standard REST + `/expenses/{id}/payments`, `/expenses/export/csv`, `/expenses/export/pdf`
- **Invoices**: Standard REST + `/invoices/{id}/payments`
- **Files**: Standard REST + `/files/{id}/download`, `/files/{id}/preview`
- **Email**: `/email/threads`, `/email/threads/{id}`, `/email/messages`, `/email/connections/test`
- **Admin**: `/admin/audit-logs`, `/admin/duplicate-jobs`, `/admin/settings/reminders`, `/admin/settings/alerts`
---
## 6. Frontend Architecture
### 6.1 Component Systems
- **Maritime Design System** (new): CSS custom properties (design tokens) for colors, typography, spacing, glassmorphism effects. Components: MaritimeButton, MaritimeCard, MaritimeInput, MaritimeModal, etc. Feature-flagged rollout.
- **Vuetify** (legacy): Still loaded globally. Competing styles increase bundle size.
- **Nuxt UI** (v3): Also present — three UI systems coexisting.
### 6.2 State Management
- One Pinia store (`expenses`) with 5-min cache TTL, optimistic updates, rollback on failure
- All other state: page-level refs, composable-level refs, `nuxtApp.payload.data`
- Auth state flows through middleware → payload → 3 different composables reading same data
### 6.3 Pages to Drop in Rebuild
~16 mockup/abandoned/test pages: `interest-list-mockup.vue`, `files-mockup.vue`, `expense-mockup.vue`, `dropdown-demo.vue`, `dropdown-test.vue`, `sidebar-test.vue`, `client-support.vue`, `data.vue`, `interest-analytics.vue`, `interest-berth-list.vue`, `interest-emails.vue`, `interest-eoi-queue.vue`, `portnimaraAI.vue`, `site.vue`, `social-media.vue`, `expenses-old.vue`
### 6.4 Iframe Embeds (9 pages)
Metabase (analytics, data), NocoDB views (EOI queue, berth gallery), webmail, Port Nimara AI, site analytics (Umami), social media marketing, client support. Recommendation: replace NocoDB views + webmail with native CRM features; keep 5-6 as iframes.
---
## 7. Critical Business Rules to Preserve
These rules are encoded in the current system (sometimes in both frontend and backend) and must carry forward:
1. Linking a berth to an interest auto-moves `Berth.Status` from `Available` to `Under Offer`
2. Unlinking a berth only resets to `Available` when: berth is still `Under Offer`, interest not at high-close state, and no other interested parties remain
3. Creating/editing an interest with yacht dimensions can auto-promote sales level from General to Specific Qualified Interest (frontend logic)
4. Quick EOI generation requires: populated client name, email, yacht name, L×W×D, and ≥1 linked berth
5. EOI generation blocked if manual/uploaded EOI documents already exist on the Interest
6. Generated EOIs set `EOI Status = Waiting for Signatures` and `Sales Process Level = EOI and NDA Sent`
7. Manual uploaded EOI documents immediately set `EOI Status = Signed` and `Sales Process Level = Signed EOI and NDA`
8. Documenso completion sends finalized PDF to client + developer + sales, stores signed PDF in MinIO, stamps `all_signed_notified_at`
9. Reminder sending gated by per-interest `reminder_enabled` + system-wide cooldown window
10. Expense exports compute EUR subtotal + 5% processing fee
11. PDF invoice generation applies 2% discount for `net10` payment terms before any optional fee
12. Invoice creation must update both Invoice record and every linked Expense back-reference (with rollback attempt on failure)
13. Roles enforced from Keycloak `groups` — no app-managed role table