# 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=`. 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 `` (when multi-value) or `` + "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 ~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 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 - `` 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: ```ts 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 (~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_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 (~1–2h) - Confirm the marketing site's `/sign//` 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 ~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: 1. Documenso Phase 7 (~1h) — unblock the linked-user signing UX. 2. Supplemental form per-port setting (~2h) — small win. 3. Documenso Phase 2 (~3–4h) — meaningful UX improvement. 4. Documenso Phase 5 (~1–2h) — security + role copy. 5. EOI field overrides + reminders (~1.5 weeks combined) — the big ones, picked up after the Documenso quick wins land.