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

252 lines
9.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:
```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 (~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.