From d8ac62f6f4cdef378208b3edf46ee7227640d380 Mon Sep 17 00:00:00 2001 From: Matt Ciaccio Date: Tue, 28 Apr 2026 01:51:41 +0200 Subject: [PATCH] docs(spec): documents hub + reservation agreements + visual polish (Phase A) Captures the brainstorm output covering: - Documents hub at /[port]/documents replacing existing list - Document detail page with vertical signers panel, watchers, timeline - Generalised create-document wizard (HTML / PDF AcroForm / PDF overlay / Documenso-rendered + ad-hoc PDF upload) - Reservation agreements as a doc type with new CRM-side detail page - Email composer attachments + System-vs-User From selector (admin-gated) - Reminder framework polish (per-template cadence, per-doc override, per-doc disable, per-signer manual reminders); drops interests.reminderEnabled gating - Documenso v1.13.1/v2.x version-aware abstraction for field placement + void - System-wide visual polish (token additions, primitive components, sweep) - Test plan including click-everything sweep + expanded realapi round-trip - Build sequence: 11 PRs, ~3.5 weeks critical path Co-Authored-By: Claude Opus 4.7 (1M context) --- .../specs/2026-04-28-documents-hub-design.md | 775 ++++++++++++++++++ 1 file changed, 775 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-28-documents-hub-design.md diff --git a/docs/superpowers/specs/2026-04-28-documents-hub-design.md b/docs/superpowers/specs/2026-04-28-documents-hub-design.md new file mode 100644 index 0000000..c967180 --- /dev/null +++ b/docs/superpowers/specs/2026-04-28-documents-hub-design.md @@ -0,0 +1,775 @@ +# Documents Hub, Reservation Agreements, and Visual Polish (Phase A) + +**Status:** Draft — awaiting final review +**Date:** 2026-04-28 +**Phase:** A of D (B = Insights & Alerts; C = Website integration; D = Pre-prod ops) + +## Overview + +Phase A delivers a unified Documents Hub that tracks every signature-based document (EOI, Reservation Agreements, NDAs, ad-hoc uploads), generalises the existing single-purpose EOI dialog into a multi-format create-document wizard, builds the missing CRM-side reservation detail page with an end-to-end agreement workflow, polishes the reminder framework so non-EOI docs auto-remind correctly, and applies a system-wide visual upgrade to the polished-SaaS aesthetic the project already has tokens for. + +The project already ships a usable CRM with auth, multi-tenancy, full client/yacht/company/interest/berth/reservation data model, an EOI dual-path (Documenso template + in-app PDF), socket-driven real-time updates, and 130 smoke specs. What's missing for the next release: a single place to see what documents need signing and chase the people who haven't signed. + +## Scope boundaries + +### In scope (this spec) + +- New `/[port]/documents` hub page replacing the existing list +- New `/[port]/documents/[id]` document detail page +- Generalised create-document wizard supporting four template formats (HTML, PDF AcroForm fillable, PDF overlay-positioned, Documenso-rendered) plus ad-hoc PDF upload +- New `/[port]/berth-reservations/[id]` reservation detail page with agreement-generation flow +- Reservation Agreement as a first-class document type with default template seeded +- Email composer extended with attachments and a System-vs-User From selector (admin-gated) +- Reminder framework: per-template cadence, per-doc override, per-doc disable, per-signer manual reminders +- Documenso version-aware abstraction layer covering field placement and document voiding across v1.13.1 and v2.x +- System-wide visual polish: shadow scale, gradient layer, animation tokens, primitive components (``, ``, ``, polished ``), applied across all list and detail pages +- Mobile-responsive sweep across every page touched +- Comprehensive test coverage: unit, integration, smoke, exhaustive click-through, real-API round-trips, visual baseline regeneration + +### Explicitly out of scope (deferred to later phases) + +- Analytics dashboard, alert framework, interests-by-berth view, expense duplicate detection (Phase B) +- Website-side integration: `/api/form/[token]/data` prefill endpoint, `/api/webhook/document-signed` callback receiver, public-endpoint shape compat (Phase C) +- NocoDB to Postgres data migration, email deliverability (DKIM/SPF/DMARC), Sentry error reporting, audit log retention, performance baseline at 5k clients / 50k interests, backup/restore automation, production deploy readiness (Phase D) +- Native in-CRM PDF field-placement editor (deferred until upload-path pain emerges; Phase A v1 ships with auto-placed footer signature fields and a "Customize fields in Documenso" link) +- Word `.docx` template upload (deferred; PDF prioritized because Word adds LibreOffice/CloudConvert toolchain dependency without saving the field-placement step) +- Per-interest "silence all reminders" toggle (was implicit in old `interests.reminderEnabled` gating which this spec drops; can be re-added as a bulk action if anyone misses it) + +## Information architecture + +### URL surface + +``` +/[port]/documents hub (replaces existing list) +/[port]/documents/[id] document detail (new) +/[port]/documents/new create-document wizard (new) +/[port]/berth-reservations/[id] reservation detail (new) +/[port]/admin/templates existing; extended for new template formats +/[port]/admin/email existing; one new toggle +``` + +### Schema deltas + +``` +documents — additions: ++ reservation_id text null references berth_reservations(id) ++ reminders_disabled boolean default false ++ reminder_cadence_override int null + +document_templates — additions: ++ reminder_cadence_days int null (null = no auto-reminders) ++ template_format text default 'html' ('html'|'pdf_form'|'pdf_overlay'|'documenso_render') ++ source_file_id text null references files(id) ++ documenso_template_id text null ++ field_mapping jsonb default '{}' (pdf_form: { acroFieldName: mergeToken }) ++ overlay_positions jsonb default '[]' (pdf_overlay: [{token, page, x, y, fontSize}]) + +document_templates.body_html — relax to nullable (only required when template_format='html') + +document_watchers — new table: + document_id text not null references documents(id) on delete cascade + user_id text not null references users(id) + added_by text not null references users(id) + added_at timestamptz default now() + primary key (document_id, user_id) + +documents indexes — additions: ++ idx_docs_reservation on (reservation_id) ++ idx_docs_status_port on (port_id, status) — powers tab counts cheaply + +document_watchers indexes: ++ idx_doc_watchers_doc on (document_id) ++ idx_doc_watchers_user on (user_id) + +documents.documentType enum — already includes 'reservation_agreement'; no migration needed +documents.status enum — already accepts 'expired'; no migration needed +documentSigners.status enum — pending|signed|declined; no migration needed +``` + +Backfill (one statement, safe to run in same migration): + +```sql +UPDATE document_templates SET reminder_cadence_days = 1 WHERE template_type = 'eoi'; +``` + +This preserves the existing 1-day-effective reminder cadence for existing EOI templates. Admins can edit per-template later. + +After running migration on a dev/staging server, restart `next dev` to flush postgres.js prepared-statement cache (existing project convention). + +### Polymorphic ownership pattern + +Documents already use the multi-FK pattern (`interest_id`, `client_id`, `yacht_id`, `company_id` as separate nullable columns). Adding `reservation_id` matches this. No conversion to polymorphic discriminator columns despite yachts and invoices using that pattern; staying consistent with the existing documents shape avoids a destructive migration. + +### Service-layer changes + +- `documents.service.ts`: + - `createFromWizard(portId, data, meta)` — dispatches across template/upload paths + - `createFromUpload(portId, data, meta)` — new upload-driven path; calls Documenso `createDocument`, stores file in MinIO via `files` service, mirrors to `documents` + `documentSigners`, optionally calls `sendDocument` if `sendImmediately` + - `cancelDocument(documentId, portId, meta)` — user-initiated cancel; calls Documenso void, updates DB status, logs event + - `composeSignedDocEmail(documentId, portId)` — returns prefilled `{ to, cc, subject, body, attachments, defaultSenderType }` for the composer + - `getDocumentDetail(id, portId)` — single-roundtrip aggregator returning doc + signers + events + watchers + linked-entity summary + +- `document-templates.ts`: + - `generateAndSign` extended for new `template_format` values + - `fillAcroForm(sourceFile, fieldMapping, mergeContext)` — pdf-lib AcroForm fill + - `drawOverlay(sourceFile, overlayPositions, mergeContext)` — pdf-lib text-draw at positions + - Documenso-render path uses existing `generateDocumentFromTemplate` + +- `documenso-client.ts`: + - `placeFields(docId, fields, portId?)` — version-aware bulk field placement + - `placeDefaultSignatureFields(docId, recipientIds, portId?)` — auto-position one SIGNATURE per recipient at footer + - `voidDocument(docId, portId?)` — version-aware doc void/delete + - Coordinate normalization helpers (caller passes percent 0-100; converted to pixels for v1 using cached page dimensions) + +- `document-reminders.ts`: + - `sendReminderIfAllowed(documentId, portId, options?)` — extended signature with optional `signerId` and `auto: boolean` + - `processReminderQueue(portId)` — query rewritten around `documents.reminder_cadence_override ?? template.reminder_cadence_days`; drops `interests.reminderEnabled` gating + +- `notifications.service.ts`: + - `notifyDocumentEvent(docId, eventType)` — fans out to creator + entity-assignee + watchers; existing socket events keep firing + +- New: `reservation-agreement-context.ts`: + - `buildReservationAgreementContext(reservationId, portId)` — joins reservation -> client + yacht + berth -> port; returns context shape for template merge + +- `email-compose.service.ts`: + - Validator extended: `{ senderType: 'system'|'user', accountId? (when user), attachments[] }` + - System path: calls `lib/email/index.ts → sendEmail()` with `portId` + attachments; logs `documentEvents` row `signed_doc_emailed`; skips `email_messages`/`email_threads` writes + - User path: existing flow, with attachments resolution from `files` table + - Port-isolation: cross-port `fileId` returns 403 + +- `lib/email/index.ts`: + - `SendEmailOptions.attachments?: Array<{ fileId, filename? }>` — fetches files from MinIO, passes to nodemailer + +## Documents hub page + +Replaces existing `/[port]/documents` list. + +### Layout + +``` +[ Header strip: title, KPI sub-line, "+ New document" button ] + +[ Tabs: All | Awaiting them (count) | Awaiting me (count) | Completed | Expired ] + +[ Search · Type · Status · Sent · Watcher filter chips · saved-view selector · overflow ] + +[ Table: + checkbox | Document | Type pill | Subject pill | Status (X/Y signed + dot) | Sent + ▾ expand row inline to show signers + watchers strip +] + +[ Sticky bulk-action bar appears when ≥1 row checked: + "N selected" | Remind unsigned | Cancel | Export | pagination +] +``` + +### Tab queries + +- All — every document in port +- Awaiting them — `status IN ('sent','partially_signed')` AND has pending signer != current user +- Awaiting me — at least one `documentSigners` row matching `signer_email = current user email` AND `status = 'pending'` +- Completed — `status IN ('completed','signed')` +- Expired — `status = 'expired'` OR (`status IN ('sent','partially_signed')` AND `expires_at < now()`) + +Counts run cheap thanks to `idx_docs_status_port`. + +### Filters and saved views + +- Search: fuzzy match on title, subject name, signer email +- Type: multi-select doc types +- Status: multi-select status enum +- Sent: date-range chips (Today, 7d, 30d, custom) +- Watcher: filter by watching user +- "Signature-based only" chip defaults to ON; toggle off to see non-signed docs (welcome letters etc.) as well, rendered with a "Delivered" pill +- Saved-view integration: filter combos save to existing `saved_views` table + +### Row anatomy + +- Collapsed: name (links to detail), type pill (colored per type), subject pill (links to entity), status indicator (X/Y signed with progress dot), sent age +- Expanded: per-signer rows with email, status pill, sent timestamp, signed timestamp, `[Remind]` and overflow `[...]` (resend invite, copy signing link, skip — skip is UI-only flag, not implemented in v1) +- Watchers strip at bottom of expansion: chips + `+ Add watcher` autocomplete +- Hover: row gets soft brand-soft gradient bg + +### Real-time + +Subscribes to existing `documents.service.ts`-emitted socket events: `document:created`, `document:updated`, `document:deleted`, `document:sent`, `document:completed`, `document:expired`, `document:cancelled`, `document:rejected`, `document:signer:signed`, `document:signer:opened`. All already fire today. + +### Empty states + +- No docs yet: illustration + 1-line explanation + `[+ New document]` CTA +- Filtered empty: "No docs match these filters. Clear filters?" + +### Mobile (< 768px) + +- Tabs collapse into `` < 640px +- Sidebar slides over content < 1024px +- Primary "+ New" actions float as FAB bottom-right < 640px + +## Test plan + +### Unit (`tests/unit/`) + +- `document-reminders-cadence.test.ts` — `isReminderDue` math; manual-vs-auto window/cooldown bypass +- `documenso-place-fields.test.ts` — v1/v2 dispatch (mocked HTTP); coord normalization; default field staggering for 1/2/3/5 recipients +- `email-attachments-resolver.test.ts` — fileId → MinIO buffer; cross-port 403; 10 MB cap warning + +### Integration (`tests/integration/`) + +- Extend `document-templates-generate-and-sign.test.ts` — new template formats (`pdf_form`, `pdf_overlay`, `documenso_render`); upload-path test +- New `document-watchers.test.ts` — add/remove endpoints; notification fan-out; port isolation +- New `document-cancel.test.ts` — user-initiated cancel; mocked Documenso void; status + event log; reject 409 if completed +- New `reservation-agreement-contract-mirror.test.ts` — `handleDocumentCompleted` mirrors `signedFileId` to `berth_reservations.contractFileId` only for `reservation_agreement` type +- New `reminder-cron-cadence.test.ts` — seed varied templates; simulated time advance; assert correct docs reminded + +### E2E smoke (`tests/e2e/smoke/`) + +- Extend `04-documents.spec.ts` — hub tabs, expand row, per-signer remind with cooldown, type/status filters, saved-view round-trip, bulk-remind with per-row toast reasons +- Extend `05-eoi-generate.spec.ts` — wizard invocation prefills (template, interest); existing flow regression +- New `27-document-create-wizard.spec.ts` — template path full flow; upload path full flow; watcher addition; reminder-override radios produce correct DB state +- New `28-reservation-agreements.spec.ts` — reservation detail → Generate agreement → wizard prefilled → Send → agreement section state transitions; post-completion contract attached + email button visible +- New `29-email-attachments.spec.ts` — system path send (documentEvents row, no email_messages); user path send when toggle on (email_messages with attachment_file_ids); cross-port 403 + +### E2E exhaustive (`tests/e2e/exhaustive/`) — click-everything sweep + +- New `10-documents-hub.spec.ts` — crawl each tab, filter dropdowns, saved-view, expand row, signer-row buttons, bulk-action bar +- New `11-document-detail.spec.ts` — crawl in three states (draft/sent/completed); watcher add/remove; notes auto-save; preview download; "Email signed PDF" launch +- New `12-document-create-wizard.spec.ts` — crawl each wizard step under both template and upload paths; picker dropdowns, signer add/remove, drag-handle, reminder-cadence radios +- New `13-reservation-detail.spec.ts` — crawl in three states (pending no agreement / agreement-in-flight / agreement-completed); Activate/Cancel/Generate buttons; inline notes +- New `14-email-composer.spec.ts` — crawl composer drawer with attachments; From dropdown; attach button; recipient chips +- Extend exhaustive `05-eoi-generate.spec.ts` — parallel-mode + signing-order edge cases (greyed-out reminder buttons; out-of-order remind rejection) + +### E2E real-API (`tests/e2e/realapi/`) + +Each spec gates on env vars; clean skip if missing. + +- Extend `documenso-real-api.spec.ts`: + - Generate from Documenso template (real send) and assert in real Documenso + - Generate from in-app PDF AcroForm fill, upload to real Documenso, assert + - Generate from upload path with auto-placed signature fields, assert fields visible in Documenso + - v1 and v2 explicit version-flag tests (via `DOCUMENSO_API_VERSION`) + - Manually sign in real Documenso (or simulate webhook) and assert local DB updates + - Cancel real in-flight doc, assert local + remote state + - Send reminder via real Documenso, assert HTTP + documentEvents row + +- New `smtp-system-send.spec.ts` — system-path send → IMAP fetch → assert subject + attachment; verify port-config from-identity; cleanup via IMAP delete +- New `smtp-user-send.spec.ts` — user-path send (requires connected account, allowPersonalAccountSends=true) → IMAP fetch → email_messages row with attachment_file_ids +- New `minio-file-lifecycle.spec.ts` — upload, list, preview, download (byte-equal), delete; port isolation; mime-type validation +- New `documenso-webhook-ingress.spec.ts` — requires cloudflared tunnel; configure tunnel URL as Documenso webhook target; trigger doc completion; assert webhook fires + handler updates DB; verify timing-safe secret check rejects wrong secret with 401; verify event normalisation (uppercase enum + lowercase-dotted both accepted) +- New `email-attachments-roundtrip.spec.ts` — compose with fileId attachment; SMTP send; IMAP fetch; assert attachment bytes match; reject cross-port fileId with 403 before SMTP touched + +### Visual baselines (`tests/e2e/visual/`) + +`snapshots.spec.ts-snapshots/` regenerated as polish ships per page; one PR per surface group, baselines reviewed in PR diff. New baselines added: documents hub, doc detail, create-document wizard (each step), reservation detail, email composer with attachments. + +### Test data fixtures + +`global-setup.ts` extended with: + +- Seed default `reservation_agreement` template (HTML format) +- Seed default `signed_doc_completion` template +- Seed one in-flight EOI doc with two pending signers (for hub-tab tests) +- Seed one `berth_reservation` with `status='active'` and no agreement (for lifecycle alert query) + +### CI vs local runs + +| Project | When | +| ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | +| `setup` + `smoke` (~14 min) | Every PR via CI | +| `exhaustive` (with new click-everything specs) | Every PR via CI; ~25 min budget | +| `visual` | Every PR; baselines reviewed in PR diffs | +| `realapi` | Locally before merging touch-points; pre-release; not on CI (avoids burning Documenso quota and SMTP costs) | + +## Build sequence + +| # | Title | Effort | Depends on | +| ----- | ------------------------------------------------- | ------ | -------------- | +| 1 | Data model + service skeletons | 1d | — | +| 2 | Documenso v1/v2 abstraction layer | 1d | — | +| 3 | Visual primitives + token additions | 1.5d | — | +| 4 | Documents hub page | 2d | 1, 3 | +| 5 | Document detail page | 2d | 1, 3 | +| 6 | Create-document wizard + new template formats | 2.5d | 1, 2, 3 | +| 7 | Reservation detail + agreement flow | 1.5d | 1, 6 | +| 8 | Email composer attachments + From selector | 1d | 1, 3 | +| 9 | Reminder framework polish | 1d | 1 | +| 10a-e | Visual polish sweep (5 PRs across surface groups) | 3-4d | 3 | +| 11 | Real-API integration tests | 1.5d | 2, 4-9 shipped | + +### Critical path + +``` +1 → 2 → 6 → 7 (data model → Documenso → wizard → reservation) +1 → 3 → 4 → 5 → 9 (data model → primitives → hub → detail → reminders) + 1 → 8 (composer) + 3 → 10a-e (sweep) + all → 11 (realapi) +``` + +Wall-clock minimum ~9 days; realistic with overhead ~17 days; calendar ~3.5-5 weeks. + +### Acceptance gates per PR + +- `pnpm tsc --noEmit` and `pnpm lint` clean +- Vitest unit + integration green +- Playwright smoke green for surface touched +- Visual baselines regenerated and reviewed in PR diff +- For PRs touching external integrations (2, 6 upload, 7 contract mirror, 8 SMTP, 11): relevant `realapi` spec verified locally before merge + +### Risk register + +| Risk | Mitigation | +| ---------------------------------------------------------- | ----------------------------------------------------------------------------------------- | +| Documenso v2 endpoint shape drifts from docs | PR2 validates against real v2 instance during dev; realapi spec re-runs nightly post-ship | +| Visual polish scope creeps | One PR per surface group (10a-e), each independently shippable | +| Cron migration changes effective behaviour | Backfill sets EOI cadence to 1 day matching today's effective; run on staging first | +| Mobile responsive regressions | Visual baselines include phone-viewport snapshots; PR10e is the responsive sweep | +| EOI dialog → wizard migration breaks "Generate EOI" button | Wizard launched with prefills from interest detail; PR6 includes regression spec | +| AcroForm template format confuses non-technical admins | HTML default; inline help; default templates seeded | +| Phase A wall-clock past 5 weeks | Tier-2 sweep items + optional realapi specs deferrable to follow-up release | + +## Glossary + +- **Documenso** — open-source document signing service, self-hosted instance at `signatures.portnimara.dev` +- **EOI** — Expression of Interest, a pre-reservation signed document +- **Reservation Agreement** — contract signed when a berth reservation is committed +- **Hub** — the new `/[port]/documents` page +- **Watcher** — a CRM user added to a doc to receive notifications on signature events without being a signer themselves +- **Signing order** — sequential index across signers; sequential mode requires lower order to sign first; parallel mode lets all sign concurrently +- **Cadence** — interval in days between auto-reminders to unsigned signers +- **System send / User send** — email dispatch identity: System uses port-config noreply SMTP; User uses connected personal email account (gated by admin toggle) +- **Render location** — where the PDF is generated (CRM-local via HTML/AcroForm/overlay, or in Documenso). Signing is always Documenso; render location is independent.