505 lines
25 KiB
Markdown
505 lines
25 KiB
Markdown
|
|
# Port Nimara CRM — Business Rules Specification
|
|||
|
|
|
|||
|
|
**Compiled:** 2026-03-11
|
|||
|
|
**Core Principle:** The system never forces a workflow. All automated actions are convenience features that can be manually overridden by the salesperson at any time.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 1. Berth Status Rules
|
|||
|
|
|
|||
|
|
### BR-001: Configurable Berth Status Transition Rules
|
|||
|
|
|
|||
|
|
Berth status transitions are **admin-configurable** via a rules engine stored in `system_settings` (key: `berth_status_rules`, per-port). Each rule defines: a trigger action, a target status, and a mode.
|
|||
|
|
|
|||
|
|
**Modes:**
|
|||
|
|
|
|||
|
|
- **auto** — Status changes silently with an audit log entry. No user interaction needed.
|
|||
|
|
- **suggest** — A toast/confirmation prompt appears: "Change berth B-12 status to Under Offer?" — salesperson accepts or dismisses. If dismissed, status stays unchanged.
|
|||
|
|
- **off** — No status change, no prompt. The trigger is ignored.
|
|||
|
|
|
|||
|
|
**Default rules (shipped out of the box, admin can modify):**
|
|||
|
|
|
|||
|
|
| Trigger Action | Default Mode | Target Status | Notes |
|
|||
|
|
| ------------------------------------------------------------ | ------------ | ------------- | ------------------------------------------------ |
|
|||
|
|
| First active interest linked to berth | **suggest** | `under_offer` | Only fires when berth is currently `available` |
|
|||
|
|
| All active interests unlinked/archived from berth | **suggest** | `available` | Only fires when berth is currently `under_offer` |
|
|||
|
|
| EOI sent on any linked interest | **auto** | `under_offer` | Sending an EOI signals real engagement |
|
|||
|
|
| EOI fully signed on any linked interest | **auto** | `under_offer` | Keeps under_offer (doesn't escalate yet) |
|
|||
|
|
| Deposit received (interest reaches `deposit_10pct`) | **suggest** | `sold` | Big moment — prompt to confirm |
|
|||
|
|
| Contract signed (interest reaches `contract` or `completed`) | **suggest** | `sold` | Final confirmation |
|
|||
|
|
| Interest manually archived (sole link to berth) | **suggest** | `available` | Same as unlink |
|
|||
|
|
|
|||
|
|
**Admin configuration UI:** Settings → Berth Status Rules page. Table with columns: Trigger, Mode (dropdown: auto/suggest/off), Target Status (dropdown: available/under_offer/sold). Admin can also add custom rules for future trigger types.
|
|||
|
|
|
|||
|
|
**Rule evaluation order:** Rules are evaluated in the order listed above. First matching rule wins. If multiple rules match the same action, only the first applies.
|
|||
|
|
|
|||
|
|
**Manual override always available:** Regardless of rule configuration, the salesperson can manually set any berth status at any time from the berth detail panel. Manual changes are never blocked by the rules engine — rules only govern _automatic_ or _suggested_ transitions.
|
|||
|
|
|
|||
|
|
**Audit trail:** Every status change (auto, suggested-and-accepted, or manual) is logged in the audit log with: old status, new status, trigger action (or "manual"), rule mode used, and the user who accepted/performed it.
|
|||
|
|
|
|||
|
|
### BR-002: Berth Status on Website Map
|
|||
|
|
|
|||
|
|
- Public API serves berth status directly. Website map color-codes: green = available, orange = under_offer, red = sold.
|
|||
|
|
- Status changes propagate to the website in real-time via the public API (no caching on berth status).
|
|||
|
|
|
|||
|
|
### BR-003: Berth Tenure Expiration
|
|||
|
|
|
|||
|
|
- **Trigger:** Daily job checks all fixed-term berths
|
|||
|
|
- **Condition:** Berth `tenure_end_date` is within configurable warning period (default: 6 months)
|
|||
|
|
- **Action:** Create notification for admin. If waiting list entries exist, create notifications for waiting list clients.
|
|||
|
|
- **On expiration:** Berth does NOT auto-change status. Admin must manually decide next steps.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 2. Interest Pipeline Rules
|
|||
|
|
|
|||
|
|
### BR-010: Pipeline Stages
|
|||
|
|
|
|||
|
|
Ordered stages: `open` → `details_sent` → `in_communication` → `visited` → `signed_eoi_nda` → `deposit_10pct` → `contract` → `completed`
|
|||
|
|
|
|||
|
|
- All forward and backward transitions are allowed
|
|||
|
|
- Salesperson can set any stage at any time (no enforced progression)
|
|||
|
|
- Stage changes are logged in the audit log with before/after values
|
|||
|
|
|
|||
|
|
### BR-011: Auto-Promote on Vessel Dimensions
|
|||
|
|
|
|||
|
|
- **Trigger:** Interest updated with yacht length, width, and draft values (all three populated)
|
|||
|
|
- **Condition:** Current `lead_category` is `general_interest`
|
|||
|
|
- **Action:** Auto-promote `lead_category` to `specific_qualified`
|
|||
|
|
- **Override:** Category can be manually changed back
|
|||
|
|
|
|||
|
|
### BR-012: Auto-Stage on EOI Send
|
|||
|
|
|
|||
|
|
- **Trigger:** EOI document generated and sent via Documenso
|
|||
|
|
- **Action:** Set `eoi_status` = `waiting_for_signatures`, advance `pipeline_stage` to `signed_eoi_nda` (if not already at or past that stage)
|
|||
|
|
- **Override:** Stage can be manually changed
|
|||
|
|
|
|||
|
|
### BR-013: Auto-Stage on Manual EOI Upload
|
|||
|
|
|
|||
|
|
- **Trigger:** Manually uploaded signed EOI document
|
|||
|
|
- **Action:** Set `eoi_status` = `signed`, advance `pipeline_stage` to `signed_eoi_nda`
|
|||
|
|
- **Override:** Stage can be manually changed
|
|||
|
|
|
|||
|
|
### BR-014: Interest Archiving
|
|||
|
|
|
|||
|
|
- Archived interests are excluded from: active pipeline views, dashboard metrics, berth occupancy counts, auto-status calculations
|
|||
|
|
- Archived interests remain searchable via archive view and global search
|
|||
|
|
- Archiving an interest that is the sole link to a berth triggers BR-002 (berth status reset check)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 3. EOI & Document Rules
|
|||
|
|
|
|||
|
|
### BR-020: EOI Generation Prerequisites
|
|||
|
|
|
|||
|
|
Generation is blocked unless ALL of:
|
|||
|
|
|
|||
|
|
1. Client has: full name, at least one email contact
|
|||
|
|
2. Interest has: yacht name, yacht length, yacht width, yacht draft
|
|||
|
|
3. At least one berth linked to the interest
|
|||
|
|
4. No existing manual/uploaded EOI documents on the interest (unless user explicitly overrides)
|
|||
|
|
|
|||
|
|
### BR-021: EOI Signing Order
|
|||
|
|
|
|||
|
|
- Sequential 3-party signing: Client (order 1) → Developer (order 2) → Sales/Approver (order 3)
|
|||
|
|
- Each signer receives notification only after the previous signer completes
|
|||
|
|
- If any signer declines, the document is marked as `declined` and the interest's `eoi_status` updates accordingly
|
|||
|
|
|
|||
|
|
### BR-022: EOI Completion
|
|||
|
|
|
|||
|
|
- **Trigger:** All signers have signed (Documenso `DOCUMENT_COMPLETED` event)
|
|||
|
|
- **Actions:**
|
|||
|
|
1. Download completed signed PDF from Documenso
|
|||
|
|
2. Store in MinIO under client's EOI folder
|
|||
|
|
3. Email signed PDF to all three parties
|
|||
|
|
4. Set `eoi_status` = `signed`
|
|||
|
|
5. Timestamp `all_signed_notified_at`
|
|||
|
|
|
|||
|
|
### BR-023: Signature Reminders
|
|||
|
|
|
|||
|
|
- Gated by: per-interest `reminder_enabled` toggle AND system-wide cooldown window
|
|||
|
|
- Send window: configurable hours (e.g., 09:00-16:00 in port's timezone)
|
|||
|
|
- Cooldown: minimum time between reminders for the same document (configurable, default 24h)
|
|||
|
|
- Delivered via Poste.io transactional email
|
|||
|
|
|
|||
|
|
### BR-024: Document Deduplication
|
|||
|
|
|
|||
|
|
- Documenso webhook events deduplicated via `signature_hash` unique index on `document_events`
|
|||
|
|
- Prevents double-processing when webhooks are retried or fallback polling fires simultaneously
|
|||
|
|
|
|||
|
|
### BR-025: Generic Document Signing
|
|||
|
|
|
|||
|
|
- Any document type (not just EOIs) can be sent through Documenso
|
|||
|
|
- Same webhook infrastructure handles all document types
|
|||
|
|
- Document type tag determines which pipeline auto-actions fire (only EOI type triggers pipeline stage changes)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 4. Client & Duplicate Rules
|
|||
|
|
|
|||
|
|
### BR-030: Website Registration Duplicate Check
|
|||
|
|
|
|||
|
|
- **Trigger:** New interest registration from website (`POST /api/public/interests`)
|
|||
|
|
- **Check:** Does the submitted email match any existing `client_contacts` entry where `channel = 'email'`?
|
|||
|
|
- **Match found:** Create new interest under existing client. Do NOT create new client. Notify salesperson of new interest on existing client.
|
|||
|
|
- **No match:** Create new client + new interest. Run fuzzy duplicate check (BR-031).
|
|||
|
|
|
|||
|
|
### BR-031: Fuzzy Duplicate Detection
|
|||
|
|
|
|||
|
|
- **Trigger:** New client created (manual or website)
|
|||
|
|
- **Matching criteria (scored):**
|
|||
|
|
- Same email across any contact entry: score 1.0 (this triggers auto-merge per BR-030)
|
|||
|
|
- Same phone number: score 0.9
|
|||
|
|
- Similar name (Levenshtein distance < 3) + same phone: score 0.8
|
|||
|
|
- Similar name + similar address: score 0.7
|
|||
|
|
- **Threshold:** Score ≥ 0.7 creates a duplicate alert for manual review
|
|||
|
|
- **Below threshold:** No alert
|
|||
|
|
|
|||
|
|
### BR-032: Client Merge
|
|||
|
|
|
|||
|
|
- Surviving client keeps all their own data
|
|||
|
|
- Merged client's data fills in any blank fields on the surviving record
|
|||
|
|
- All of the merged client's interests, notes, files, timeline entries, and relationships transfer to the surviving client
|
|||
|
|
- The merge is logged in `client_merge_log` with full detail of which fields came from which record
|
|||
|
|
- The merged client record is deleted after transfer
|
|||
|
|
- Merge is reversible from the audit log (super admin only)
|
|||
|
|
|
|||
|
|
### BR-033: Notes Edit Window
|
|||
|
|
|
|||
|
|
- Notes (on clients or interests) are editable by the author for 15 minutes after creation
|
|||
|
|
- After 15 minutes, notes are locked (`is_locked = true`)
|
|||
|
|
- Locked notes cannot be edited or deleted (except by super admin via audit log revert)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 5. Financial Rules
|
|||
|
|
|
|||
|
|
### BR-040: Expense Currency Conversion
|
|||
|
|
|
|||
|
|
- All expenses store: original `amount` + `currency`, plus `amount_usd` (converted) and `exchange_rate` used
|
|||
|
|
- Conversion performed at time of creation using current rate from `currency_rates` table
|
|||
|
|
- Primary business currency: USD
|
|||
|
|
- Local currency: ECD (Eastern Caribbean Dollar)
|
|||
|
|
- Common client currencies: EUR, GBP
|
|||
|
|
- If rate unavailable, expense is saved without conversion and flagged for manual rate entry
|
|||
|
|
|
|||
|
|
### BR-041: Invoice Auto-Numbering
|
|||
|
|
|
|||
|
|
- Format: `INV-YYYYMM-###`
|
|||
|
|
- Sequential within each port per month
|
|||
|
|
- Example: INV-202603-001, INV-202603-002
|
|||
|
|
- Counter resets each month
|
|||
|
|
|
|||
|
|
### BR-042: Invoice Net 10 Discount
|
|||
|
|
|
|||
|
|
- When `payment_terms` = `net10`, apply 2% discount on subtotal
|
|||
|
|
- Discount calculated automatically on invoice creation/update
|
|||
|
|
- `discount_pct` = 2, `discount_amount` = subtotal × 0.02
|
|||
|
|
- Configurable: the 2% rate can be changed in system settings
|
|||
|
|
|
|||
|
|
### BR-043: Parent Company Export
|
|||
|
|
|
|||
|
|
- Expense export for parent company includes:
|
|||
|
|
- All expenses in selection with receipt images
|
|||
|
|
- EUR subtotal (all expenses converted to EUR)
|
|||
|
|
- 5% processing fee on the EUR subtotal
|
|||
|
|
- Configurable: the 5% rate can be changed in system settings
|
|||
|
|
|
|||
|
|
### BR-044: Invoice Overdue Detection
|
|||
|
|
|
|||
|
|
- **Trigger:** Daily job checks all invoices with status `sent`
|
|||
|
|
- **Condition:** `due_date` < today
|
|||
|
|
- **Action:** Update status to `overdue`, create notification for invoice creator and admin
|
|||
|
|
|
|||
|
|
### BR-045: Invoice-Expense Integrity
|
|||
|
|
|
|||
|
|
- When creating an invoice from expenses, both the invoice and each linked expense are updated in a single database transaction
|
|||
|
|
- If any part fails, the entire operation rolls back
|
|||
|
|
- Bidirectional link maintained via `invoice_expenses` junction table (no comma-separated IDs)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 6. Notification Rules
|
|||
|
|
|
|||
|
|
### BR-050: Notification Creation
|
|||
|
|
|
|||
|
|
Notifications are created by the system (not users). Events that trigger notifications:
|
|||
|
|
|
|||
|
|
| Event | Recipient(s) | Type |
|
|||
|
|
| ------------------------------------- | ---------------------------------------------- | ------------------- |
|
|||
|
|
| New website registration | Salesperson(s) at port | `new_registration` |
|
|||
|
|
| Reminder due today | Reminder assignee | `reminder_due` |
|
|||
|
|
| Reminder overdue | Reminder assignee + creator | `reminder_overdue` |
|
|||
|
|
| EOI signer completed | Next signer + interest owner | `eoi_signed` |
|
|||
|
|
| All signers completed | All signers + interest owner | `eoi_completed` |
|
|||
|
|
| Duplicate client detected | All users with `clients.create` permission | `duplicate_alert` |
|
|||
|
|
| Invoice overdue | Invoice creator + admin | `invoice_overdue` |
|
|||
|
|
| Berth became available (waiting list) | Waiting list clients (via email) + salesperson | `waiting_list` |
|
|||
|
|
| Background job failure | Super admin | `system_alert` |
|
|||
|
|
| Follow-up reminder auto-created | Interest owner / assigned salesperson | `follow_up_created` |
|
|||
|
|
| Berth tenure expiring | Admin | `tenure_expiring` |
|
|||
|
|
|
|||
|
|
### BR-051: Notification Preferences
|
|||
|
|
|
|||
|
|
- Each user can configure per-notification-type: in-app (on/off), email (on/off)
|
|||
|
|
- System alerts to super admin are always delivered (cannot be suppressed)
|
|||
|
|
- Defaults: all notification types on for both in-app and email
|
|||
|
|
|
|||
|
|
### BR-052: Notification Cooldown
|
|||
|
|
|
|||
|
|
- Notifications of the same type for the same entity are throttled
|
|||
|
|
- Default cooldown: 1 hour (configurable per notification type)
|
|||
|
|
- Prevents notification spam for rapid successive events
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 7. Reminder & Calendar Rules
|
|||
|
|
|
|||
|
|
### BR-060: Follow-Up Reminder (Auto-Generated)
|
|||
|
|
|
|||
|
|
- **Trigger:** Interest has `reminder_enabled = true` and `reminder_days` set
|
|||
|
|
- **Check:** Has there been any activity (note, status change, email, call log) on this interest in the last `reminder_days` days?
|
|||
|
|
- **No activity:** Create a CRM reminder "Follow up with {client name}" assigned to the interest's salesperson (marked `auto_generated = true`), and create a notification
|
|||
|
|
- **Activity found:** Reset the timer. Check again in `reminder_days` days.
|
|||
|
|
- **Schedule:** Checked hourly via BullMQ recurring job
|
|||
|
|
- **Google Calendar:** Auto-generated follow-up reminders are NOT automatically pushed to Google Calendar (salesperson can manually toggle sync on each one)
|
|||
|
|
|
|||
|
|
### BR-061: Google Calendar Sync
|
|||
|
|
|
|||
|
|
- **Push:** When a reminder is created/updated with `sync_to_calendar = true`, create/update a Google Calendar event via the API. Store the `google_calendar_event_id` on the reminder record.
|
|||
|
|
- **Pull:** Three sync triggers: (1) Background poll every 30 minutes for all connected users, (2) On user login if calendar is connected, (3) On navigation to any calendar-displaying page (dashboard, reminders, client detail) if last sync > 5 min ago. All syncs fetch upcoming events (next 14 days) and upsert into `google_calendar_cache`.
|
|||
|
|
- **Two-way detection:** On each pull sync, if a CRM-pushed event was deleted or moved in Google Calendar, update the corresponding CRM reminder's `due_at` or mark as `dismissed`.
|
|||
|
|
- **Token refresh:** If access token is expired, use refresh token to obtain a new one. If refresh fails (revoked), mark the connection as disconnected and notify the user.
|
|||
|
|
- **Scope:** Calendar sync is per-user, not per-port. A user's calendar connection works across all ports they have access to.
|
|||
|
|
|
|||
|
|
### BR-062: Reminder Snooze
|
|||
|
|
|
|||
|
|
- **Action:** Salesperson clicks "Snooze" on a reminder → selects snooze duration (1 hour, 4 hours, tomorrow, next week, custom)
|
|||
|
|
- **Effect:** Reminder status changes to `snoozed`, `snoozed_until` is set. Reminder resurfaces as `pending` when the snooze time passes.
|
|||
|
|
- **Google Calendar:** If synced, the Google Calendar event is updated to the new time.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 8. Multi-Tenancy Rules
|
|||
|
|
|
|||
|
|
### BR-070: Port Scoping
|
|||
|
|
|
|||
|
|
- Every database query for port-scoped tables MUST include `WHERE port_id = :currentPortId`
|
|||
|
|
- Middleware extracts current port from session/header and injects into request context
|
|||
|
|
- Drizzle query builder enforced via wrapper functions: `withPortScope(query, portId)`
|
|||
|
|
- Missing port context = 400 error (never fall through to unscoped query)
|
|||
|
|
|
|||
|
|
### BR-071: Super Admin Bypass
|
|||
|
|
|
|||
|
|
- Super admin (`is_super_admin = true`) can:
|
|||
|
|
- Query across all ports (omit port_id filter)
|
|||
|
|
- Access any port's admin panel
|
|||
|
|
- Manage global roles and system settings
|
|||
|
|
- View unified cross-port dashboard
|
|||
|
|
- Still respects audit logging (all super admin actions logged)
|
|||
|
|
|
|||
|
|
### BR-072: Port Switcher & Single-Port Mode
|
|||
|
|
|
|||
|
|
- **Single-port mode:** When only 1 active port exists, all multi-port UI is hidden (port switcher, port columns in tables, port selection on entity forms, port filter dropdowns). Port context set automatically behind the scenes.
|
|||
|
|
- **Multi-port mode:** When 2+ active ports exist, users with roles at multiple ports see a port switcher. Switching port changes all data context immediately.
|
|||
|
|
- Mode is derived dynamically from `SELECT count(*) FROM ports WHERE is_active = true` — no manual toggle needed.
|
|||
|
|
- Current port stored in session, not URL (avoid URL manipulation)
|
|||
|
|
- Port switch logged in audit log
|
|||
|
|
|
|||
|
|
### BR-073: New Port Setup
|
|||
|
|
|
|||
|
|
- Creating a new port auto-inherits: all global roles, all global system settings
|
|||
|
|
- Port-specific overrides start empty (inherit globals until overridden)
|
|||
|
|
- Onboarding wizard guides: port details → berth import → user assignment → branding
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 9. Audit & Undo Rules
|
|||
|
|
|
|||
|
|
### BR-080: Audit Log Completeness
|
|||
|
|
|
|||
|
|
- Every create, update, delete, archive, restore, merge, login, logout action is logged
|
|||
|
|
- Update logs include: field name, old value (JSON), new value (JSON)
|
|||
|
|
- No exceptions — even bulk operations log individual changes
|
|||
|
|
|
|||
|
|
### BR-081: Undo Eligibility
|
|||
|
|
|
|||
|
|
- **Undoable:** Field value changes, status changes, berth link/unlink, tag changes, role changes
|
|||
|
|
- **Not undoable:** File deletions (file removed from storage), sent emails, signed documents, completed Documenso flows
|
|||
|
|
- Undo creates a new audit entry with `revert_of` pointing to the original change
|
|||
|
|
- Undo only available to super admin
|
|||
|
|
|
|||
|
|
### BR-082: Audit Retention
|
|||
|
|
|
|||
|
|
- Audit logs are never automatically deleted
|
|||
|
|
- Exportable as CSV for archival
|
|||
|
|
- Future: configurable retention policy (archive logs older than X years)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 10. File Management Rules
|
|||
|
|
|
|||
|
|
### BR-090: Client-Grouped Storage
|
|||
|
|
|
|||
|
|
- Files uploaded via a client context are stored in `/clients/{client_id}/{category}/`
|
|||
|
|
- Category determined by upload context (EOI page → eoi folder, expense → receipts folder)
|
|||
|
|
- Files uploaded without client context go to `/port/{port_id}/general/`
|
|||
|
|
|
|||
|
|
### BR-091: File Deletion
|
|||
|
|
|
|||
|
|
- File deletion removes: MinIO object + `files` table entry
|
|||
|
|
- Deletion logged in audit log (with filename and path for reference)
|
|||
|
|
- Files referenced by other records (e.g., receipt on an expense, PDF on an invoice) cannot be deleted until the reference is removed
|
|||
|
|
|
|||
|
|
### BR-092: S3 Migration
|
|||
|
|
|
|||
|
|
- One-time migration job: scan all existing MinIO files → attempt to match to clients → propose reorganization
|
|||
|
|
- Uncertain matches flagged for manual review
|
|||
|
|
- Migration runs as a BullMQ job with progress tracking
|
|||
|
|
- Rollback: original paths stored in migration log, can undo file moves
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 11. Search Rules
|
|||
|
|
|
|||
|
|
### BR-100: Global Search Scope
|
|||
|
|
|
|||
|
|
- Searches: client full name, company name, email, phone, yacht name; berth mooring number, area; interest pipeline stage; document titles; invoice numbers; expense establishment names; note content
|
|||
|
|
- Results scoped to current port (unless super admin doing cross-port search)
|
|||
|
|
- Archived records included in results but marked as archived
|
|||
|
|
- Results ranked by relevance with entity type grouping
|
|||
|
|
|
|||
|
|
### BR-101: Duplicate Search on Create
|
|||
|
|
|
|||
|
|
- Before creating a new client, the create form runs a real-time search to show potential matches
|
|||
|
|
- Uses same fuzzy matching as BR-031 but displays as suggestions, not blocks
|
|||
|
|
- User can choose to proceed with creation or select an existing client
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 12. Webhook Rules
|
|||
|
|
|
|||
|
|
### BR-110: Webhook Delivery
|
|||
|
|
|
|||
|
|
- Webhooks fire asynchronously via BullMQ (do not block the triggering operation)
|
|||
|
|
- Retry: 3 attempts with exponential backoff (1s, 10s, 100s)
|
|||
|
|
- After 3 failures: move to dead letter queue, create system alert
|
|||
|
|
- Payload signed with HMAC-SHA256 using the webhook's secret (header: `X-CRM-Signature`)
|
|||
|
|
|
|||
|
|
### BR-111: Webhook Events
|
|||
|
|
|
|||
|
|
Events that can trigger webhooks:
|
|||
|
|
`client.created`, `client.updated`, `interest.created`, `interest.updated`, `interest.stage_changed`, `berth.status_changed`, `document.signed`, `document.completed`, `invoice.created`, `invoice.paid`, `expense.created`, `registration.new`
|
|||
|
|
|
|||
|
|
### BR-112: Webhook Port Scoping
|
|||
|
|
|
|||
|
|
- Webhooks are port-scoped: each webhook belongs to a specific port and only fires for events within that port
|
|||
|
|
- Super admin cross-port actions trigger the webhook for the port where the data resides (not the super admin's "home" port)
|
|||
|
|
- Webhook `port_id` is set on creation and cannot be changed (create a new webhook for a different port)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 13. Scheduled Report Rules
|
|||
|
|
|
|||
|
|
### BR-120: Report Generation
|
|||
|
|
|
|||
|
|
- Reports generated as PDF files via BullMQ scheduled job
|
|||
|
|
- Date range automatically calculated: daily (last 24h), weekly (last 7 days), monthly (last calendar month)
|
|||
|
|
- Generated PDFs stored temporarily in MinIO, attached to email, then cleaned up after 7 days
|
|||
|
|
- If generation fails, retry once, then create system alert
|
|||
|
|
|
|||
|
|
### BR-121: Report Delivery
|
|||
|
|
|
|||
|
|
- Sent via Poste.io to all configured recipients
|
|||
|
|
- Both CRM users (by user ID → email) and external email addresses supported
|
|||
|
|
- Delivery failures logged but do not prevent delivery to other recipients
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 14. Form & Credential Rules
|
|||
|
|
|
|||
|
|
### BR-130: Form Expiry
|
|||
|
|
|
|||
|
|
- Form submission tokens have a configurable expiry (default: 7 days)
|
|||
|
|
- **Trigger:** Hourly background job checks `form_submissions` for `status = 'pending'` AND `expires_at < now()`
|
|||
|
|
- **Action:** Set status to `expired`. Expired forms return a friendly "this link has expired" page.
|
|||
|
|
- Salesperson can regenerate a new form link for the same client/interest
|
|||
|
|
|
|||
|
|
### BR-131: Email Account Credential Security
|
|||
|
|
|
|||
|
|
- User email credentials encrypted with AES-256-GCM before storage in `email_accounts.credentials_enc`
|
|||
|
|
- Encryption key stored in environment variable (never in database or source code)
|
|||
|
|
- Credentials decrypted only at point of use (SMTP/IMAP connection), never logged or exposed in API responses
|
|||
|
|
- On credential update, old credentials are overwritten (not versioned)
|
|||
|
|
- Failed IMAP/SMTP connection after 3 consecutive attempts disables the account and notifies the user
|
|||
|
|
|
|||
|
|
### BR-132: Custom Field Validation
|
|||
|
|
|
|||
|
|
- Required custom fields are enforced on entity create/update
|
|||
|
|
- Select-type fields validate against defined options
|
|||
|
|
- Deleting a custom field definition also deletes all corresponding values (cascade)
|
|||
|
|
- Custom fields are included in export operations
|
|||
|
|
|
|||
|
|
### BR-133: Milestone Auto-Population
|
|||
|
|
|
|||
|
|
- When a document of type `eoi` is sent: auto-set `interest.date_eoi_sent` (if not already set)
|
|||
|
|
- When a document of type `eoi` is completed (all signed): auto-set `interest.date_eoi_signed`
|
|||
|
|
- When a document of type `contract` is sent: auto-set `interest.date_contract_sent`
|
|||
|
|
- When a document of type `contract` is completed: auto-set `interest.date_contract_signed`
|
|||
|
|
- All auto-populated dates are overridable by manual edit
|
|||
|
|
|
|||
|
|
### BR-134: Waiting List Priority Notification Order
|
|||
|
|
|
|||
|
|
- When a berth becomes available (status changes to `available` from `under_offer` or `sold`):
|
|||
|
|
- High-priority waiting list clients are notified first
|
|||
|
|
- Within same priority level, notify in position order (first in line first)
|
|||
|
|
- Notifications staggered: 1 hour delay between each client notification to allow first responders to claim
|
|||
|
|
- First client to respond and confirm interest gets the berth linked (salesperson confirms manually)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 15. Document Template Rules
|
|||
|
|
|
|||
|
|
### BR-140: Merge Field Resolution
|
|||
|
|
|
|||
|
|
- When generating a document from a template, all `{{entity.field}}` tokens are resolved against the provided context (client, interest, berth, port)
|
|||
|
|
- Missing required context (e.g., template references `{{berth.mooring_number}}` but no berth provided) → return validation error listing missing data
|
|||
|
|
- Optional merge fields that resolve to null/empty are replaced with empty string (not left as raw tokens)
|
|||
|
|
- Merge fields support nested access: `{{client.contacts.primary_email}}`, `{{interest.berth.mooring_number}}`
|
|||
|
|
|
|||
|
|
### BR-141: Template Management
|
|||
|
|
|
|||
|
|
- Only users with `admin.manage_forms` permission can create, edit, or delete templates
|
|||
|
|
- All users with `documents.create` permission can generate documents from templates
|
|||
|
|
- Deleting a template does NOT delete previously generated documents — they are standalone files
|
|||
|
|
- Templates are port-scoped (each port has its own template library)
|
|||
|
|
|
|||
|
|
### BR-142: Generated Document Storage
|
|||
|
|
|
|||
|
|
- Documents generated from templates are stored as PDF files in MinIO under the client's correspondence folder
|
|||
|
|
- A `documents` table entry is created with `document_type = 'template_generated'` and reference to the source template
|
|||
|
|
- Generated documents appear in the client's file list and document history
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 16. Record PDF Export Rules
|
|||
|
|
|
|||
|
|
### BR-150: PDF Branding
|
|||
|
|
|
|||
|
|
- All record export PDFs include the port's logo and primary color from `ports` settings
|
|||
|
|
- If no logo configured, a text-only header with the port name is used
|
|||
|
|
- PDF layout is consistent across all record types (header, content sections, footer with generation timestamp)
|
|||
|
|
|
|||
|
|
### BR-151: PDF Content Scope
|
|||
|
|
|
|||
|
|
- Client PDF: includes all non-archived data — contacts, vessel details, linked interests with current status, recent 20 activity entries, files list (names only, not embedded)
|
|||
|
|
- Berth PDF: includes full spec sheet data, current status, pricing in all configured currencies, linked interests summary, maintenance log summary
|
|||
|
|
- Interest PDF: includes client info, berth info, pipeline stage, EOI/contract status, all milestones, all notes, recent 20 timeline entries
|
|||
|
|
- Custom fields are included in exports for the relevant entity type
|
|||
|
|
|
|||
|
|
### BR-152: PDF Generation
|
|||
|
|
|
|||
|
|
- PDFs generated server-side via @pdfme (same library used for EOI generation)
|
|||
|
|
- Generation runs synchronously for single-record exports (no background job needed — fast enough)
|
|||
|
|
- If generation fails, return error to user (no silent failure)
|