Files
pn-new-crm/09-BUSINESS-RULES.md
Matt 67d7e6e3d5
Some checks failed
Build & Push Docker Images / build-and-push (push) Has been cancelled
Build & Push Docker Images / deploy (push) Has been cancelled
Build & Push Docker Images / lint (push) Has been cancelled
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00

25 KiB
Raw Permalink Blame History

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: opendetails_sentin_communicationvisitedsigned_eoi_ndadeposit_10pctcontractcompleted

  • 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)