Files
pn-new-crm/docs/POST-AUDIT-SPEC-2026-05-18.md
Matt 0f99f054b3 feat(post-audit): batch A+B quick-wins + audit-side residuals
Bundles the user-prioritised follow-ups from the post-audit punch-list.

Batch A — pipeline + EOI safety:
 - §1.1 timeline buildAuditDescription renders diff fields ("leadCategory → hot_lead").
 - §4.13 EOI rejection cascade: notification to assigned rep + audit row + rose banner.
 - §4.10b finish doc-detail: SigningProgress reuse, linked-entity names (server-resolved),
   per-event icons + tooltips + show-more in activity panel.
 - §7.2 stage guidance card replaces empty Payments slot pre-reservation.
 - §4.15 deal-pulse trigger audit (docs/deal-pulse-trigger-audit.md).

Batch B — UX consistency + docs:
 - §1.4 quick log-contact button on interest header.
 - §2.1 contact-log compose: Dialog → Sheet.
 - §7.1 docs/deal-pulse explainer page; /docs/ in PUBLIC_PATHS.
 - DocumentStatus now includes 'rejected' + 'declined' across constants, labels, tone maps.

Audit-side residuals:
 - M-NEW-1 /me/ports skips port-context requirement.
 - M-AU03 audit log CSV export endpoint + UI button.
 - M-IN03 dead receipt-scanner.ts deleted; live path already per-port.
 - M-P01 pg_trgm GIN indexes (migration 0071).
 - §10.1 webhook tests verified passing (was stale).

Deferred per user direction:
 - §11.3 email copy refactor (needs old-CRM reference).
 - M-EM03 IMAP bounce-to-interest linking.

Tests: 1374/1374. tsc + lint clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 14:22:11 +02:00

9.5 KiB
Raw Blame History

Post-Audit Implementation Spec — 2026-05-18

Captures the design decisions from the post-audit conversation so the implementation can start without re-litigating the trade-offs. Each section ends with an Effort estimate.


1. EOI document field overrides

Goal

When generating an EOI, the rep should be able to override pre-filled field values (contact info, addresses, yacht details) while preserving the canonical record. Manual entries persist as tracked secondary values so future EOIs can pick them up from a dropdown.

Design

Client contact channels (email, phone):

  • The EOI form's email/phone fields render as a dropdown of every client_contacts row for the linked client, defaulting to the primary for each channel.
  • Rep types a brand-new value → on EOI save, a new client_contacts row is created with is_primary=false, source='eoi-custom-input', source_document_id=<doc-id>. Labelled [EOI] on the client detail page contacts panel.
  • The current EOI uses the new value; future EOIs default to primary unless the rep explicitly picks the new row from the dropdown.
  • A "Set as default for future documents" toggle on the EOI form promotes the new value to is_primary=true (demoting the prior primary).

Client addresses: Same pattern via client_addresses (which is already multi-value per CLAUDE.md).

Yacht name + dimensions: Yachts are single-valued; rep needs a different yacht → opens a "Create yacht" modal inline, fills in name + dims for the new yacht record, linked to the same client/interest, tagged eoi-generated. The EOI uses the new yacht. The original yacht is unchanged. (No yacht_aliases / yacht_dimension_overrides table.)

Interest-specific fields (rare): Same dropdown pattern via the existing fields on the interest record. Custom entries promote-or-stay following the toggle.

Audit trail: Every override action (create-non-primary, promote-to- primary, create-yacht-from-eoi) emits an audit_log row with action eoi_field_override and metadata identifying the source document.

Per-document override (no record-side write): Doc-level overrides remain available as a checkbox — when ticked, the value lives only on the doc and never touches client_contacts. Default is unchecked.

Schema additions

  • client_contacts.source text — extend the existing enum: 'manual', 'imported', 'eoi-custom-input'.
  • client_contacts.source_document_id text references documents(id) on delete set null — surfaces the originating EOI.
  • client_addresses.source + source_document_id (mirror).
  • yachts.source + source_document_id (mirror; nullable so existing records aren't disturbed).
  • audit_actions enum gains eoi_field_override + promote_to_primary.

UI

  • EOI Generate drawer: each editable field becomes either a <Combobox> (when multi-value) or <Input> + "Save as new …" hint (yacht).
  • Below each field: [ ] Use only for this EOI checkbox (default off)
    • [ ] Set as default for future docs checkbox (default off).
  • Client + Yacht detail panels: [EOI] badge on non-primary rows; "Set as primary" action on each.

Effort

~11.5 weeks. Bundle the schema + EOI form + client/yacht detail UI into one PR (user picked "All at once").

Open implementation questions

  • The yacht-creation inline modal needs the existing YachtForm wired in; on save it tags the new yacht with the eoi-generated marker. Tag the yacht via tags? Or a dedicated source column? Recommend column for queryability.
  • Should [EOI] badges fade out after a TTL or stay forever? Recommend forever — the rep deliberately chose this label.

2. Reminders

Goal

Reps can: per-interest follow-up cadence with note + time, standalone tasks (no entity), assignable-to-another-rep tasks. The existing rich reminders table holds the canonical data; the per-interest cadence on the interests row stays for backward compat as a quick-tick.

Design

Per-interest cadence (kept):

  • interests.reminderEnabled + interests.reminderDays retained.
  • New: interests.reminderNote text NULL — surfaced in the notification body + the inbox row.
  • The cadence fires a row into reminders on each tick (with interest_id set) instead of the current ad-hoc notification flow, unifying the inbox.

Standalone tasks (new):

  • Rich reminders table already has every column we need (title, note, priority, due_at, assigned_to, snoozed_until, google_calendar_event_id).
  • Two UI surfaces (both submit to the same dialog component):
    • RemindersInbox top-right [+ New task] button.
    • Per-entity detail page (interest, client, berth, yacht): [+ Task] button inside the existing Reminders section. Linked-entity field pre-filled and locked.
  • The dialog: Title (required), Note (optional), Due date+time, Priority, Assign to (default = current rep), Linked entity (optional dropdown for inbox surface; locked for per-entity).

Time-of-day:

  • New user-settings field: digest_time_of_day time, default '09:00'. Stored in user_profiles.
  • Per-reminder override: each reminder's due_at carries the exact firing moment (existing column). The dialog defaults the time picker to the user's digest_time_of_day but lets them override per row.
  • Worker scheduler: a 15-min cron tick scans reminders for rows whose due_at <= now() AND fired_at IS NULL, fires the notification, sets fired_at.

Assignment:

  • reminders.assigned_to (existing). Dialog has an "Assign to" picker (port users via /api/v1/admin/users/picker), defaults to current user.
  • Inbox shows the assignee chip when not me; filter [Mine | All my port].

Schema additions

  • interests.reminder_note text NULL
  • user_profiles.digest_time_of_day time NOT NULL DEFAULT '09:00'
  • reminders.fired_at timestamptz NULL (new — drives the worker idempotency)
  • No new tables. The existing reminders table covers standalone tasks.

UI

  • <CreateReminderDialog> component (shared).
  • RemindersInbox: [+ New task] button → dialog (linked entity blank).
  • Interest / client / berth / yacht detail pages: existing Reminders section gains [+ Task] button → dialog (linked entity pre-filled, field disabled).
  • Settings page: time picker for "default reminder time" → writes user_profiles.digest_time_of_day.

Effort

~34 days. Schema migration + dialog component + 4 entity-page wires

  • worker scheduler refactor + inbox filter.

3. Supplemental info form — per-port setting

Goal

The "Send supplemental info form" link in the auto-email should resolve to the marketing site when configured; fall back to a CRM-hosted route otherwise. Confirmed: per-port setting.

Design

  • New system_settings key: supplemental_form_url (per-port, optional, text). Defaults to NULL.
  • Link generator in the email service:
    const url = cfg.supplementalFormUrl
      ? `${cfg.supplementalFormUrl}?token=${raw}`
      : `${env.APP_URL}/supplemental/${raw}`;
    
  • Existing /supplemental/[token] CRM route stays as the fallback. Add a "Loading…" skeleton + dual-mode copy ("If you don't see your details, contact your rep").
  • Admin UI: add the field to /admin/email/page.tsx (or a new /admin/supplemental/page.tsx) — single text input with the help hint "Leave blank to use the built-in CRM page."

Effort

~2 hours (single setting + 1 admin field + link resolver).


4. Documenso phases 2 → 7 → 5 (you picked Phase 7 first)

Phase 7 — Project Director RBAC (~1h)

  • Add "Linked to CRM user" dropdown in /admin/documenso/page.tsx pointing at the existing developer_user_id + approver_user_id settings.
  • Auto-fill name/email from the selected user (read via /api/v1/admin/users/picker).
  • Webhook handler in src/app/api/webhooks/documenso/route.ts: when an event arrives for the developer or approver, also fire an in-CRM documenso:signed notification routed to the linked user's CRM notifications inbox.

Phase 2 — Webhook handler enhancement (~34h)

  • Cascading "your turn" emails: when signer N completes, fire an invitation email to signer N+1 (sequential signing only).
  • On-completion PDF distribution: when status flips to COMPLETED, email the signed PDF to all documents.completion_cc_emails.
  • Token-based recipient matching: prefer signing_token over email for webhook → signer resolution (handles aliased emails).
  • Idempotency lock: replace the current body-hash dedup with a composite (documensoDocumentId, recipientEmail, eventType) unique constraint on documentEvents.
  • Schema is already in place from Phase 1 — this is pure handler logic.

Phase 5 — Embedded signing URL verification (~12h)

  • Confirm the marketing site's /sign/<type>/<token> page handles every signer-role × documentType combo.
  • Update signerMessages map in the signing-invitation email template to surface role-specific copy.
  • Apply nginx CORS block from the integration audit (constrain Documenso webhook origin).

Effort total

~67h across the three phases. Phase 4 (field placement UI, 1014h) stays deferred — covered separately by the PDF template editor work you picked Phases 1+2 for.


What I'll build first

Per your sequencing:

  1. Documenso Phase 7 (~1h) — unblock the linked-user signing UX.
  2. Supplemental form per-port setting (~2h) — small win.
  3. Documenso Phase 2 (~34h) — meaningful UX improvement.
  4. Documenso Phase 5 (~12h) — security + role copy.
  5. EOI field overrides + reminders (~1.5 weeks combined) — the big ones, picked up after the Documenso quick wins land.