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>
9.5 KiB
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_contactsrow for the linked client, defaulting to the primary for each channel. - Rep types a brand-new value → on EOI save, a new
client_contactsrow is created withis_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_actionsenum gainseoi_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 EOIcheckbox (default off)[ ] Set as default for future docscheckbox (default off).
- Client + Yacht detail panels:
[EOI]badge on non-primary rows; "Set as primary" action on each.
Effort
~1–1.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 dedicatedsourcecolumn? 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.reminderDaysretained.- New:
interests.reminderNote text NULL— surfaced in the notification body + the inbox row. - The cadence fires a row into
reminderson each tick (withinterest_idset) instead of the current ad-hoc notification flow, unifying the inbox.
Standalone tasks (new):
- Rich
reminderstable 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.
- RemindersInbox top-right
- 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_atcarries the exact firing moment (existing column). The dialog defaults the time picker to the user'sdigest_time_of_daybut lets them override per row. - Worker scheduler: a 15-min cron tick scans
remindersfor rows whosedue_at <= now() AND fired_at IS NULL, fires the notification, setsfired_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 NULLuser_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
reminderstable 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
~3–4 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.tsxpointing at the existingdeveloper_user_id+approver_user_idsettings. - 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-CRMdocumenso:signednotification routed to the linked user's CRM notifications inbox.
Phase 2 — Webhook handler enhancement (~3–4h)
- 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_tokenover 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 (~1–2h)
- Confirm the marketing site's
/sign/<type>/<token>page handles every signer-role × documentType combo. - Update
signerMessagesmap 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
~6–7h across the three phases. Phase 4 (field placement UI, 10–14h) stays deferred — covered separately by the PDF template editor work you picked Phases 1+2 for.
What I'll build first
Per your sequencing:
- Documenso Phase 7 (~1h) — unblock the linked-user signing UX.
- Supplemental form per-port setting (~2h) — small win.
- Documenso Phase 2 (~3–4h) — meaningful UX improvement.
- Documenso Phase 5 (~1–2h) — security + role copy.
- EOI field overrides + reminders (~1.5 weeks combined) — the big ones, picked up after the Documenso quick wins land.