Files
pn-new-crm/src/lib/services/documenso-payload.ts

370 lines
14 KiB
TypeScript
Raw Normal View History

feat(sales): admin-configurable EOI signers + richer timeline events 1. Per-port EOI signer config - New `eoi_signers` system_settings key (JSON: { developer, approver }, each `{ name, email }`). Settings UI exposes it under Admin → Settings. - getPortEoiSigners(portId) reads the setting with a typed validator; falls back to the legacy David Mizrahi / Abbie May defaults if the row is missing or malformed (so older ports keep working until an admin saves a value). - Both EOI generation pathways now read from the helper instead of hardcoded constants: * documenso-template path (generateAndSignViaDocumensoTemplate) * in-app PDF-fill path (generateAndSignViaInApp) 2. Timeline upgrades The interest detail Activity tab now distinguishes the new automation events that arrived with sessions 1+2: - Stage auto-advances (userId='system') get a small "Auto" pill and carry their reason into the description (e.g. "Stage advanced to EOI Signed (auto-advanced — EOI signed via Documenso)"). - outcome_set events show "Marked as Won" / "Marked as Lost — went to another marina" with optional reason; trophy/X icons. - outcome_cleared events show "Reopened to {stage}" with a refresh icon. - Document events humanized: "Document 'X' fully signed" instead of "Document X: completed". - Stage labels run through stageLabel() so the timeline shows the human label, not the enum key. - Timestamps switched to relative-time with full-date tooltip. - "by system" is rendered plainly (no longer the literal user-id). tsc clean. vitest 832/832 pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 00:19:55 +02:00
import { and, eq } from 'drizzle-orm';
import { db } from '@/lib/db';
import { systemSettings } from '@/lib/db/schema/system';
fix(audit): comprehensive 2026-05-15 audit fix wave + Documenso v2 polish Bundles the prior session's 50-task fix sweep (Documenso v2 + EOI/signing- progress redesign + env-to-admin migration + dev-mode banner) with the 2026-05-18 audit fix wave (3 CRITICAL, 14 HIGH, 28 MEDIUM, 6 LOW). CRITICAL (3): - C-01 interest-berths INNER JOIN -> LEFT JOIN so hard-deleted berths no longer silently drop interest links - C-02 /setup added to PUBLIC_PATHS; fresh-deploy bootstrap loop fixed - C-03 generic PATCH /interests/[id] no longer accepts pipelineStage — callers must go through /stage with the override-guard chain HIGH (14/15): - H-01 explicit ON DELETE on previously-implicit NO ACTION FKs across interests/documents/reservations/reminders/invoices (migration 0070) - H-02 login page reads ?redirect= param with same-origin guard - H-03 CRM invite token moves to URL fragment so it never lands in nginx access logs / Referer headers - H-04 Retry-After header on sign-in-by-identifier 429 (RFC 6585 §4) - H-05 toggleAccount writes an audit row - H-06 upsertSetting masks any value whose key ends with _encrypted - H-07 archiveClient cascade fires per-interest audit rows - H-08 createSalesTransporter applies SMTP_TIMEOUTS - H-09 AppShell stable children — viewport flip across breakpoint no longer destroys in-progress form drafts - H-10 portal documents page swaps Unicode glyph status icons for Lucide CheckCircle2/XCircle/Circle + aria-labels - H-12 list components swap alert(...) for toast.warning(...) - H-13 5 icon-only buttons gain aria-label - H-14 parseBody treats empty bodies as {} - H-15 admin layout renders a 403 panel instead of silent bounce - H-11 not applicable — mobile-search-overlay IS a mobile bottom-sheet MEDIUM (28+): - M-MT01-05 defense-in-depth port_id/parent-id filters on UPDATE/DELETE WHEREs across custom-fields, notes (all 6 entity types x update + delete), client-contacts, yacht ownerClient lookup, webhook reads - M-D01 documents-hub realtime event-name typo (file:created -> uploaded) - M-EM01 portal-auth emails thread through portId - M-EM02 sendEmail accepts cc/bcc params - M-EM04 notification_digest catalog key - M-IN01 portal presigned download URLs use 4h TTL - M-IN02 OpenAI client lazy-instantiated - M-IN04 stale pdfme refs updated to pdf-lib AcroForm - M-IN05 umami.testConnection returns tagged union - M-L01 reservations tenure_type unified with berths - M-L02 report-generators canonicalize stage values - M-AU01 audit log placeholder copy fixed - M-AU04 outcome_set / outcome_cleared distinct audit verbs - M-NEW-2 activity feed entity name+type separator - M-R01 portal allowlist narrowed + portal_session backstop in proxy - M-SC02 companies archived partial index - M-SC04 audit_logs.searchText documented as DB-managed - M-S01 storage_s3_access_key_encrypted admin field - M-U01 audit log empty state uses <EmptyState> - M-U09 invoice delete dialog -> <AlertDialog> - M-U10 toast.success on ClientForm + InterestForm create/edit - M-U11 settings-form-card logo preview alt text - M-U14 mobile topbar title on clients/yachts/interests/berths - M-U15 Invoices in mobile More-sheet LOW (6/8): - L-AU01 severity defaults for security-relevant verbs - L-AU02 +13 missing actions in admin audit filter - L-AU03 +7 missing entity types in admin audit filter - L-AU04 dead listAuditLogs stubbed - L-D02 CLAUDE.md Owner-wins chain tightened Bonus — Document detail polish (#67 partial, 3/6 deliverables): - state-aware action button per signer - watcher Add UI with display-name resolution - cleanSignerName cleanup Prior session work bundled in: - Documenso v2 webhook + envelope-ID normalization + sequential signing - SigningProgress UI redesign (avatars, per-signer state, timestamps) - env->admin settings registry + RegistryDrivenForm + encrypted creds - Embedded-signing card + Test connection + setup help - Dev-mode EMAIL_REDIRECT_TO banner - Pipeline rules admin page - Sales email config card - Audit log details Sheet - EOI tab: Finalising badge, absolute timestamps, sequential indicator - Notes pipeline_stage_at_creation (migration 0069) - Documenso numeric ID dual-key webhook (migration 0068) - Dimensions criterion copy (migration 0067) Tests: 1374/1374 vitest pass. tsc clean. lint clean. See docs/AUDIT-FIX-WAVE-2026-05-18.md for the full progress report and the user-input items still pending. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 13:28:50 +02:00
import { user, userProfiles } from '@/lib/db/schema/users';
import type { EoiContext } from '@/lib/services/eoi-context';
fix(audit): comprehensive 2026-05-15 audit fix wave + Documenso v2 polish Bundles the prior session's 50-task fix sweep (Documenso v2 + EOI/signing- progress redesign + env-to-admin migration + dev-mode banner) with the 2026-05-18 audit fix wave (3 CRITICAL, 14 HIGH, 28 MEDIUM, 6 LOW). CRITICAL (3): - C-01 interest-berths INNER JOIN -> LEFT JOIN so hard-deleted berths no longer silently drop interest links - C-02 /setup added to PUBLIC_PATHS; fresh-deploy bootstrap loop fixed - C-03 generic PATCH /interests/[id] no longer accepts pipelineStage — callers must go through /stage with the override-guard chain HIGH (14/15): - H-01 explicit ON DELETE on previously-implicit NO ACTION FKs across interests/documents/reservations/reminders/invoices (migration 0070) - H-02 login page reads ?redirect= param with same-origin guard - H-03 CRM invite token moves to URL fragment so it never lands in nginx access logs / Referer headers - H-04 Retry-After header on sign-in-by-identifier 429 (RFC 6585 §4) - H-05 toggleAccount writes an audit row - H-06 upsertSetting masks any value whose key ends with _encrypted - H-07 archiveClient cascade fires per-interest audit rows - H-08 createSalesTransporter applies SMTP_TIMEOUTS - H-09 AppShell stable children — viewport flip across breakpoint no longer destroys in-progress form drafts - H-10 portal documents page swaps Unicode glyph status icons for Lucide CheckCircle2/XCircle/Circle + aria-labels - H-12 list components swap alert(...) for toast.warning(...) - H-13 5 icon-only buttons gain aria-label - H-14 parseBody treats empty bodies as {} - H-15 admin layout renders a 403 panel instead of silent bounce - H-11 not applicable — mobile-search-overlay IS a mobile bottom-sheet MEDIUM (28+): - M-MT01-05 defense-in-depth port_id/parent-id filters on UPDATE/DELETE WHEREs across custom-fields, notes (all 6 entity types x update + delete), client-contacts, yacht ownerClient lookup, webhook reads - M-D01 documents-hub realtime event-name typo (file:created -> uploaded) - M-EM01 portal-auth emails thread through portId - M-EM02 sendEmail accepts cc/bcc params - M-EM04 notification_digest catalog key - M-IN01 portal presigned download URLs use 4h TTL - M-IN02 OpenAI client lazy-instantiated - M-IN04 stale pdfme refs updated to pdf-lib AcroForm - M-IN05 umami.testConnection returns tagged union - M-L01 reservations tenure_type unified with berths - M-L02 report-generators canonicalize stage values - M-AU01 audit log placeholder copy fixed - M-AU04 outcome_set / outcome_cleared distinct audit verbs - M-NEW-2 activity feed entity name+type separator - M-R01 portal allowlist narrowed + portal_session backstop in proxy - M-SC02 companies archived partial index - M-SC04 audit_logs.searchText documented as DB-managed - M-S01 storage_s3_access_key_encrypted admin field - M-U01 audit log empty state uses <EmptyState> - M-U09 invoice delete dialog -> <AlertDialog> - M-U10 toast.success on ClientForm + InterestForm create/edit - M-U11 settings-form-card logo preview alt text - M-U14 mobile topbar title on clients/yachts/interests/berths - M-U15 Invoices in mobile More-sheet LOW (6/8): - L-AU01 severity defaults for security-relevant verbs - L-AU02 +13 missing actions in admin audit filter - L-AU03 +7 missing entity types in admin audit filter - L-AU04 dead listAuditLogs stubbed - L-D02 CLAUDE.md Owner-wins chain tightened Bonus — Document detail polish (#67 partial, 3/6 deliverables): - state-aware action button per signer - watcher Add UI with display-name resolution - cleanSignerName cleanup Prior session work bundled in: - Documenso v2 webhook + envelope-ID normalization + sequential signing - SigningProgress UI redesign (avatars, per-signer state, timestamps) - env->admin settings registry + RegistryDrivenForm + encrypted creds - Embedded-signing card + Test connection + setup help - Dev-mode EMAIL_REDIRECT_TO banner - Pipeline rules admin page - Sales email config card - Audit log details Sheet - EOI tab: Finalising badge, absolute timestamps, sequential indicator - Notes pipeline_stage_at_creation (migration 0069) - Documenso numeric ID dual-key webhook (migration 0068) - Dimensions criterion copy (migration 0067) Tests: 1374/1374 vitest pass. tsc clean. lint clean. See docs/AUDIT-FIX-WAVE-2026-05-18.md for the full progress report and the user-input items still pending. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 13:28:50 +02:00
import { readSetting, SETTING_KEYS } from '@/lib/services/port-config';
export interface DocumensoTemplatePayload {
title: string;
externalId: string;
meta: {
message: string;
subject: string;
redirectUrl: string;
distributionMethod: 'NONE' | 'EMAIL';
feat(documenso): full v2 endpoint coverage + sequential signing + redirectUrl Wire up the remaining version-aware paths so a port pointed at Documenso 2.x takes the v2 endpoint on every CRUD operation, with two new v2-only settings exposed in admin UI. documenso-client.ts: - createDocument: v2 multipart /envelope/create + getDocument follow-up to return the full doc shape (v1 path unchanged) - sendDocument: v2 /envelope/distribute (returns per-recipient signingUrl in the same response — eliminates the v1 separate-GET round-trip) - sendReminder: v2 /envelope/redistribute with recipientIds filter - downloadSignedPdf: v2 /envelope/{id}/download - CreateDocumentMeta type: { subject, message, redirectUrl, signingOrder } threaded through v1 + v2 paths (v1 ignores signingOrder) port-config.ts: - New settings: documenso_signing_order (PARALLEL/SEQUENTIAL, v2-only), documenso_redirect_url (both versions honour) - PortDocumensoConfig gains signingOrder + redirectUrl documenso-payload.ts: - DocumensoTemplatePayload.meta gains signingOrder - buildDocumensoPayload reads from options.signingOrder, omits when null document-templates.ts (EOI template flow): - Pass docCfg.signingOrder + docCfg.redirectUrl into buildDocumensoPayload documents.service.ts (sendForSigning uploaded-doc flow): - Pass portId to documensoCreate + documensoSend (was missing) - Thread signingOrder + redirectUrl via the new meta param Admin Documenso settings page: - v2 benefits card updated: now lists envelope CRUD, one-call send, sequential enforcement, post-sign redirect as wired (was roadmap) - Roadmap callout pruned to the three remaining deferred items: template/use migration, /envelope/update, non-SIGNER recipient roles - New "v2 signing behaviour" SettingsFormCard with the two new settings Template flow stays on /api/v1/templates/{id}/generate-document by design — Documenso 2.x accepts v1 endpoints via backward compat; full migration to v2 /template/use requires per-template field-ID capture (admin schema work, deferred). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 14:38:45 +02:00
/**
* PARALLEL = all signers can sign in any order (default, current behaviour).
* SEQUENTIAL = signers must complete in the order their `signingOrder`
* number dictates (client developer approver for EOI). v2 enforces
* this server-side; v1 ignores the key and behaves as PARALLEL regardless.
*/
signingOrder?: 'PARALLEL' | 'SEQUENTIAL';
};
fix(audit): comprehensive 2026-05-15 audit fix wave + Documenso v2 polish Bundles the prior session's 50-task fix sweep (Documenso v2 + EOI/signing- progress redesign + env-to-admin migration + dev-mode banner) with the 2026-05-18 audit fix wave (3 CRITICAL, 14 HIGH, 28 MEDIUM, 6 LOW). CRITICAL (3): - C-01 interest-berths INNER JOIN -> LEFT JOIN so hard-deleted berths no longer silently drop interest links - C-02 /setup added to PUBLIC_PATHS; fresh-deploy bootstrap loop fixed - C-03 generic PATCH /interests/[id] no longer accepts pipelineStage — callers must go through /stage with the override-guard chain HIGH (14/15): - H-01 explicit ON DELETE on previously-implicit NO ACTION FKs across interests/documents/reservations/reminders/invoices (migration 0070) - H-02 login page reads ?redirect= param with same-origin guard - H-03 CRM invite token moves to URL fragment so it never lands in nginx access logs / Referer headers - H-04 Retry-After header on sign-in-by-identifier 429 (RFC 6585 §4) - H-05 toggleAccount writes an audit row - H-06 upsertSetting masks any value whose key ends with _encrypted - H-07 archiveClient cascade fires per-interest audit rows - H-08 createSalesTransporter applies SMTP_TIMEOUTS - H-09 AppShell stable children — viewport flip across breakpoint no longer destroys in-progress form drafts - H-10 portal documents page swaps Unicode glyph status icons for Lucide CheckCircle2/XCircle/Circle + aria-labels - H-12 list components swap alert(...) for toast.warning(...) - H-13 5 icon-only buttons gain aria-label - H-14 parseBody treats empty bodies as {} - H-15 admin layout renders a 403 panel instead of silent bounce - H-11 not applicable — mobile-search-overlay IS a mobile bottom-sheet MEDIUM (28+): - M-MT01-05 defense-in-depth port_id/parent-id filters on UPDATE/DELETE WHEREs across custom-fields, notes (all 6 entity types x update + delete), client-contacts, yacht ownerClient lookup, webhook reads - M-D01 documents-hub realtime event-name typo (file:created -> uploaded) - M-EM01 portal-auth emails thread through portId - M-EM02 sendEmail accepts cc/bcc params - M-EM04 notification_digest catalog key - M-IN01 portal presigned download URLs use 4h TTL - M-IN02 OpenAI client lazy-instantiated - M-IN04 stale pdfme refs updated to pdf-lib AcroForm - M-IN05 umami.testConnection returns tagged union - M-L01 reservations tenure_type unified with berths - M-L02 report-generators canonicalize stage values - M-AU01 audit log placeholder copy fixed - M-AU04 outcome_set / outcome_cleared distinct audit verbs - M-NEW-2 activity feed entity name+type separator - M-R01 portal allowlist narrowed + portal_session backstop in proxy - M-SC02 companies archived partial index - M-SC04 audit_logs.searchText documented as DB-managed - M-S01 storage_s3_access_key_encrypted admin field - M-U01 audit log empty state uses <EmptyState> - M-U09 invoice delete dialog -> <AlertDialog> - M-U10 toast.success on ClientForm + InterestForm create/edit - M-U11 settings-form-card logo preview alt text - M-U14 mobile topbar title on clients/yachts/interests/berths - M-U15 Invoices in mobile More-sheet LOW (6/8): - L-AU01 severity defaults for security-relevant verbs - L-AU02 +13 missing actions in admin audit filter - L-AU03 +7 missing entity types in admin audit filter - L-AU04 dead listAuditLogs stubbed - L-D02 CLAUDE.md Owner-wins chain tightened Bonus — Document detail polish (#67 partial, 3/6 deliverables): - state-aware action button per signer - watcher Add UI with display-name resolution - cleanSignerName cleanup Prior session work bundled in: - Documenso v2 webhook + envelope-ID normalization + sequential signing - SigningProgress UI redesign (avatars, per-signer state, timestamps) - env->admin settings registry + RegistryDrivenForm + encrypted creds - Embedded-signing card + Test connection + setup help - Dev-mode EMAIL_REDIRECT_TO banner - Pipeline rules admin page - Sales email config card - Audit log details Sheet - EOI tab: Finalising badge, absolute timestamps, sequential indicator - Notes pipeline_stage_at_creation (migration 0069) - Documenso numeric ID dual-key webhook (migration 0068) - Dimensions criterion copy (migration 0067) Tests: 1374/1374 vitest pass. tsc clean. lint clean. See docs/AUDIT-FIX-WAVE-2026-05-18.md for the full progress report and the user-input items still pending. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 13:28:50 +02:00
/**
* Legacy v1 path: form-field values keyed by field NAME. Documenso v1.13.x
* accepts only this shape. v2 instances accept it via backward compat too.
*/
formValues: {
Name: string;
Email: string;
Address: string;
'Yacht Name': string;
Length: string;
Width: string;
Draft: string;
feat(eoi): multi-berth EOI generation + berth-range formatter Plan §4.6 + §1: a render function that compresses every berth marked is_in_eoi_bundle=true on an interest into a compact range string ("A1-A3, B5-B7"), wired into both EOI generation paths (the Documenso template-generate call and the in-app pdf-lib AcroForm fill). - src/lib/templates/berth-range.ts: pure formatBerthRange() with the full edge-case set from §4.6 - empty, single, run, gap, multiple prefixes, sort/dedup, multi-letter prefixes, non-canonical passthrough, long ranges. Sorts by (prefix, number); dedupes; passes non-canonical inputs through with a logger warning. - src/lib/templates/merge-fields.ts: new {{eoi.berthRange}} token added to VALID_MERGE_TOKENS allow-list under a fresh `eoi` scope so unknown-token validation at template creation time still rejects typos. - src/lib/services/eoi-context.ts: EoiContext gains eoiBerthRange. Resolved by joining interest_berths (is_in_eoi_bundle=true) → berths and feeding the mooring numbers through formatBerthRange. - src/lib/services/documenso-payload.ts: formValues now includes "Berth Range" alongside the legacy "Berth Number". Multi-berth EOIs surface here; single-berth EOIs duplicate the primary. - src/lib/pdf/fill-eoi-form.ts: in-app AcroForm fill mirrors the Documenso payload by populating "Berth Range". Falls back silently when older PDFs don't have the field (setText is no-op-on-missing). 15 unit tests on the formatter; existing EoiContext + Documenso payload tests updated to assert the new field. 1022 -> 1037 passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 03:03:29 +02:00
/**
feat(reporting): money-math sweep — Step 1 PRE-DEPLOY-PLAN Single coherent commit completing § 1.1 (hot-path correctness) plus § 1.1.4.5 (multi-berth EOI mooring fix). Numbers users see are now self-consistent across dashboard / kanban / hot deals / PDF reports. ## Active-interest sweep (canonical predicate everywhere) Routed every "active interest" filter through `activeInterestsWhere` (commit b966d81 helper). The helper enforces port-scoping + archivedAt IS NULL + outcome IS NULL — strict definition, won is closed. Touched sites: - src/lib/services/reminders.service.ts:digestPort — no longer fires reminders for won/lost/cancelled deals - src/lib/services/berths.service.ts:getLatestInterestStageByBerth - src/lib/services/client-archive-dossier.service.ts (next-in-line others lookup) - src/lib/services/client-archive.service.ts (remaining-under-offer recount before flipping berth back to available) - src/lib/services/client-restore.service.ts (yacht-usage check) - src/lib/services/interests.service.ts:listInterestsForBoard + getInterestStageCounts + the "others on same berth" lookup — kanban / board now exclude terminal deals - src/lib/services/report-generators.ts: fetchPipelineData, fetchRevenueData stage breakdowns, top-N interests ## Pipeline-value currency conversion `getKpis()` now fetches the port's defaultCurrency from `ports` and converts each berth's `priceCurrency`→port-default via `currency.service`. Returns `pipelineValue` + `pipelineValueCurrency` instead of the lying `pipelineValueUsd`. Missing rates fall through to raw amount summing (so the tile still shows an approximate number) — behind a follow-up to surface a "rates incomplete" indicator. 3 consumers updated: KpiCards, PipelineValueTile, ActiveDealsTile. ## Occupancy = sold only Both the dashboard KPI tile and the revenue-report PDF occupancy data now count only `berth.status='sold'`. `under_offer` is a hold, not occupation. The analytics timeline switches from `berth_reservations`-derived to a cumulative-won-deals derivation via `interests.outcome='won' AND outcome_at::date <= day` — same source of truth, historical shape preserved. ## Revenue PDF two-card layout Added `totalForecast` + `pipelineWeights` to `RevenueData`. Summary section now renders both: - "Completed revenue (won)" — money in the bank - "Forecast revenue (pipeline-weighted)" — expected pipeline value Pipeline weights resolve from `system_settings.pipeline_weights` (per-port admin override) and fall back to STAGE_WEIGHTS defaults. PDF and dashboard forecast tiles reconcile. ## Multi-berth EOI mooring (4.5) Documenso `Berth Number` form field now carries the formatBerthRange output for BOTH single- and multi-berth EOIs. Single-berth output is byte-identical to the legacy primary-only path (`formatBerthRange(['A1']) === 'A1'`). Multi-berth EOIs now render the full range ("A1-A3, B5") in the existing field instead of being silently dropped against a nonexistent `Berth Range` field. Dropped: - `'Berth Range'` from the Documenso formValues payload + TS type - `setBerthRange()` helper from fill-eoi-form.ts (now redundant) - The "missing Berth Range AcroForm field" warning log Updated CLAUDE.md to reflect — no Documenso admin template change needed. ## Tests - Updated `documenso-payload.test.ts` — new fixture asserts formatBerthRange output flows into Berth Number; multi-berth case added. - Updated `analytics-service.test.ts:computeOccupancyTimeline` — fixture creates a won interest instead of a reservation. - Updated `alerts-engine.test.ts:interest.stale` — fixture stage switched from dead `'in_communication'` to canonical `'qualified'`. - Updated `report-templates.test.tsx:revenue` — fixture carries `totalForecast` + `pipelineWeights` to match new RevenueData. 1373/1373 vitest pass. tsc + eslint clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 15:19:38 +02:00
* Berth mooring as the human sees it. Single-berth EOI the
* primary mooring (e.g. "A1"). Multi-berth EOI the compact range
* (e.g. "A1-A3, B5") produced by `formatBerthRange()`. Single-
* berth output is byte-identical to the legacy primary-only path.
* 2026-05-14: collapsed the prior separate `Berth Range` form field
* into this one the Documenso template has only `Berth Number`,
* and Documenso silently dropped unknown formValues.
feat(eoi): multi-berth EOI generation + berth-range formatter Plan §4.6 + §1: a render function that compresses every berth marked is_in_eoi_bundle=true on an interest into a compact range string ("A1-A3, B5-B7"), wired into both EOI generation paths (the Documenso template-generate call and the in-app pdf-lib AcroForm fill). - src/lib/templates/berth-range.ts: pure formatBerthRange() with the full edge-case set from §4.6 - empty, single, run, gap, multiple prefixes, sort/dedup, multi-letter prefixes, non-canonical passthrough, long ranges. Sorts by (prefix, number); dedupes; passes non-canonical inputs through with a logger warning. - src/lib/templates/merge-fields.ts: new {{eoi.berthRange}} token added to VALID_MERGE_TOKENS allow-list under a fresh `eoi` scope so unknown-token validation at template creation time still rejects typos. - src/lib/services/eoi-context.ts: EoiContext gains eoiBerthRange. Resolved by joining interest_berths (is_in_eoi_bundle=true) → berths and feeding the mooring numbers through formatBerthRange. - src/lib/services/documenso-payload.ts: formValues now includes "Berth Range" alongside the legacy "Berth Number". Multi-berth EOIs surface here; single-berth EOIs duplicate the primary. - src/lib/pdf/fill-eoi-form.ts: in-app AcroForm fill mirrors the Documenso payload by populating "Berth Range". Falls back silently when older PDFs don't have the field (setText is no-op-on-missing). 15 unit tests on the formatter; existing EoiContext + Documenso payload tests updated to assert the new field. 1022 -> 1037 passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 03:03:29 +02:00
*/
feat(reporting): money-math sweep — Step 1 PRE-DEPLOY-PLAN Single coherent commit completing § 1.1 (hot-path correctness) plus § 1.1.4.5 (multi-berth EOI mooring fix). Numbers users see are now self-consistent across dashboard / kanban / hot deals / PDF reports. ## Active-interest sweep (canonical predicate everywhere) Routed every "active interest" filter through `activeInterestsWhere` (commit b966d81 helper). The helper enforces port-scoping + archivedAt IS NULL + outcome IS NULL — strict definition, won is closed. Touched sites: - src/lib/services/reminders.service.ts:digestPort — no longer fires reminders for won/lost/cancelled deals - src/lib/services/berths.service.ts:getLatestInterestStageByBerth - src/lib/services/client-archive-dossier.service.ts (next-in-line others lookup) - src/lib/services/client-archive.service.ts (remaining-under-offer recount before flipping berth back to available) - src/lib/services/client-restore.service.ts (yacht-usage check) - src/lib/services/interests.service.ts:listInterestsForBoard + getInterestStageCounts + the "others on same berth" lookup — kanban / board now exclude terminal deals - src/lib/services/report-generators.ts: fetchPipelineData, fetchRevenueData stage breakdowns, top-N interests ## Pipeline-value currency conversion `getKpis()` now fetches the port's defaultCurrency from `ports` and converts each berth's `priceCurrency`→port-default via `currency.service`. Returns `pipelineValue` + `pipelineValueCurrency` instead of the lying `pipelineValueUsd`. Missing rates fall through to raw amount summing (so the tile still shows an approximate number) — behind a follow-up to surface a "rates incomplete" indicator. 3 consumers updated: KpiCards, PipelineValueTile, ActiveDealsTile. ## Occupancy = sold only Both the dashboard KPI tile and the revenue-report PDF occupancy data now count only `berth.status='sold'`. `under_offer` is a hold, not occupation. The analytics timeline switches from `berth_reservations`-derived to a cumulative-won-deals derivation via `interests.outcome='won' AND outcome_at::date <= day` — same source of truth, historical shape preserved. ## Revenue PDF two-card layout Added `totalForecast` + `pipelineWeights` to `RevenueData`. Summary section now renders both: - "Completed revenue (won)" — money in the bank - "Forecast revenue (pipeline-weighted)" — expected pipeline value Pipeline weights resolve from `system_settings.pipeline_weights` (per-port admin override) and fall back to STAGE_WEIGHTS defaults. PDF and dashboard forecast tiles reconcile. ## Multi-berth EOI mooring (4.5) Documenso `Berth Number` form field now carries the formatBerthRange output for BOTH single- and multi-berth EOIs. Single-berth output is byte-identical to the legacy primary-only path (`formatBerthRange(['A1']) === 'A1'`). Multi-berth EOIs now render the full range ("A1-A3, B5") in the existing field instead of being silently dropped against a nonexistent `Berth Range` field. Dropped: - `'Berth Range'` from the Documenso formValues payload + TS type - `setBerthRange()` helper from fill-eoi-form.ts (now redundant) - The "missing Berth Range AcroForm field" warning log Updated CLAUDE.md to reflect — no Documenso admin template change needed. ## Tests - Updated `documenso-payload.test.ts` — new fixture asserts formatBerthRange output flows into Berth Number; multi-berth case added. - Updated `analytics-service.test.ts:computeOccupancyTimeline` — fixture creates a won interest instead of a reservation. - Updated `alerts-engine.test.ts:interest.stale` — fixture stage switched from dead `'in_communication'` to canonical `'qualified'`. - Updated `report-templates.test.tsx:revenue` — fixture carries `totalForecast` + `pipelineWeights` to match new RevenueData. 1373/1373 vitest pass. tsc + eslint clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 15:19:38 +02:00
'Berth Number': string;
Lease_10: boolean;
Purchase: boolean;
};
fix(audit): comprehensive 2026-05-15 audit fix wave + Documenso v2 polish Bundles the prior session's 50-task fix sweep (Documenso v2 + EOI/signing- progress redesign + env-to-admin migration + dev-mode banner) with the 2026-05-18 audit fix wave (3 CRITICAL, 14 HIGH, 28 MEDIUM, 6 LOW). CRITICAL (3): - C-01 interest-berths INNER JOIN -> LEFT JOIN so hard-deleted berths no longer silently drop interest links - C-02 /setup added to PUBLIC_PATHS; fresh-deploy bootstrap loop fixed - C-03 generic PATCH /interests/[id] no longer accepts pipelineStage — callers must go through /stage with the override-guard chain HIGH (14/15): - H-01 explicit ON DELETE on previously-implicit NO ACTION FKs across interests/documents/reservations/reminders/invoices (migration 0070) - H-02 login page reads ?redirect= param with same-origin guard - H-03 CRM invite token moves to URL fragment so it never lands in nginx access logs / Referer headers - H-04 Retry-After header on sign-in-by-identifier 429 (RFC 6585 §4) - H-05 toggleAccount writes an audit row - H-06 upsertSetting masks any value whose key ends with _encrypted - H-07 archiveClient cascade fires per-interest audit rows - H-08 createSalesTransporter applies SMTP_TIMEOUTS - H-09 AppShell stable children — viewport flip across breakpoint no longer destroys in-progress form drafts - H-10 portal documents page swaps Unicode glyph status icons for Lucide CheckCircle2/XCircle/Circle + aria-labels - H-12 list components swap alert(...) for toast.warning(...) - H-13 5 icon-only buttons gain aria-label - H-14 parseBody treats empty bodies as {} - H-15 admin layout renders a 403 panel instead of silent bounce - H-11 not applicable — mobile-search-overlay IS a mobile bottom-sheet MEDIUM (28+): - M-MT01-05 defense-in-depth port_id/parent-id filters on UPDATE/DELETE WHEREs across custom-fields, notes (all 6 entity types x update + delete), client-contacts, yacht ownerClient lookup, webhook reads - M-D01 documents-hub realtime event-name typo (file:created -> uploaded) - M-EM01 portal-auth emails thread through portId - M-EM02 sendEmail accepts cc/bcc params - M-EM04 notification_digest catalog key - M-IN01 portal presigned download URLs use 4h TTL - M-IN02 OpenAI client lazy-instantiated - M-IN04 stale pdfme refs updated to pdf-lib AcroForm - M-IN05 umami.testConnection returns tagged union - M-L01 reservations tenure_type unified with berths - M-L02 report-generators canonicalize stage values - M-AU01 audit log placeholder copy fixed - M-AU04 outcome_set / outcome_cleared distinct audit verbs - M-NEW-2 activity feed entity name+type separator - M-R01 portal allowlist narrowed + portal_session backstop in proxy - M-SC02 companies archived partial index - M-SC04 audit_logs.searchText documented as DB-managed - M-S01 storage_s3_access_key_encrypted admin field - M-U01 audit log empty state uses <EmptyState> - M-U09 invoice delete dialog -> <AlertDialog> - M-U10 toast.success on ClientForm + InterestForm create/edit - M-U11 settings-form-card logo preview alt text - M-U14 mobile topbar title on clients/yachts/interests/berths - M-U15 Invoices in mobile More-sheet LOW (6/8): - L-AU01 severity defaults for security-relevant verbs - L-AU02 +13 missing actions in admin audit filter - L-AU03 +7 missing entity types in admin audit filter - L-AU04 dead listAuditLogs stubbed - L-D02 CLAUDE.md Owner-wins chain tightened Bonus — Document detail polish (#67 partial, 3/6 deliverables): - state-aware action button per signer - watcher Add UI with display-name resolution - cleanSignerName cleanup Prior session work bundled in: - Documenso v2 webhook + envelope-ID normalization + sequential signing - SigningProgress UI redesign (avatars, per-signer state, timestamps) - env->admin settings registry + RegistryDrivenForm + encrypted creds - Embedded-signing card + Test connection + setup help - Dev-mode EMAIL_REDIRECT_TO banner - Pipeline rules admin page - Sales email config card - Audit log details Sheet - EOI tab: Finalising badge, absolute timestamps, sequential indicator - Notes pipeline_stage_at_creation (migration 0069) - Documenso numeric ID dual-key webhook (migration 0068) - Dimensions criterion copy (migration 0067) Tests: 1374/1374 vitest pass. tsc clean. lint clean. See docs/AUDIT-FIX-WAVE-2026-05-18.md for the full progress report and the user-input items still pending. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 13:28:50 +02:00
/**
* v2-native path: prefill values keyed by field ID. Generated by mapping
* each `formValues` entry through the cached `documenso_eoi_field_map`
* (name ID) discovered via the admin's "Sync from Documenso" button.
* v1 instances ignore this field; v2 instances accept either prefillFields
* OR formValues but prefillFields-by-ID is the canonical modern path.
*/
prefillFields?: Array<{
id: number;
type: 'text' | 'number' | 'date' | 'checkbox' | 'dropdown';
value: string;
}>;
recipients: Array<{
id: number;
name: string;
email: string;
fix(audit): comprehensive 2026-05-15 audit fix wave + Documenso v2 polish Bundles the prior session's 50-task fix sweep (Documenso v2 + EOI/signing- progress redesign + env-to-admin migration + dev-mode banner) with the 2026-05-18 audit fix wave (3 CRITICAL, 14 HIGH, 28 MEDIUM, 6 LOW). CRITICAL (3): - C-01 interest-berths INNER JOIN -> LEFT JOIN so hard-deleted berths no longer silently drop interest links - C-02 /setup added to PUBLIC_PATHS; fresh-deploy bootstrap loop fixed - C-03 generic PATCH /interests/[id] no longer accepts pipelineStage — callers must go through /stage with the override-guard chain HIGH (14/15): - H-01 explicit ON DELETE on previously-implicit NO ACTION FKs across interests/documents/reservations/reminders/invoices (migration 0070) - H-02 login page reads ?redirect= param with same-origin guard - H-03 CRM invite token moves to URL fragment so it never lands in nginx access logs / Referer headers - H-04 Retry-After header on sign-in-by-identifier 429 (RFC 6585 §4) - H-05 toggleAccount writes an audit row - H-06 upsertSetting masks any value whose key ends with _encrypted - H-07 archiveClient cascade fires per-interest audit rows - H-08 createSalesTransporter applies SMTP_TIMEOUTS - H-09 AppShell stable children — viewport flip across breakpoint no longer destroys in-progress form drafts - H-10 portal documents page swaps Unicode glyph status icons for Lucide CheckCircle2/XCircle/Circle + aria-labels - H-12 list components swap alert(...) for toast.warning(...) - H-13 5 icon-only buttons gain aria-label - H-14 parseBody treats empty bodies as {} - H-15 admin layout renders a 403 panel instead of silent bounce - H-11 not applicable — mobile-search-overlay IS a mobile bottom-sheet MEDIUM (28+): - M-MT01-05 defense-in-depth port_id/parent-id filters on UPDATE/DELETE WHEREs across custom-fields, notes (all 6 entity types x update + delete), client-contacts, yacht ownerClient lookup, webhook reads - M-D01 documents-hub realtime event-name typo (file:created -> uploaded) - M-EM01 portal-auth emails thread through portId - M-EM02 sendEmail accepts cc/bcc params - M-EM04 notification_digest catalog key - M-IN01 portal presigned download URLs use 4h TTL - M-IN02 OpenAI client lazy-instantiated - M-IN04 stale pdfme refs updated to pdf-lib AcroForm - M-IN05 umami.testConnection returns tagged union - M-L01 reservations tenure_type unified with berths - M-L02 report-generators canonicalize stage values - M-AU01 audit log placeholder copy fixed - M-AU04 outcome_set / outcome_cleared distinct audit verbs - M-NEW-2 activity feed entity name+type separator - M-R01 portal allowlist narrowed + portal_session backstop in proxy - M-SC02 companies archived partial index - M-SC04 audit_logs.searchText documented as DB-managed - M-S01 storage_s3_access_key_encrypted admin field - M-U01 audit log empty state uses <EmptyState> - M-U09 invoice delete dialog -> <AlertDialog> - M-U10 toast.success on ClientForm + InterestForm create/edit - M-U11 settings-form-card logo preview alt text - M-U14 mobile topbar title on clients/yachts/interests/berths - M-U15 Invoices in mobile More-sheet LOW (6/8): - L-AU01 severity defaults for security-relevant verbs - L-AU02 +13 missing actions in admin audit filter - L-AU03 +7 missing entity types in admin audit filter - L-AU04 dead listAuditLogs stubbed - L-D02 CLAUDE.md Owner-wins chain tightened Bonus — Document detail polish (#67 partial, 3/6 deliverables): - state-aware action button per signer - watcher Add UI with display-name resolution - cleanSignerName cleanup Prior session work bundled in: - Documenso v2 webhook + envelope-ID normalization + sequential signing - SigningProgress UI redesign (avatars, per-signer state, timestamps) - env->admin settings registry + RegistryDrivenForm + encrypted creds - Embedded-signing card + Test connection + setup help - Dev-mode EMAIL_REDIRECT_TO banner - Pipeline rules admin page - Sales email config card - Audit log details Sheet - EOI tab: Finalising badge, absolute timestamps, sequential indicator - Notes pipeline_stage_at_creation (migration 0069) - Documenso numeric ID dual-key webhook (migration 0068) - Dimensions criterion copy (migration 0067) Tests: 1374/1374 vitest pass. tsc clean. lint clean. See docs/AUDIT-FIX-WAVE-2026-05-18.md for the full progress report and the user-input items still pending. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 13:28:50 +02:00
role: 'SIGNER' | 'APPROVER' | 'CC' | 'VIEWER';
signingOrder: number;
}>;
fix(audit): comprehensive 2026-05-15 audit fix wave + Documenso v2 polish Bundles the prior session's 50-task fix sweep (Documenso v2 + EOI/signing- progress redesign + env-to-admin migration + dev-mode banner) with the 2026-05-18 audit fix wave (3 CRITICAL, 14 HIGH, 28 MEDIUM, 6 LOW). CRITICAL (3): - C-01 interest-berths INNER JOIN -> LEFT JOIN so hard-deleted berths no longer silently drop interest links - C-02 /setup added to PUBLIC_PATHS; fresh-deploy bootstrap loop fixed - C-03 generic PATCH /interests/[id] no longer accepts pipelineStage — callers must go through /stage with the override-guard chain HIGH (14/15): - H-01 explicit ON DELETE on previously-implicit NO ACTION FKs across interests/documents/reservations/reminders/invoices (migration 0070) - H-02 login page reads ?redirect= param with same-origin guard - H-03 CRM invite token moves to URL fragment so it never lands in nginx access logs / Referer headers - H-04 Retry-After header on sign-in-by-identifier 429 (RFC 6585 §4) - H-05 toggleAccount writes an audit row - H-06 upsertSetting masks any value whose key ends with _encrypted - H-07 archiveClient cascade fires per-interest audit rows - H-08 createSalesTransporter applies SMTP_TIMEOUTS - H-09 AppShell stable children — viewport flip across breakpoint no longer destroys in-progress form drafts - H-10 portal documents page swaps Unicode glyph status icons for Lucide CheckCircle2/XCircle/Circle + aria-labels - H-12 list components swap alert(...) for toast.warning(...) - H-13 5 icon-only buttons gain aria-label - H-14 parseBody treats empty bodies as {} - H-15 admin layout renders a 403 panel instead of silent bounce - H-11 not applicable — mobile-search-overlay IS a mobile bottom-sheet MEDIUM (28+): - M-MT01-05 defense-in-depth port_id/parent-id filters on UPDATE/DELETE WHEREs across custom-fields, notes (all 6 entity types x update + delete), client-contacts, yacht ownerClient lookup, webhook reads - M-D01 documents-hub realtime event-name typo (file:created -> uploaded) - M-EM01 portal-auth emails thread through portId - M-EM02 sendEmail accepts cc/bcc params - M-EM04 notification_digest catalog key - M-IN01 portal presigned download URLs use 4h TTL - M-IN02 OpenAI client lazy-instantiated - M-IN04 stale pdfme refs updated to pdf-lib AcroForm - M-IN05 umami.testConnection returns tagged union - M-L01 reservations tenure_type unified with berths - M-L02 report-generators canonicalize stage values - M-AU01 audit log placeholder copy fixed - M-AU04 outcome_set / outcome_cleared distinct audit verbs - M-NEW-2 activity feed entity name+type separator - M-R01 portal allowlist narrowed + portal_session backstop in proxy - M-SC02 companies archived partial index - M-SC04 audit_logs.searchText documented as DB-managed - M-S01 storage_s3_access_key_encrypted admin field - M-U01 audit log empty state uses <EmptyState> - M-U09 invoice delete dialog -> <AlertDialog> - M-U10 toast.success on ClientForm + InterestForm create/edit - M-U11 settings-form-card logo preview alt text - M-U14 mobile topbar title on clients/yachts/interests/berths - M-U15 Invoices in mobile More-sheet LOW (6/8): - L-AU01 severity defaults for security-relevant verbs - L-AU02 +13 missing actions in admin audit filter - L-AU03 +7 missing entity types in admin audit filter - L-AU04 dead listAuditLogs stubbed - L-D02 CLAUDE.md Owner-wins chain tightened Bonus — Document detail polish (#67 partial, 3/6 deliverables): - state-aware action button per signer - watcher Add UI with display-name resolution - cleanSignerName cleanup Prior session work bundled in: - Documenso v2 webhook + envelope-ID normalization + sequential signing - SigningProgress UI redesign (avatars, per-signer state, timestamps) - env->admin settings registry + RegistryDrivenForm + encrypted creds - Embedded-signing card + Test connection + setup help - Dev-mode EMAIL_REDIRECT_TO banner - Pipeline rules admin page - Sales email config card - Audit log details Sheet - EOI tab: Finalising badge, absolute timestamps, sequential indicator - Notes pipeline_stage_at_creation (migration 0069) - Documenso numeric ID dual-key webhook (migration 0068) - Dimensions criterion copy (migration 0067) Tests: 1374/1374 vitest pass. tsc clean. lint clean. See docs/AUDIT-FIX-WAVE-2026-05-18.md for the full progress report and the user-input items still pending. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 13:28:50 +02:00
/**
* Extra recipients beyond the canonical client + developer + approver trio.
* Used by the "send a copy to my manager" workflow: pass CC slots here and
* they'll be appended to the recipients array at send time.
*/
extraRecipients?: Array<{
id: number;
name: string;
email: string;
role: 'CC' | 'VIEWER';
signingOrder?: number;
}>;
}
export interface DocumensoPayloadOptions {
/** `interestId` used to build `externalId` and Documenso referencing. */
interestId: string;
/** Documenso recipient IDs - come from env vars. */
clientRecipientId: number;
developerRecipientId: number;
approvalRecipientId: number;
/** Hardcoded developer + approver names/emails (legacy). */
developerName?: string;
developerEmail?: string;
approverName?: string;
approverEmail?: string;
/** Redirect URL after signing. Defaults to the app URL. */
redirectUrl?: string;
feat(documenso): full v2 endpoint coverage + sequential signing + redirectUrl Wire up the remaining version-aware paths so a port pointed at Documenso 2.x takes the v2 endpoint on every CRUD operation, with two new v2-only settings exposed in admin UI. documenso-client.ts: - createDocument: v2 multipart /envelope/create + getDocument follow-up to return the full doc shape (v1 path unchanged) - sendDocument: v2 /envelope/distribute (returns per-recipient signingUrl in the same response — eliminates the v1 separate-GET round-trip) - sendReminder: v2 /envelope/redistribute with recipientIds filter - downloadSignedPdf: v2 /envelope/{id}/download - CreateDocumentMeta type: { subject, message, redirectUrl, signingOrder } threaded through v1 + v2 paths (v1 ignores signingOrder) port-config.ts: - New settings: documenso_signing_order (PARALLEL/SEQUENTIAL, v2-only), documenso_redirect_url (both versions honour) - PortDocumensoConfig gains signingOrder + redirectUrl documenso-payload.ts: - DocumensoTemplatePayload.meta gains signingOrder - buildDocumensoPayload reads from options.signingOrder, omits when null document-templates.ts (EOI template flow): - Pass docCfg.signingOrder + docCfg.redirectUrl into buildDocumensoPayload documents.service.ts (sendForSigning uploaded-doc flow): - Pass portId to documensoCreate + documensoSend (was missing) - Thread signingOrder + redirectUrl via the new meta param Admin Documenso settings page: - v2 benefits card updated: now lists envelope CRUD, one-call send, sequential enforcement, post-sign redirect as wired (was roadmap) - Roadmap callout pruned to the three remaining deferred items: template/use migration, /envelope/update, non-SIGNER recipient roles - New "v2 signing behaviour" SettingsFormCard with the two new settings Template flow stays on /api/v1/templates/{id}/generate-document by design — Documenso 2.x accepts v1 endpoints via backward compat; full migration to v2 /template/use requires per-template field-ID capture (admin schema work, deferred). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 14:38:45 +02:00
/**
* PARALLEL (default) or SEQUENTIAL v2-only enforcement (v1 ignores).
* Set via per-port `documenso_signing_order` system_settings key.
*/
signingOrder?: 'PARALLEL' | 'SEQUENTIAL';
fix(audit): comprehensive 2026-05-15 audit fix wave + Documenso v2 polish Bundles the prior session's 50-task fix sweep (Documenso v2 + EOI/signing- progress redesign + env-to-admin migration + dev-mode banner) with the 2026-05-18 audit fix wave (3 CRITICAL, 14 HIGH, 28 MEDIUM, 6 LOW). CRITICAL (3): - C-01 interest-berths INNER JOIN -> LEFT JOIN so hard-deleted berths no longer silently drop interest links - C-02 /setup added to PUBLIC_PATHS; fresh-deploy bootstrap loop fixed - C-03 generic PATCH /interests/[id] no longer accepts pipelineStage — callers must go through /stage with the override-guard chain HIGH (14/15): - H-01 explicit ON DELETE on previously-implicit NO ACTION FKs across interests/documents/reservations/reminders/invoices (migration 0070) - H-02 login page reads ?redirect= param with same-origin guard - H-03 CRM invite token moves to URL fragment so it never lands in nginx access logs / Referer headers - H-04 Retry-After header on sign-in-by-identifier 429 (RFC 6585 §4) - H-05 toggleAccount writes an audit row - H-06 upsertSetting masks any value whose key ends with _encrypted - H-07 archiveClient cascade fires per-interest audit rows - H-08 createSalesTransporter applies SMTP_TIMEOUTS - H-09 AppShell stable children — viewport flip across breakpoint no longer destroys in-progress form drafts - H-10 portal documents page swaps Unicode glyph status icons for Lucide CheckCircle2/XCircle/Circle + aria-labels - H-12 list components swap alert(...) for toast.warning(...) - H-13 5 icon-only buttons gain aria-label - H-14 parseBody treats empty bodies as {} - H-15 admin layout renders a 403 panel instead of silent bounce - H-11 not applicable — mobile-search-overlay IS a mobile bottom-sheet MEDIUM (28+): - M-MT01-05 defense-in-depth port_id/parent-id filters on UPDATE/DELETE WHEREs across custom-fields, notes (all 6 entity types x update + delete), client-contacts, yacht ownerClient lookup, webhook reads - M-D01 documents-hub realtime event-name typo (file:created -> uploaded) - M-EM01 portal-auth emails thread through portId - M-EM02 sendEmail accepts cc/bcc params - M-EM04 notification_digest catalog key - M-IN01 portal presigned download URLs use 4h TTL - M-IN02 OpenAI client lazy-instantiated - M-IN04 stale pdfme refs updated to pdf-lib AcroForm - M-IN05 umami.testConnection returns tagged union - M-L01 reservations tenure_type unified with berths - M-L02 report-generators canonicalize stage values - M-AU01 audit log placeholder copy fixed - M-AU04 outcome_set / outcome_cleared distinct audit verbs - M-NEW-2 activity feed entity name+type separator - M-R01 portal allowlist narrowed + portal_session backstop in proxy - M-SC02 companies archived partial index - M-SC04 audit_logs.searchText documented as DB-managed - M-S01 storage_s3_access_key_encrypted admin field - M-U01 audit log empty state uses <EmptyState> - M-U09 invoice delete dialog -> <AlertDialog> - M-U10 toast.success on ClientForm + InterestForm create/edit - M-U11 settings-form-card logo preview alt text - M-U14 mobile topbar title on clients/yachts/interests/berths - M-U15 Invoices in mobile More-sheet LOW (6/8): - L-AU01 severity defaults for security-relevant verbs - L-AU02 +13 missing actions in admin audit filter - L-AU03 +7 missing entity types in admin audit filter - L-AU04 dead listAuditLogs stubbed - L-D02 CLAUDE.md Owner-wins chain tightened Bonus — Document detail polish (#67 partial, 3/6 deliverables): - state-aware action button per signer - watcher Add UI with display-name resolution - cleanSignerName cleanup Prior session work bundled in: - Documenso v2 webhook + envelope-ID normalization + sequential signing - SigningProgress UI redesign (avatars, per-signer state, timestamps) - env->admin settings registry + RegistryDrivenForm + encrypted creds - Embedded-signing card + Test connection + setup help - Dev-mode EMAIL_REDIRECT_TO banner - Pipeline rules admin page - Sales email config card - Audit log details Sheet - EOI tab: Finalising badge, absolute timestamps, sequential indicator - Notes pipeline_stage_at_creation (migration 0069) - Documenso numeric ID dual-key webhook (migration 0068) - Dimensions criterion copy (migration 0067) Tests: 1374/1374 vitest pass. tsc clean. lint clean. See docs/AUDIT-FIX-WAVE-2026-05-18.md for the full progress report and the user-input items still pending. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 13:28:50 +02:00
/**
* Optional extra recipients beyond the canonical client+developer+approver
* trio. Used by the "send a copy to my manager" workflow. CC = receives a
* copy of the signed PDF; VIEWER = can view but not sign. Slot IDs must
* exist on the Documenso template (CRM operator adds them in the template
* editor first). v2-only on v2 instances; v1 ignores unknown roles.
*/
extraRecipients?: Array<{
id: number;
name: string;
email: string;
role: 'CC' | 'VIEWER';
signingOrder?: number;
}>;
/**
* Which side of the yacht's stored dimensions (ft|m) flows into the EOI's
* Length/Width/Draft formValues. Defaults to 'ft' when omitted for legacy
* call sites; the EOI-generate drawer always supplies the rep's choice.
*/
dimensionUnit?: 'ft' | 'm';
}
feat(branding): multi-tenant brand naming + per-port email shell + auth UI continuity Removes the last hardcoded "Port Nimara" references so a tenant cloning the deploy with a fresh slug sees their own brand throughout. Browser + native chrome: - `generateMetadata` reads `branding_app_name` from the first port row so the browser tab title, apple-web-app title, and template literal reflect the tenant (fallback "CRM" until DB is seeded). - Mobile topbar derives the brand-mark initials from the port slug ("port-nimara" → "PN", "marina-alpha" → "MA") — no code edit on clone. - `documenso-payload` default redirect URL is `""` so Documenso falls back to its own post-sign page instead of routing every tenant's signers to portnimara.com; per-port `redirectUrl` setting still wins. - Server-startup log uses generic "CRM server listening". Email + auth shell: - New `auth-shell-branding.ts` resolves logo / background / appName once per request from `system_settings`; used by both the email shell and the auth-pages SSR layout. - `auth-branding-provider` wraps `/login`, `/reset-password`, `/set-password`, portal `/portal/*` so the branded shell hydrates with the same assets the inbox sees. - `me/email` change email uses the branded shell instead of inline HTML with "Port Nimara CRM" baked into copy. - Admin branding page adds an email-preview card (POSTs to `/api/v1/admin/branding/email-preview`) so an admin can spot-check their templates before going live. - `/api/public/files/[id]` exposes branding-category files anonymously so inbox images (no session cookie) can render; any other category still flows through authenticated `/api/v1/files/[id]/preview`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 15:54:10 +02:00
// Empty string lets Documenso fall back to its own default post-sign
// landing page when the port admin hasn't configured a redirect URL.
// Never hardcode a tenant's marketing-site URL here — that would route
// every other port's signers to the wrong host.
const DEFAULT_REDIRECT_URL = '';
feat(sales): admin-configurable EOI signers + richer timeline events 1. Per-port EOI signer config - New `eoi_signers` system_settings key (JSON: { developer, approver }, each `{ name, email }`). Settings UI exposes it under Admin → Settings. - getPortEoiSigners(portId) reads the setting with a typed validator; falls back to the legacy David Mizrahi / Abbie May defaults if the row is missing or malformed (so older ports keep working until an admin saves a value). - Both EOI generation pathways now read from the helper instead of hardcoded constants: * documenso-template path (generateAndSignViaDocumensoTemplate) * in-app PDF-fill path (generateAndSignViaInApp) 2. Timeline upgrades The interest detail Activity tab now distinguishes the new automation events that arrived with sessions 1+2: - Stage auto-advances (userId='system') get a small "Auto" pill and carry their reason into the description (e.g. "Stage advanced to EOI Signed (auto-advanced — EOI signed via Documenso)"). - outcome_set events show "Marked as Won" / "Marked as Lost — went to another marina" with optional reason; trophy/X icons. - outcome_cleared events show "Reopened to {stage}" with a refresh icon. - Document events humanized: "Document 'X' fully signed" instead of "Document X: completed". - Stage labels run through stageLabel() so the timeline shows the human label, not the enum key. - Timestamps switched to relative-time with full-date tooltip. - "by system" is rendered plainly (no longer the literal user-id). tsc clean. vitest 832/832 pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 00:19:55 +02:00
export interface EoiSignerConfig {
developer: { name: string; email: string };
approver: { name: string; email: string };
}
fix(audit): comprehensive 2026-05-15 audit fix wave + Documenso v2 polish Bundles the prior session's 50-task fix sweep (Documenso v2 + EOI/signing- progress redesign + env-to-admin migration + dev-mode banner) with the 2026-05-18 audit fix wave (3 CRITICAL, 14 HIGH, 28 MEDIUM, 6 LOW). CRITICAL (3): - C-01 interest-berths INNER JOIN -> LEFT JOIN so hard-deleted berths no longer silently drop interest links - C-02 /setup added to PUBLIC_PATHS; fresh-deploy bootstrap loop fixed - C-03 generic PATCH /interests/[id] no longer accepts pipelineStage — callers must go through /stage with the override-guard chain HIGH (14/15): - H-01 explicit ON DELETE on previously-implicit NO ACTION FKs across interests/documents/reservations/reminders/invoices (migration 0070) - H-02 login page reads ?redirect= param with same-origin guard - H-03 CRM invite token moves to URL fragment so it never lands in nginx access logs / Referer headers - H-04 Retry-After header on sign-in-by-identifier 429 (RFC 6585 §4) - H-05 toggleAccount writes an audit row - H-06 upsertSetting masks any value whose key ends with _encrypted - H-07 archiveClient cascade fires per-interest audit rows - H-08 createSalesTransporter applies SMTP_TIMEOUTS - H-09 AppShell stable children — viewport flip across breakpoint no longer destroys in-progress form drafts - H-10 portal documents page swaps Unicode glyph status icons for Lucide CheckCircle2/XCircle/Circle + aria-labels - H-12 list components swap alert(...) for toast.warning(...) - H-13 5 icon-only buttons gain aria-label - H-14 parseBody treats empty bodies as {} - H-15 admin layout renders a 403 panel instead of silent bounce - H-11 not applicable — mobile-search-overlay IS a mobile bottom-sheet MEDIUM (28+): - M-MT01-05 defense-in-depth port_id/parent-id filters on UPDATE/DELETE WHEREs across custom-fields, notes (all 6 entity types x update + delete), client-contacts, yacht ownerClient lookup, webhook reads - M-D01 documents-hub realtime event-name typo (file:created -> uploaded) - M-EM01 portal-auth emails thread through portId - M-EM02 sendEmail accepts cc/bcc params - M-EM04 notification_digest catalog key - M-IN01 portal presigned download URLs use 4h TTL - M-IN02 OpenAI client lazy-instantiated - M-IN04 stale pdfme refs updated to pdf-lib AcroForm - M-IN05 umami.testConnection returns tagged union - M-L01 reservations tenure_type unified with berths - M-L02 report-generators canonicalize stage values - M-AU01 audit log placeholder copy fixed - M-AU04 outcome_set / outcome_cleared distinct audit verbs - M-NEW-2 activity feed entity name+type separator - M-R01 portal allowlist narrowed + portal_session backstop in proxy - M-SC02 companies archived partial index - M-SC04 audit_logs.searchText documented as DB-managed - M-S01 storage_s3_access_key_encrypted admin field - M-U01 audit log empty state uses <EmptyState> - M-U09 invoice delete dialog -> <AlertDialog> - M-U10 toast.success on ClientForm + InterestForm create/edit - M-U11 settings-form-card logo preview alt text - M-U14 mobile topbar title on clients/yachts/interests/berths - M-U15 Invoices in mobile More-sheet LOW (6/8): - L-AU01 severity defaults for security-relevant verbs - L-AU02 +13 missing actions in admin audit filter - L-AU03 +7 missing entity types in admin audit filter - L-AU04 dead listAuditLogs stubbed - L-D02 CLAUDE.md Owner-wins chain tightened Bonus — Document detail polish (#67 partial, 3/6 deliverables): - state-aware action button per signer - watcher Add UI with display-name resolution - cleanSignerName cleanup Prior session work bundled in: - Documenso v2 webhook + envelope-ID normalization + sequential signing - SigningProgress UI redesign (avatars, per-signer state, timestamps) - env->admin settings registry + RegistryDrivenForm + encrypted creds - Embedded-signing card + Test connection + setup help - Dev-mode EMAIL_REDIRECT_TO banner - Pipeline rules admin page - Sales email config card - Audit log details Sheet - EOI tab: Finalising badge, absolute timestamps, sequential indicator - Notes pipeline_stage_at_creation (migration 0069) - Documenso numeric ID dual-key webhook (migration 0068) - Dimensions criterion copy (migration 0067) Tests: 1374/1374 vitest pass. tsc clean. lint clean. See docs/AUDIT-FIX-WAVE-2026-05-18.md for the full progress report and the user-input items still pending. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 13:28:50 +02:00
const EMPTY_SIGNERS: EoiSignerConfig = {
developer: { name: '', email: '' },
approver: { name: '', email: '' },
feat(sales): admin-configurable EOI signers + richer timeline events 1. Per-port EOI signer config - New `eoi_signers` system_settings key (JSON: { developer, approver }, each `{ name, email }`). Settings UI exposes it under Admin → Settings. - getPortEoiSigners(portId) reads the setting with a typed validator; falls back to the legacy David Mizrahi / Abbie May defaults if the row is missing or malformed (so older ports keep working until an admin saves a value). - Both EOI generation pathways now read from the helper instead of hardcoded constants: * documenso-template path (generateAndSignViaDocumensoTemplate) * in-app PDF-fill path (generateAndSignViaInApp) 2. Timeline upgrades The interest detail Activity tab now distinguishes the new automation events that arrived with sessions 1+2: - Stage auto-advances (userId='system') get a small "Auto" pill and carry their reason into the description (e.g. "Stage advanced to EOI Signed (auto-advanced — EOI signed via Documenso)"). - outcome_set events show "Marked as Won" / "Marked as Lost — went to another marina" with optional reason; trophy/X icons. - outcome_cleared events show "Reopened to {stage}" with a refresh icon. - Document events humanized: "Document 'X' fully signed" instead of "Document X: completed". - Stage labels run through stageLabel() so the timeline shows the human label, not the enum key. - Timestamps switched to relative-time with full-date tooltip. - "by system" is rendered plainly (no longer the literal user-id). tsc clean. vitest 832/832 pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 00:19:55 +02:00
};
function isSignerEntry(v: unknown): v is { name: string; email: string } {
return (
!!v &&
typeof v === 'object' &&
typeof (v as Record<string, unknown>).name === 'string' &&
typeof (v as Record<string, unknown>).email === 'string' &&
!!(v as Record<string, string>).name &&
!!(v as Record<string, string>).email
);
}
fix(audit): comprehensive 2026-05-15 audit fix wave + Documenso v2 polish Bundles the prior session's 50-task fix sweep (Documenso v2 + EOI/signing- progress redesign + env-to-admin migration + dev-mode banner) with the 2026-05-18 audit fix wave (3 CRITICAL, 14 HIGH, 28 MEDIUM, 6 LOW). CRITICAL (3): - C-01 interest-berths INNER JOIN -> LEFT JOIN so hard-deleted berths no longer silently drop interest links - C-02 /setup added to PUBLIC_PATHS; fresh-deploy bootstrap loop fixed - C-03 generic PATCH /interests/[id] no longer accepts pipelineStage — callers must go through /stage with the override-guard chain HIGH (14/15): - H-01 explicit ON DELETE on previously-implicit NO ACTION FKs across interests/documents/reservations/reminders/invoices (migration 0070) - H-02 login page reads ?redirect= param with same-origin guard - H-03 CRM invite token moves to URL fragment so it never lands in nginx access logs / Referer headers - H-04 Retry-After header on sign-in-by-identifier 429 (RFC 6585 §4) - H-05 toggleAccount writes an audit row - H-06 upsertSetting masks any value whose key ends with _encrypted - H-07 archiveClient cascade fires per-interest audit rows - H-08 createSalesTransporter applies SMTP_TIMEOUTS - H-09 AppShell stable children — viewport flip across breakpoint no longer destroys in-progress form drafts - H-10 portal documents page swaps Unicode glyph status icons for Lucide CheckCircle2/XCircle/Circle + aria-labels - H-12 list components swap alert(...) for toast.warning(...) - H-13 5 icon-only buttons gain aria-label - H-14 parseBody treats empty bodies as {} - H-15 admin layout renders a 403 panel instead of silent bounce - H-11 not applicable — mobile-search-overlay IS a mobile bottom-sheet MEDIUM (28+): - M-MT01-05 defense-in-depth port_id/parent-id filters on UPDATE/DELETE WHEREs across custom-fields, notes (all 6 entity types x update + delete), client-contacts, yacht ownerClient lookup, webhook reads - M-D01 documents-hub realtime event-name typo (file:created -> uploaded) - M-EM01 portal-auth emails thread through portId - M-EM02 sendEmail accepts cc/bcc params - M-EM04 notification_digest catalog key - M-IN01 portal presigned download URLs use 4h TTL - M-IN02 OpenAI client lazy-instantiated - M-IN04 stale pdfme refs updated to pdf-lib AcroForm - M-IN05 umami.testConnection returns tagged union - M-L01 reservations tenure_type unified with berths - M-L02 report-generators canonicalize stage values - M-AU01 audit log placeholder copy fixed - M-AU04 outcome_set / outcome_cleared distinct audit verbs - M-NEW-2 activity feed entity name+type separator - M-R01 portal allowlist narrowed + portal_session backstop in proxy - M-SC02 companies archived partial index - M-SC04 audit_logs.searchText documented as DB-managed - M-S01 storage_s3_access_key_encrypted admin field - M-U01 audit log empty state uses <EmptyState> - M-U09 invoice delete dialog -> <AlertDialog> - M-U10 toast.success on ClientForm + InterestForm create/edit - M-U11 settings-form-card logo preview alt text - M-U14 mobile topbar title on clients/yachts/interests/berths - M-U15 Invoices in mobile More-sheet LOW (6/8): - L-AU01 severity defaults for security-relevant verbs - L-AU02 +13 missing actions in admin audit filter - L-AU03 +7 missing entity types in admin audit filter - L-AU04 dead listAuditLogs stubbed - L-D02 CLAUDE.md Owner-wins chain tightened Bonus — Document detail polish (#67 partial, 3/6 deliverables): - state-aware action button per signer - watcher Add UI with display-name resolution - cleanSignerName cleanup Prior session work bundled in: - Documenso v2 webhook + envelope-ID normalization + sequential signing - SigningProgress UI redesign (avatars, per-signer state, timestamps) - env->admin settings registry + RegistryDrivenForm + encrypted creds - Embedded-signing card + Test connection + setup help - Dev-mode EMAIL_REDIRECT_TO banner - Pipeline rules admin page - Sales email config card - Audit log details Sheet - EOI tab: Finalising badge, absolute timestamps, sequential indicator - Notes pipeline_stage_at_creation (migration 0069) - Documenso numeric ID dual-key webhook (migration 0068) - Dimensions criterion copy (migration 0067) Tests: 1374/1374 vitest pass. tsc clean. lint clean. See docs/AUDIT-FIX-WAVE-2026-05-18.md for the full progress report and the user-input items still pending. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 13:28:50 +02:00
/** Look up `{name, email}` for a CRM user id by joining `userProfiles`
* (display name) + `user` (auth email). Returns nulls on miss. */
async function resolveCrmUser(
userId: string | null,
): Promise<{ name: string; email: string } | null> {
if (!userId) return null;
const [row] = await db
.select({
displayName: userProfiles.displayName,
email: user.email,
})
.from(user)
.leftJoin(userProfiles, eq(userProfiles.userId, user.id))
.where(eq(user.id, userId))
.limit(1);
if (!row || !row.email) return null;
return { name: row.displayName ?? row.email, email: row.email };
}
/**
* Resolve the developer + approver name/email for the EOI signing trio.
*
* Priority chain per slot (highest lowest):
* 1. Linked CRM user (`documenso_<role>_user_id`) recommended path
* because "the person on this slot" changes via a CRM admin re-link,
* not a Documenso template edit. The display name comes from
* `userProfiles.displayName`, the email from `user.email`.
* 2. Free-text overrides (`documenso_<role>_name` +
* `documenso_<role>_email`) for ports where the signer isn't a
* CRM-platform user (e.g. external counsel).
* 3. Legacy `eoi_signers` JSON blob kept for backward compat with
* ports that haven't migrated to the registry-driven settings yet.
* 4. Empty strings let the Documenso template's stored values win.
*
* Either slot can resolve via a different tier than the other.
*/
feat(sales): admin-configurable EOI signers + richer timeline events 1. Per-port EOI signer config - New `eoi_signers` system_settings key (JSON: { developer, approver }, each `{ name, email }`). Settings UI exposes it under Admin → Settings. - getPortEoiSigners(portId) reads the setting with a typed validator; falls back to the legacy David Mizrahi / Abbie May defaults if the row is missing or malformed (so older ports keep working until an admin saves a value). - Both EOI generation pathways now read from the helper instead of hardcoded constants: * documenso-template path (generateAndSignViaDocumensoTemplate) * in-app PDF-fill path (generateAndSignViaInApp) 2. Timeline upgrades The interest detail Activity tab now distinguishes the new automation events that arrived with sessions 1+2: - Stage auto-advances (userId='system') get a small "Auto" pill and carry their reason into the description (e.g. "Stage advanced to EOI Signed (auto-advanced — EOI signed via Documenso)"). - outcome_set events show "Marked as Won" / "Marked as Lost — went to another marina" with optional reason; trophy/X icons. - outcome_cleared events show "Reopened to {stage}" with a refresh icon. - Document events humanized: "Document 'X' fully signed" instead of "Document X: completed". - Stage labels run through stageLabel() so the timeline shows the human label, not the enum key. - Timestamps switched to relative-time with full-date tooltip. - "by system" is rendered plainly (no longer the literal user-id). tsc clean. vitest 832/832 pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 00:19:55 +02:00
export async function getPortEoiSigners(portId: string): Promise<EoiSignerConfig> {
fix(audit): comprehensive 2026-05-15 audit fix wave + Documenso v2 polish Bundles the prior session's 50-task fix sweep (Documenso v2 + EOI/signing- progress redesign + env-to-admin migration + dev-mode banner) with the 2026-05-18 audit fix wave (3 CRITICAL, 14 HIGH, 28 MEDIUM, 6 LOW). CRITICAL (3): - C-01 interest-berths INNER JOIN -> LEFT JOIN so hard-deleted berths no longer silently drop interest links - C-02 /setup added to PUBLIC_PATHS; fresh-deploy bootstrap loop fixed - C-03 generic PATCH /interests/[id] no longer accepts pipelineStage — callers must go through /stage with the override-guard chain HIGH (14/15): - H-01 explicit ON DELETE on previously-implicit NO ACTION FKs across interests/documents/reservations/reminders/invoices (migration 0070) - H-02 login page reads ?redirect= param with same-origin guard - H-03 CRM invite token moves to URL fragment so it never lands in nginx access logs / Referer headers - H-04 Retry-After header on sign-in-by-identifier 429 (RFC 6585 §4) - H-05 toggleAccount writes an audit row - H-06 upsertSetting masks any value whose key ends with _encrypted - H-07 archiveClient cascade fires per-interest audit rows - H-08 createSalesTransporter applies SMTP_TIMEOUTS - H-09 AppShell stable children — viewport flip across breakpoint no longer destroys in-progress form drafts - H-10 portal documents page swaps Unicode glyph status icons for Lucide CheckCircle2/XCircle/Circle + aria-labels - H-12 list components swap alert(...) for toast.warning(...) - H-13 5 icon-only buttons gain aria-label - H-14 parseBody treats empty bodies as {} - H-15 admin layout renders a 403 panel instead of silent bounce - H-11 not applicable — mobile-search-overlay IS a mobile bottom-sheet MEDIUM (28+): - M-MT01-05 defense-in-depth port_id/parent-id filters on UPDATE/DELETE WHEREs across custom-fields, notes (all 6 entity types x update + delete), client-contacts, yacht ownerClient lookup, webhook reads - M-D01 documents-hub realtime event-name typo (file:created -> uploaded) - M-EM01 portal-auth emails thread through portId - M-EM02 sendEmail accepts cc/bcc params - M-EM04 notification_digest catalog key - M-IN01 portal presigned download URLs use 4h TTL - M-IN02 OpenAI client lazy-instantiated - M-IN04 stale pdfme refs updated to pdf-lib AcroForm - M-IN05 umami.testConnection returns tagged union - M-L01 reservations tenure_type unified with berths - M-L02 report-generators canonicalize stage values - M-AU01 audit log placeholder copy fixed - M-AU04 outcome_set / outcome_cleared distinct audit verbs - M-NEW-2 activity feed entity name+type separator - M-R01 portal allowlist narrowed + portal_session backstop in proxy - M-SC02 companies archived partial index - M-SC04 audit_logs.searchText documented as DB-managed - M-S01 storage_s3_access_key_encrypted admin field - M-U01 audit log empty state uses <EmptyState> - M-U09 invoice delete dialog -> <AlertDialog> - M-U10 toast.success on ClientForm + InterestForm create/edit - M-U11 settings-form-card logo preview alt text - M-U14 mobile topbar title on clients/yachts/interests/berths - M-U15 Invoices in mobile More-sheet LOW (6/8): - L-AU01 severity defaults for security-relevant verbs - L-AU02 +13 missing actions in admin audit filter - L-AU03 +7 missing entity types in admin audit filter - L-AU04 dead listAuditLogs stubbed - L-D02 CLAUDE.md Owner-wins chain tightened Bonus — Document detail polish (#67 partial, 3/6 deliverables): - state-aware action button per signer - watcher Add UI with display-name resolution - cleanSignerName cleanup Prior session work bundled in: - Documenso v2 webhook + envelope-ID normalization + sequential signing - SigningProgress UI redesign (avatars, per-signer state, timestamps) - env->admin settings registry + RegistryDrivenForm + encrypted creds - Embedded-signing card + Test connection + setup help - Dev-mode EMAIL_REDIRECT_TO banner - Pipeline rules admin page - Sales email config card - Audit log details Sheet - EOI tab: Finalising badge, absolute timestamps, sequential indicator - Notes pipeline_stage_at_creation (migration 0069) - Documenso numeric ID dual-key webhook (migration 0068) - Dimensions criterion copy (migration 0067) Tests: 1374/1374 vitest pass. tsc clean. lint clean. See docs/AUDIT-FIX-WAVE-2026-05-18.md for the full progress report and the user-input items still pending. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 13:28:50 +02:00
const [developerUserId, approverUserId, devName, devEmail, apprName, apprEmail, legacyRow] =
await Promise.all([
readSetting<string>(SETTING_KEYS.documensoDeveloperUserId, portId),
readSetting<string>(SETTING_KEYS.documensoApproverUserId, portId),
readSetting<string>(SETTING_KEYS.documensoDeveloperName, portId),
readSetting<string>(SETTING_KEYS.documensoDeveloperEmail, portId),
readSetting<string>(SETTING_KEYS.documensoApproverName, portId),
readSetting<string>(SETTING_KEYS.documensoApproverEmail, portId),
db.query.systemSettings.findFirst({
where: and(eq(systemSettings.key, 'eoi_signers'), eq(systemSettings.portId, portId)),
}),
]);
const legacyValue = legacyRow?.value as Record<string, unknown> | undefined;
const legacyDev =
legacyValue && isSignerEntry(legacyValue.developer) ? legacyValue.developer : null;
const legacyApr =
legacyValue && isSignerEntry(legacyValue.approver) ? legacyValue.approver : null;
const [developerFromUser, approverFromUser] = await Promise.all([
resolveCrmUser(developerUserId ?? null),
resolveCrmUser(approverUserId ?? null),
]);
const developer =
developerFromUser ??
(devName && devEmail ? { name: devName, email: devEmail } : null) ??
legacyDev ??
EMPTY_SIGNERS.developer;
const approver =
approverFromUser ??
(apprName && apprEmail ? { name: apprName, email: apprEmail } : null) ??
legacyApr ??
EMPTY_SIGNERS.approver;
return { developer, approver };
feat(sales): admin-configurable EOI signers + richer timeline events 1. Per-port EOI signer config - New `eoi_signers` system_settings key (JSON: { developer, approver }, each `{ name, email }`). Settings UI exposes it under Admin → Settings. - getPortEoiSigners(portId) reads the setting with a typed validator; falls back to the legacy David Mizrahi / Abbie May defaults if the row is missing or malformed (so older ports keep working until an admin saves a value). - Both EOI generation pathways now read from the helper instead of hardcoded constants: * documenso-template path (generateAndSignViaDocumensoTemplate) * in-app PDF-fill path (generateAndSignViaInApp) 2. Timeline upgrades The interest detail Activity tab now distinguishes the new automation events that arrived with sessions 1+2: - Stage auto-advances (userId='system') get a small "Auto" pill and carry their reason into the description (e.g. "Stage advanced to EOI Signed (auto-advanced — EOI signed via Documenso)"). - outcome_set events show "Marked as Won" / "Marked as Lost — went to another marina" with optional reason; trophy/X icons. - outcome_cleared events show "Reopened to {stage}" with a refresh icon. - Document events humanized: "Document 'X' fully signed" instead of "Document X: completed". - Stage labels run through stageLabel() so the timeline shows the human label, not the enum key. - Timestamps switched to relative-time with full-date tooltip. - "by system" is rendered plainly (no longer the literal user-id). tsc clean. vitest 832/832 pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 00:19:55 +02:00
}
function formatAddress(address: EoiContext['client']['address']): string {
if (!address) return '';
fix(audit): comprehensive 2026-05-15 audit fix wave + Documenso v2 polish Bundles the prior session's 50-task fix sweep (Documenso v2 + EOI/signing- progress redesign + env-to-admin migration + dev-mode banner) with the 2026-05-18 audit fix wave (3 CRITICAL, 14 HIGH, 28 MEDIUM, 6 LOW). CRITICAL (3): - C-01 interest-berths INNER JOIN -> LEFT JOIN so hard-deleted berths no longer silently drop interest links - C-02 /setup added to PUBLIC_PATHS; fresh-deploy bootstrap loop fixed - C-03 generic PATCH /interests/[id] no longer accepts pipelineStage — callers must go through /stage with the override-guard chain HIGH (14/15): - H-01 explicit ON DELETE on previously-implicit NO ACTION FKs across interests/documents/reservations/reminders/invoices (migration 0070) - H-02 login page reads ?redirect= param with same-origin guard - H-03 CRM invite token moves to URL fragment so it never lands in nginx access logs / Referer headers - H-04 Retry-After header on sign-in-by-identifier 429 (RFC 6585 §4) - H-05 toggleAccount writes an audit row - H-06 upsertSetting masks any value whose key ends with _encrypted - H-07 archiveClient cascade fires per-interest audit rows - H-08 createSalesTransporter applies SMTP_TIMEOUTS - H-09 AppShell stable children — viewport flip across breakpoint no longer destroys in-progress form drafts - H-10 portal documents page swaps Unicode glyph status icons for Lucide CheckCircle2/XCircle/Circle + aria-labels - H-12 list components swap alert(...) for toast.warning(...) - H-13 5 icon-only buttons gain aria-label - H-14 parseBody treats empty bodies as {} - H-15 admin layout renders a 403 panel instead of silent bounce - H-11 not applicable — mobile-search-overlay IS a mobile bottom-sheet MEDIUM (28+): - M-MT01-05 defense-in-depth port_id/parent-id filters on UPDATE/DELETE WHEREs across custom-fields, notes (all 6 entity types x update + delete), client-contacts, yacht ownerClient lookup, webhook reads - M-D01 documents-hub realtime event-name typo (file:created -> uploaded) - M-EM01 portal-auth emails thread through portId - M-EM02 sendEmail accepts cc/bcc params - M-EM04 notification_digest catalog key - M-IN01 portal presigned download URLs use 4h TTL - M-IN02 OpenAI client lazy-instantiated - M-IN04 stale pdfme refs updated to pdf-lib AcroForm - M-IN05 umami.testConnection returns tagged union - M-L01 reservations tenure_type unified with berths - M-L02 report-generators canonicalize stage values - M-AU01 audit log placeholder copy fixed - M-AU04 outcome_set / outcome_cleared distinct audit verbs - M-NEW-2 activity feed entity name+type separator - M-R01 portal allowlist narrowed + portal_session backstop in proxy - M-SC02 companies archived partial index - M-SC04 audit_logs.searchText documented as DB-managed - M-S01 storage_s3_access_key_encrypted admin field - M-U01 audit log empty state uses <EmptyState> - M-U09 invoice delete dialog -> <AlertDialog> - M-U10 toast.success on ClientForm + InterestForm create/edit - M-U11 settings-form-card logo preview alt text - M-U14 mobile topbar title on clients/yachts/interests/berths - M-U15 Invoices in mobile More-sheet LOW (6/8): - L-AU01 severity defaults for security-relevant verbs - L-AU02 +13 missing actions in admin audit filter - L-AU03 +7 missing entity types in admin audit filter - L-AU04 dead listAuditLogs stubbed - L-D02 CLAUDE.md Owner-wins chain tightened Bonus — Document detail polish (#67 partial, 3/6 deliverables): - state-aware action button per signer - watcher Add UI with display-name resolution - cleanSignerName cleanup Prior session work bundled in: - Documenso v2 webhook + envelope-ID normalization + sequential signing - SigningProgress UI redesign (avatars, per-signer state, timestamps) - env->admin settings registry + RegistryDrivenForm + encrypted creds - Embedded-signing card + Test connection + setup help - Dev-mode EMAIL_REDIRECT_TO banner - Pipeline rules admin page - Sales email config card - Audit log details Sheet - EOI tab: Finalising badge, absolute timestamps, sequential indicator - Notes pipeline_stage_at_creation (migration 0069) - Documenso numeric ID dual-key webhook (migration 0068) - Dimensions criterion copy (migration 0067) Tests: 1374/1374 vitest pass. tsc clean. lint clean. See docs/AUDIT-FIX-WAVE-2026-05-18.md for the full progress report and the user-input items still pending. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 13:28:50 +02:00
// Shortest comprehensive format so the line fits the EOI's Address field:
// street, city, REGION (ISO-3166-2 suffix), postal, COUNTRY (alpha-2).
return [address.street, address.city, address.subdivision, address.postalCode, address.countryIso]
.filter(Boolean)
.join(', ');
}
function buildMessage(context: EoiContext): string {
const greeting = `Dear ${context.client.fullName},`;
const body = `Thank you for your interest in a berth at ${context.port.name}. Please click the link above to sign your LOI.`;
const onBehalf =
context.owner.type === 'company' && context.company
? `\n\nOn behalf of ${context.company.legalName ?? context.company.name} (representing the yacht's owner).`
: '';
const footer = `\n\nBest Regards,\n${context.port.name} Team`;
return `${greeting}\n\n${body}${onBehalf}${footer}`;
}
export function buildDocumensoPayload(
context: EoiContext,
options: DocumensoPayloadOptions,
fix(audit): comprehensive 2026-05-15 audit fix wave + Documenso v2 polish Bundles the prior session's 50-task fix sweep (Documenso v2 + EOI/signing- progress redesign + env-to-admin migration + dev-mode banner) with the 2026-05-18 audit fix wave (3 CRITICAL, 14 HIGH, 28 MEDIUM, 6 LOW). CRITICAL (3): - C-01 interest-berths INNER JOIN -> LEFT JOIN so hard-deleted berths no longer silently drop interest links - C-02 /setup added to PUBLIC_PATHS; fresh-deploy bootstrap loop fixed - C-03 generic PATCH /interests/[id] no longer accepts pipelineStage — callers must go through /stage with the override-guard chain HIGH (14/15): - H-01 explicit ON DELETE on previously-implicit NO ACTION FKs across interests/documents/reservations/reminders/invoices (migration 0070) - H-02 login page reads ?redirect= param with same-origin guard - H-03 CRM invite token moves to URL fragment so it never lands in nginx access logs / Referer headers - H-04 Retry-After header on sign-in-by-identifier 429 (RFC 6585 §4) - H-05 toggleAccount writes an audit row - H-06 upsertSetting masks any value whose key ends with _encrypted - H-07 archiveClient cascade fires per-interest audit rows - H-08 createSalesTransporter applies SMTP_TIMEOUTS - H-09 AppShell stable children — viewport flip across breakpoint no longer destroys in-progress form drafts - H-10 portal documents page swaps Unicode glyph status icons for Lucide CheckCircle2/XCircle/Circle + aria-labels - H-12 list components swap alert(...) for toast.warning(...) - H-13 5 icon-only buttons gain aria-label - H-14 parseBody treats empty bodies as {} - H-15 admin layout renders a 403 panel instead of silent bounce - H-11 not applicable — mobile-search-overlay IS a mobile bottom-sheet MEDIUM (28+): - M-MT01-05 defense-in-depth port_id/parent-id filters on UPDATE/DELETE WHEREs across custom-fields, notes (all 6 entity types x update + delete), client-contacts, yacht ownerClient lookup, webhook reads - M-D01 documents-hub realtime event-name typo (file:created -> uploaded) - M-EM01 portal-auth emails thread through portId - M-EM02 sendEmail accepts cc/bcc params - M-EM04 notification_digest catalog key - M-IN01 portal presigned download URLs use 4h TTL - M-IN02 OpenAI client lazy-instantiated - M-IN04 stale pdfme refs updated to pdf-lib AcroForm - M-IN05 umami.testConnection returns tagged union - M-L01 reservations tenure_type unified with berths - M-L02 report-generators canonicalize stage values - M-AU01 audit log placeholder copy fixed - M-AU04 outcome_set / outcome_cleared distinct audit verbs - M-NEW-2 activity feed entity name+type separator - M-R01 portal allowlist narrowed + portal_session backstop in proxy - M-SC02 companies archived partial index - M-SC04 audit_logs.searchText documented as DB-managed - M-S01 storage_s3_access_key_encrypted admin field - M-U01 audit log empty state uses <EmptyState> - M-U09 invoice delete dialog -> <AlertDialog> - M-U10 toast.success on ClientForm + InterestForm create/edit - M-U11 settings-form-card logo preview alt text - M-U14 mobile topbar title on clients/yachts/interests/berths - M-U15 Invoices in mobile More-sheet LOW (6/8): - L-AU01 severity defaults for security-relevant verbs - L-AU02 +13 missing actions in admin audit filter - L-AU03 +7 missing entity types in admin audit filter - L-AU04 dead listAuditLogs stubbed - L-D02 CLAUDE.md Owner-wins chain tightened Bonus — Document detail polish (#67 partial, 3/6 deliverables): - state-aware action button per signer - watcher Add UI with display-name resolution - cleanSignerName cleanup Prior session work bundled in: - Documenso v2 webhook + envelope-ID normalization + sequential signing - SigningProgress UI redesign (avatars, per-signer state, timestamps) - env->admin settings registry + RegistryDrivenForm + encrypted creds - Embedded-signing card + Test connection + setup help - Dev-mode EMAIL_REDIRECT_TO banner - Pipeline rules admin page - Sales email config card - Audit log details Sheet - EOI tab: Finalising badge, absolute timestamps, sequential indicator - Notes pipeline_stage_at_creation (migration 0069) - Documenso numeric ID dual-key webhook (migration 0068) - Dimensions criterion copy (migration 0067) Tests: 1374/1374 vitest pass. tsc clean. lint clean. See docs/AUDIT-FIX-WAVE-2026-05-18.md for the full progress report and the user-input items still pending. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 13:28:50 +02:00
/**
* Cached field name ID map from the per-port `documenso_eoi_field_map`
* setting (populated by the admin "Sync from Documenso" button). When
* provided, the payload also emits `prefillFields` keyed by ID required
* by v2's /template/use. v1 instances ignore this field; v2 instances
* accept either prefillFields OR the legacy formValues shape.
*/
fieldMap?: Record<string, number> | null,
): DocumensoTemplatePayload {
fix(audit): comprehensive 2026-05-15 audit fix wave + Documenso v2 polish Bundles the prior session's 50-task fix sweep (Documenso v2 + EOI/signing- progress redesign + env-to-admin migration + dev-mode banner) with the 2026-05-18 audit fix wave (3 CRITICAL, 14 HIGH, 28 MEDIUM, 6 LOW). CRITICAL (3): - C-01 interest-berths INNER JOIN -> LEFT JOIN so hard-deleted berths no longer silently drop interest links - C-02 /setup added to PUBLIC_PATHS; fresh-deploy bootstrap loop fixed - C-03 generic PATCH /interests/[id] no longer accepts pipelineStage — callers must go through /stage with the override-guard chain HIGH (14/15): - H-01 explicit ON DELETE on previously-implicit NO ACTION FKs across interests/documents/reservations/reminders/invoices (migration 0070) - H-02 login page reads ?redirect= param with same-origin guard - H-03 CRM invite token moves to URL fragment so it never lands in nginx access logs / Referer headers - H-04 Retry-After header on sign-in-by-identifier 429 (RFC 6585 §4) - H-05 toggleAccount writes an audit row - H-06 upsertSetting masks any value whose key ends with _encrypted - H-07 archiveClient cascade fires per-interest audit rows - H-08 createSalesTransporter applies SMTP_TIMEOUTS - H-09 AppShell stable children — viewport flip across breakpoint no longer destroys in-progress form drafts - H-10 portal documents page swaps Unicode glyph status icons for Lucide CheckCircle2/XCircle/Circle + aria-labels - H-12 list components swap alert(...) for toast.warning(...) - H-13 5 icon-only buttons gain aria-label - H-14 parseBody treats empty bodies as {} - H-15 admin layout renders a 403 panel instead of silent bounce - H-11 not applicable — mobile-search-overlay IS a mobile bottom-sheet MEDIUM (28+): - M-MT01-05 defense-in-depth port_id/parent-id filters on UPDATE/DELETE WHEREs across custom-fields, notes (all 6 entity types x update + delete), client-contacts, yacht ownerClient lookup, webhook reads - M-D01 documents-hub realtime event-name typo (file:created -> uploaded) - M-EM01 portal-auth emails thread through portId - M-EM02 sendEmail accepts cc/bcc params - M-EM04 notification_digest catalog key - M-IN01 portal presigned download URLs use 4h TTL - M-IN02 OpenAI client lazy-instantiated - M-IN04 stale pdfme refs updated to pdf-lib AcroForm - M-IN05 umami.testConnection returns tagged union - M-L01 reservations tenure_type unified with berths - M-L02 report-generators canonicalize stage values - M-AU01 audit log placeholder copy fixed - M-AU04 outcome_set / outcome_cleared distinct audit verbs - M-NEW-2 activity feed entity name+type separator - M-R01 portal allowlist narrowed + portal_session backstop in proxy - M-SC02 companies archived partial index - M-SC04 audit_logs.searchText documented as DB-managed - M-S01 storage_s3_access_key_encrypted admin field - M-U01 audit log empty state uses <EmptyState> - M-U09 invoice delete dialog -> <AlertDialog> - M-U10 toast.success on ClientForm + InterestForm create/edit - M-U11 settings-form-card logo preview alt text - M-U14 mobile topbar title on clients/yachts/interests/berths - M-U15 Invoices in mobile More-sheet LOW (6/8): - L-AU01 severity defaults for security-relevant verbs - L-AU02 +13 missing actions in admin audit filter - L-AU03 +7 missing entity types in admin audit filter - L-AU04 dead listAuditLogs stubbed - L-D02 CLAUDE.md Owner-wins chain tightened Bonus — Document detail polish (#67 partial, 3/6 deliverables): - state-aware action button per signer - watcher Add UI with display-name resolution - cleanSignerName cleanup Prior session work bundled in: - Documenso v2 webhook + envelope-ID normalization + sequential signing - SigningProgress UI redesign (avatars, per-signer state, timestamps) - env->admin settings registry + RegistryDrivenForm + encrypted creds - Embedded-signing card + Test connection + setup help - Dev-mode EMAIL_REDIRECT_TO banner - Pipeline rules admin page - Sales email config card - Audit log details Sheet - EOI tab: Finalising badge, absolute timestamps, sequential indicator - Notes pipeline_stage_at_creation (migration 0069) - Documenso numeric ID dual-key webhook (migration 0068) - Dimensions criterion copy (migration 0067) Tests: 1374/1374 vitest pass. tsc clean. lint clean. See docs/AUDIT-FIX-WAVE-2026-05-18.md for the full progress report and the user-input items still pending. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 13:28:50 +02:00
// Honour the rep's unit choice from the EOI drawer's toggle. Defaults to
// 'ft' for legacy call sites that don't pass `dimensionUnit`; new code
// paths (generateAndSign + the drawer) always set it explicitly.
// Append the unit suffix to every dimension value so the rendered EOI
// reads "45 ft" / "13.7 m" rather than the bare number — the original
// form field doesn't tell signers which unit they're looking at.
const dimUnit: 'ft' | 'm' = options.dimensionUnit ?? 'ft';
const yachtLength = dimUnit === 'ft' ? context.yacht?.lengthFt : context.yacht?.lengthM;
const yachtWidth = dimUnit === 'ft' ? context.yacht?.widthFt : context.yacht?.widthM;
const yachtDraft = dimUnit === 'ft' ? context.yacht?.draftFt : context.yacht?.draftM;
const withUnit = (v: string | null | undefined): string =>
v && String(v).trim() ? `${String(v).trim()} ${dimUnit}` : '';
const formValues = {
Name: context.client.fullName,
Email: context.client.primaryEmail ?? '',
Address: formatAddress(context.client.address),
// Yacht + berth are optional EOI fields; when not linked, render as
// empty strings so the corresponding template inputs stay blank.
'Yacht Name': context.yacht?.name ?? '',
Length: withUnit(yachtLength),
Width: withUnit(yachtWidth),
Draft: withUnit(yachtDraft),
// formatBerthRange(['A1']) === 'A1' — so single-berth EOIs render
// identically to the legacy primary-only flow; multi-berth EOIs
// now actually show the full range instead of just the primary
// mooring.
'Berth Number': context.eoiBerthRange || (context.berth?.mooringNumber ?? ''),
Lease_10: false,
Purchase: true,
} as const;
// v2's prefillFields-by-ID emission. Map every formValue entry through the
// cached field map; skip entries that aren't in the map (template doesn't
// have that field, which is fine — Documenso silently drops unknown ones
// in v1 too).
const prefillFields = fieldMap
? Object.entries(formValues)
.map(([label, value]) => {
const fieldId = fieldMap[label];
if (fieldId == null) return null;
const isBoolean = typeof value === 'boolean';
return {
id: fieldId,
type: isBoolean ? ('checkbox' as const) : ('text' as const),
value: String(value),
};
})
.filter((x): x is { id: number; type: 'text' | 'checkbox'; value: string } => x !== null)
: undefined;
// Title format: "<full name>-EOI-NDA[-<berth range>]". When the EOI is
// tied to one or more berths, append the formatted range so the doc
// identifies the deal at a glance in lists and Documenso dashboards.
const berthSuffix = context.eoiBerthRange || context.berth?.mooringNumber || '';
return {
fix(audit): comprehensive 2026-05-15 audit fix wave + Documenso v2 polish Bundles the prior session's 50-task fix sweep (Documenso v2 + EOI/signing- progress redesign + env-to-admin migration + dev-mode banner) with the 2026-05-18 audit fix wave (3 CRITICAL, 14 HIGH, 28 MEDIUM, 6 LOW). CRITICAL (3): - C-01 interest-berths INNER JOIN -> LEFT JOIN so hard-deleted berths no longer silently drop interest links - C-02 /setup added to PUBLIC_PATHS; fresh-deploy bootstrap loop fixed - C-03 generic PATCH /interests/[id] no longer accepts pipelineStage — callers must go through /stage with the override-guard chain HIGH (14/15): - H-01 explicit ON DELETE on previously-implicit NO ACTION FKs across interests/documents/reservations/reminders/invoices (migration 0070) - H-02 login page reads ?redirect= param with same-origin guard - H-03 CRM invite token moves to URL fragment so it never lands in nginx access logs / Referer headers - H-04 Retry-After header on sign-in-by-identifier 429 (RFC 6585 §4) - H-05 toggleAccount writes an audit row - H-06 upsertSetting masks any value whose key ends with _encrypted - H-07 archiveClient cascade fires per-interest audit rows - H-08 createSalesTransporter applies SMTP_TIMEOUTS - H-09 AppShell stable children — viewport flip across breakpoint no longer destroys in-progress form drafts - H-10 portal documents page swaps Unicode glyph status icons for Lucide CheckCircle2/XCircle/Circle + aria-labels - H-12 list components swap alert(...) for toast.warning(...) - H-13 5 icon-only buttons gain aria-label - H-14 parseBody treats empty bodies as {} - H-15 admin layout renders a 403 panel instead of silent bounce - H-11 not applicable — mobile-search-overlay IS a mobile bottom-sheet MEDIUM (28+): - M-MT01-05 defense-in-depth port_id/parent-id filters on UPDATE/DELETE WHEREs across custom-fields, notes (all 6 entity types x update + delete), client-contacts, yacht ownerClient lookup, webhook reads - M-D01 documents-hub realtime event-name typo (file:created -> uploaded) - M-EM01 portal-auth emails thread through portId - M-EM02 sendEmail accepts cc/bcc params - M-EM04 notification_digest catalog key - M-IN01 portal presigned download URLs use 4h TTL - M-IN02 OpenAI client lazy-instantiated - M-IN04 stale pdfme refs updated to pdf-lib AcroForm - M-IN05 umami.testConnection returns tagged union - M-L01 reservations tenure_type unified with berths - M-L02 report-generators canonicalize stage values - M-AU01 audit log placeholder copy fixed - M-AU04 outcome_set / outcome_cleared distinct audit verbs - M-NEW-2 activity feed entity name+type separator - M-R01 portal allowlist narrowed + portal_session backstop in proxy - M-SC02 companies archived partial index - M-SC04 audit_logs.searchText documented as DB-managed - M-S01 storage_s3_access_key_encrypted admin field - M-U01 audit log empty state uses <EmptyState> - M-U09 invoice delete dialog -> <AlertDialog> - M-U10 toast.success on ClientForm + InterestForm create/edit - M-U11 settings-form-card logo preview alt text - M-U14 mobile topbar title on clients/yachts/interests/berths - M-U15 Invoices in mobile More-sheet LOW (6/8): - L-AU01 severity defaults for security-relevant verbs - L-AU02 +13 missing actions in admin audit filter - L-AU03 +7 missing entity types in admin audit filter - L-AU04 dead listAuditLogs stubbed - L-D02 CLAUDE.md Owner-wins chain tightened Bonus — Document detail polish (#67 partial, 3/6 deliverables): - state-aware action button per signer - watcher Add UI with display-name resolution - cleanSignerName cleanup Prior session work bundled in: - Documenso v2 webhook + envelope-ID normalization + sequential signing - SigningProgress UI redesign (avatars, per-signer state, timestamps) - env->admin settings registry + RegistryDrivenForm + encrypted creds - Embedded-signing card + Test connection + setup help - Dev-mode EMAIL_REDIRECT_TO banner - Pipeline rules admin page - Sales email config card - Audit log details Sheet - EOI tab: Finalising badge, absolute timestamps, sequential indicator - Notes pipeline_stage_at_creation (migration 0069) - Documenso numeric ID dual-key webhook (migration 0068) - Dimensions criterion copy (migration 0067) Tests: 1374/1374 vitest pass. tsc clean. lint clean. See docs/AUDIT-FIX-WAVE-2026-05-18.md for the full progress report and the user-input items still pending. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 13:28:50 +02:00
title: berthSuffix
? `${context.client.fullName}-EOI-NDA-${berthSuffix}`
: `${context.client.fullName}-EOI-NDA`,
externalId: `loi-${options.interestId}`,
meta: {
message: buildMessage(context),
subject: 'Your LOI is ready to be signed',
redirectUrl: options.redirectUrl ?? DEFAULT_REDIRECT_URL,
distributionMethod: 'NONE',
feat(documenso): full v2 endpoint coverage + sequential signing + redirectUrl Wire up the remaining version-aware paths so a port pointed at Documenso 2.x takes the v2 endpoint on every CRUD operation, with two new v2-only settings exposed in admin UI. documenso-client.ts: - createDocument: v2 multipart /envelope/create + getDocument follow-up to return the full doc shape (v1 path unchanged) - sendDocument: v2 /envelope/distribute (returns per-recipient signingUrl in the same response — eliminates the v1 separate-GET round-trip) - sendReminder: v2 /envelope/redistribute with recipientIds filter - downloadSignedPdf: v2 /envelope/{id}/download - CreateDocumentMeta type: { subject, message, redirectUrl, signingOrder } threaded through v1 + v2 paths (v1 ignores signingOrder) port-config.ts: - New settings: documenso_signing_order (PARALLEL/SEQUENTIAL, v2-only), documenso_redirect_url (both versions honour) - PortDocumensoConfig gains signingOrder + redirectUrl documenso-payload.ts: - DocumensoTemplatePayload.meta gains signingOrder - buildDocumensoPayload reads from options.signingOrder, omits when null document-templates.ts (EOI template flow): - Pass docCfg.signingOrder + docCfg.redirectUrl into buildDocumensoPayload documents.service.ts (sendForSigning uploaded-doc flow): - Pass portId to documensoCreate + documensoSend (was missing) - Thread signingOrder + redirectUrl via the new meta param Admin Documenso settings page: - v2 benefits card updated: now lists envelope CRUD, one-call send, sequential enforcement, post-sign redirect as wired (was roadmap) - Roadmap callout pruned to the three remaining deferred items: template/use migration, /envelope/update, non-SIGNER recipient roles - New "v2 signing behaviour" SettingsFormCard with the two new settings Template flow stays on /api/v1/templates/{id}/generate-document by design — Documenso 2.x accepts v1 endpoints via backward compat; full migration to v2 /template/use requires per-template field-ID capture (admin schema work, deferred). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 14:38:45 +02:00
...(options.signingOrder ? { signingOrder: options.signingOrder } : {}),
},
fix(audit): comprehensive 2026-05-15 audit fix wave + Documenso v2 polish Bundles the prior session's 50-task fix sweep (Documenso v2 + EOI/signing- progress redesign + env-to-admin migration + dev-mode banner) with the 2026-05-18 audit fix wave (3 CRITICAL, 14 HIGH, 28 MEDIUM, 6 LOW). CRITICAL (3): - C-01 interest-berths INNER JOIN -> LEFT JOIN so hard-deleted berths no longer silently drop interest links - C-02 /setup added to PUBLIC_PATHS; fresh-deploy bootstrap loop fixed - C-03 generic PATCH /interests/[id] no longer accepts pipelineStage — callers must go through /stage with the override-guard chain HIGH (14/15): - H-01 explicit ON DELETE on previously-implicit NO ACTION FKs across interests/documents/reservations/reminders/invoices (migration 0070) - H-02 login page reads ?redirect= param with same-origin guard - H-03 CRM invite token moves to URL fragment so it never lands in nginx access logs / Referer headers - H-04 Retry-After header on sign-in-by-identifier 429 (RFC 6585 §4) - H-05 toggleAccount writes an audit row - H-06 upsertSetting masks any value whose key ends with _encrypted - H-07 archiveClient cascade fires per-interest audit rows - H-08 createSalesTransporter applies SMTP_TIMEOUTS - H-09 AppShell stable children — viewport flip across breakpoint no longer destroys in-progress form drafts - H-10 portal documents page swaps Unicode glyph status icons for Lucide CheckCircle2/XCircle/Circle + aria-labels - H-12 list components swap alert(...) for toast.warning(...) - H-13 5 icon-only buttons gain aria-label - H-14 parseBody treats empty bodies as {} - H-15 admin layout renders a 403 panel instead of silent bounce - H-11 not applicable — mobile-search-overlay IS a mobile bottom-sheet MEDIUM (28+): - M-MT01-05 defense-in-depth port_id/parent-id filters on UPDATE/DELETE WHEREs across custom-fields, notes (all 6 entity types x update + delete), client-contacts, yacht ownerClient lookup, webhook reads - M-D01 documents-hub realtime event-name typo (file:created -> uploaded) - M-EM01 portal-auth emails thread through portId - M-EM02 sendEmail accepts cc/bcc params - M-EM04 notification_digest catalog key - M-IN01 portal presigned download URLs use 4h TTL - M-IN02 OpenAI client lazy-instantiated - M-IN04 stale pdfme refs updated to pdf-lib AcroForm - M-IN05 umami.testConnection returns tagged union - M-L01 reservations tenure_type unified with berths - M-L02 report-generators canonicalize stage values - M-AU01 audit log placeholder copy fixed - M-AU04 outcome_set / outcome_cleared distinct audit verbs - M-NEW-2 activity feed entity name+type separator - M-R01 portal allowlist narrowed + portal_session backstop in proxy - M-SC02 companies archived partial index - M-SC04 audit_logs.searchText documented as DB-managed - M-S01 storage_s3_access_key_encrypted admin field - M-U01 audit log empty state uses <EmptyState> - M-U09 invoice delete dialog -> <AlertDialog> - M-U10 toast.success on ClientForm + InterestForm create/edit - M-U11 settings-form-card logo preview alt text - M-U14 mobile topbar title on clients/yachts/interests/berths - M-U15 Invoices in mobile More-sheet LOW (6/8): - L-AU01 severity defaults for security-relevant verbs - L-AU02 +13 missing actions in admin audit filter - L-AU03 +7 missing entity types in admin audit filter - L-AU04 dead listAuditLogs stubbed - L-D02 CLAUDE.md Owner-wins chain tightened Bonus — Document detail polish (#67 partial, 3/6 deliverables): - state-aware action button per signer - watcher Add UI with display-name resolution - cleanSignerName cleanup Prior session work bundled in: - Documenso v2 webhook + envelope-ID normalization + sequential signing - SigningProgress UI redesign (avatars, per-signer state, timestamps) - env->admin settings registry + RegistryDrivenForm + encrypted creds - Embedded-signing card + Test connection + setup help - Dev-mode EMAIL_REDIRECT_TO banner - Pipeline rules admin page - Sales email config card - Audit log details Sheet - EOI tab: Finalising badge, absolute timestamps, sequential indicator - Notes pipeline_stage_at_creation (migration 0069) - Documenso numeric ID dual-key webhook (migration 0068) - Dimensions criterion copy (migration 0067) Tests: 1374/1374 vitest pass. tsc clean. lint clean. See docs/AUDIT-FIX-WAVE-2026-05-18.md for the full progress report and the user-input items still pending. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 13:28:50 +02:00
formValues,
...(prefillFields && prefillFields.length > 0 ? { prefillFields } : {}),
// Per Documenso v2's /template/use schema, `email` and `name` accept "" as
// a sentinel meaning "use the value baked into the template recipient".
// So when an admin leaves the developer/approver name/email blank in our
// admin settings, we pass "" rather than a hardcoded fallback — Documenso
// then takes the email/name set on the template itself. A non-empty
// admin value still wins (overrides the template at send time).
recipients: [
{
id: options.clientRecipientId,
name: context.client.fullName,
email: context.client.primaryEmail ?? '',
role: 'SIGNER',
signingOrder: 1,
},
{
id: options.developerRecipientId,
fix(audit): comprehensive 2026-05-15 audit fix wave + Documenso v2 polish Bundles the prior session's 50-task fix sweep (Documenso v2 + EOI/signing- progress redesign + env-to-admin migration + dev-mode banner) with the 2026-05-18 audit fix wave (3 CRITICAL, 14 HIGH, 28 MEDIUM, 6 LOW). CRITICAL (3): - C-01 interest-berths INNER JOIN -> LEFT JOIN so hard-deleted berths no longer silently drop interest links - C-02 /setup added to PUBLIC_PATHS; fresh-deploy bootstrap loop fixed - C-03 generic PATCH /interests/[id] no longer accepts pipelineStage — callers must go through /stage with the override-guard chain HIGH (14/15): - H-01 explicit ON DELETE on previously-implicit NO ACTION FKs across interests/documents/reservations/reminders/invoices (migration 0070) - H-02 login page reads ?redirect= param with same-origin guard - H-03 CRM invite token moves to URL fragment so it never lands in nginx access logs / Referer headers - H-04 Retry-After header on sign-in-by-identifier 429 (RFC 6585 §4) - H-05 toggleAccount writes an audit row - H-06 upsertSetting masks any value whose key ends with _encrypted - H-07 archiveClient cascade fires per-interest audit rows - H-08 createSalesTransporter applies SMTP_TIMEOUTS - H-09 AppShell stable children — viewport flip across breakpoint no longer destroys in-progress form drafts - H-10 portal documents page swaps Unicode glyph status icons for Lucide CheckCircle2/XCircle/Circle + aria-labels - H-12 list components swap alert(...) for toast.warning(...) - H-13 5 icon-only buttons gain aria-label - H-14 parseBody treats empty bodies as {} - H-15 admin layout renders a 403 panel instead of silent bounce - H-11 not applicable — mobile-search-overlay IS a mobile bottom-sheet MEDIUM (28+): - M-MT01-05 defense-in-depth port_id/parent-id filters on UPDATE/DELETE WHEREs across custom-fields, notes (all 6 entity types x update + delete), client-contacts, yacht ownerClient lookup, webhook reads - M-D01 documents-hub realtime event-name typo (file:created -> uploaded) - M-EM01 portal-auth emails thread through portId - M-EM02 sendEmail accepts cc/bcc params - M-EM04 notification_digest catalog key - M-IN01 portal presigned download URLs use 4h TTL - M-IN02 OpenAI client lazy-instantiated - M-IN04 stale pdfme refs updated to pdf-lib AcroForm - M-IN05 umami.testConnection returns tagged union - M-L01 reservations tenure_type unified with berths - M-L02 report-generators canonicalize stage values - M-AU01 audit log placeholder copy fixed - M-AU04 outcome_set / outcome_cleared distinct audit verbs - M-NEW-2 activity feed entity name+type separator - M-R01 portal allowlist narrowed + portal_session backstop in proxy - M-SC02 companies archived partial index - M-SC04 audit_logs.searchText documented as DB-managed - M-S01 storage_s3_access_key_encrypted admin field - M-U01 audit log empty state uses <EmptyState> - M-U09 invoice delete dialog -> <AlertDialog> - M-U10 toast.success on ClientForm + InterestForm create/edit - M-U11 settings-form-card logo preview alt text - M-U14 mobile topbar title on clients/yachts/interests/berths - M-U15 Invoices in mobile More-sheet LOW (6/8): - L-AU01 severity defaults for security-relevant verbs - L-AU02 +13 missing actions in admin audit filter - L-AU03 +7 missing entity types in admin audit filter - L-AU04 dead listAuditLogs stubbed - L-D02 CLAUDE.md Owner-wins chain tightened Bonus — Document detail polish (#67 partial, 3/6 deliverables): - state-aware action button per signer - watcher Add UI with display-name resolution - cleanSignerName cleanup Prior session work bundled in: - Documenso v2 webhook + envelope-ID normalization + sequential signing - SigningProgress UI redesign (avatars, per-signer state, timestamps) - env->admin settings registry + RegistryDrivenForm + encrypted creds - Embedded-signing card + Test connection + setup help - Dev-mode EMAIL_REDIRECT_TO banner - Pipeline rules admin page - Sales email config card - Audit log details Sheet - EOI tab: Finalising badge, absolute timestamps, sequential indicator - Notes pipeline_stage_at_creation (migration 0069) - Documenso numeric ID dual-key webhook (migration 0068) - Dimensions criterion copy (migration 0067) Tests: 1374/1374 vitest pass. tsc clean. lint clean. See docs/AUDIT-FIX-WAVE-2026-05-18.md for the full progress report and the user-input items still pending. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 13:28:50 +02:00
name: options.developerName ?? '',
email: options.developerEmail ?? '',
role: 'SIGNER',
signingOrder: 2,
},
{
id: options.approvalRecipientId,
fix(audit): comprehensive 2026-05-15 audit fix wave + Documenso v2 polish Bundles the prior session's 50-task fix sweep (Documenso v2 + EOI/signing- progress redesign + env-to-admin migration + dev-mode banner) with the 2026-05-18 audit fix wave (3 CRITICAL, 14 HIGH, 28 MEDIUM, 6 LOW). CRITICAL (3): - C-01 interest-berths INNER JOIN -> LEFT JOIN so hard-deleted berths no longer silently drop interest links - C-02 /setup added to PUBLIC_PATHS; fresh-deploy bootstrap loop fixed - C-03 generic PATCH /interests/[id] no longer accepts pipelineStage — callers must go through /stage with the override-guard chain HIGH (14/15): - H-01 explicit ON DELETE on previously-implicit NO ACTION FKs across interests/documents/reservations/reminders/invoices (migration 0070) - H-02 login page reads ?redirect= param with same-origin guard - H-03 CRM invite token moves to URL fragment so it never lands in nginx access logs / Referer headers - H-04 Retry-After header on sign-in-by-identifier 429 (RFC 6585 §4) - H-05 toggleAccount writes an audit row - H-06 upsertSetting masks any value whose key ends with _encrypted - H-07 archiveClient cascade fires per-interest audit rows - H-08 createSalesTransporter applies SMTP_TIMEOUTS - H-09 AppShell stable children — viewport flip across breakpoint no longer destroys in-progress form drafts - H-10 portal documents page swaps Unicode glyph status icons for Lucide CheckCircle2/XCircle/Circle + aria-labels - H-12 list components swap alert(...) for toast.warning(...) - H-13 5 icon-only buttons gain aria-label - H-14 parseBody treats empty bodies as {} - H-15 admin layout renders a 403 panel instead of silent bounce - H-11 not applicable — mobile-search-overlay IS a mobile bottom-sheet MEDIUM (28+): - M-MT01-05 defense-in-depth port_id/parent-id filters on UPDATE/DELETE WHEREs across custom-fields, notes (all 6 entity types x update + delete), client-contacts, yacht ownerClient lookup, webhook reads - M-D01 documents-hub realtime event-name typo (file:created -> uploaded) - M-EM01 portal-auth emails thread through portId - M-EM02 sendEmail accepts cc/bcc params - M-EM04 notification_digest catalog key - M-IN01 portal presigned download URLs use 4h TTL - M-IN02 OpenAI client lazy-instantiated - M-IN04 stale pdfme refs updated to pdf-lib AcroForm - M-IN05 umami.testConnection returns tagged union - M-L01 reservations tenure_type unified with berths - M-L02 report-generators canonicalize stage values - M-AU01 audit log placeholder copy fixed - M-AU04 outcome_set / outcome_cleared distinct audit verbs - M-NEW-2 activity feed entity name+type separator - M-R01 portal allowlist narrowed + portal_session backstop in proxy - M-SC02 companies archived partial index - M-SC04 audit_logs.searchText documented as DB-managed - M-S01 storage_s3_access_key_encrypted admin field - M-U01 audit log empty state uses <EmptyState> - M-U09 invoice delete dialog -> <AlertDialog> - M-U10 toast.success on ClientForm + InterestForm create/edit - M-U11 settings-form-card logo preview alt text - M-U14 mobile topbar title on clients/yachts/interests/berths - M-U15 Invoices in mobile More-sheet LOW (6/8): - L-AU01 severity defaults for security-relevant verbs - L-AU02 +13 missing actions in admin audit filter - L-AU03 +7 missing entity types in admin audit filter - L-AU04 dead listAuditLogs stubbed - L-D02 CLAUDE.md Owner-wins chain tightened Bonus — Document detail polish (#67 partial, 3/6 deliverables): - state-aware action button per signer - watcher Add UI with display-name resolution - cleanSignerName cleanup Prior session work bundled in: - Documenso v2 webhook + envelope-ID normalization + sequential signing - SigningProgress UI redesign (avatars, per-signer state, timestamps) - env->admin settings registry + RegistryDrivenForm + encrypted creds - Embedded-signing card + Test connection + setup help - Dev-mode EMAIL_REDIRECT_TO banner - Pipeline rules admin page - Sales email config card - Audit log details Sheet - EOI tab: Finalising badge, absolute timestamps, sequential indicator - Notes pipeline_stage_at_creation (migration 0069) - Documenso numeric ID dual-key webhook (migration 0068) - Dimensions criterion copy (migration 0067) Tests: 1374/1374 vitest pass. tsc clean. lint clean. See docs/AUDIT-FIX-WAVE-2026-05-18.md for the full progress report and the user-input items still pending. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 13:28:50 +02:00
name: options.approverName ?? '',
email: options.approverEmail ?? '',
role: 'APPROVER',
signingOrder: 3,
},
fix(audit): comprehensive 2026-05-15 audit fix wave + Documenso v2 polish Bundles the prior session's 50-task fix sweep (Documenso v2 + EOI/signing- progress redesign + env-to-admin migration + dev-mode banner) with the 2026-05-18 audit fix wave (3 CRITICAL, 14 HIGH, 28 MEDIUM, 6 LOW). CRITICAL (3): - C-01 interest-berths INNER JOIN -> LEFT JOIN so hard-deleted berths no longer silently drop interest links - C-02 /setup added to PUBLIC_PATHS; fresh-deploy bootstrap loop fixed - C-03 generic PATCH /interests/[id] no longer accepts pipelineStage — callers must go through /stage with the override-guard chain HIGH (14/15): - H-01 explicit ON DELETE on previously-implicit NO ACTION FKs across interests/documents/reservations/reminders/invoices (migration 0070) - H-02 login page reads ?redirect= param with same-origin guard - H-03 CRM invite token moves to URL fragment so it never lands in nginx access logs / Referer headers - H-04 Retry-After header on sign-in-by-identifier 429 (RFC 6585 §4) - H-05 toggleAccount writes an audit row - H-06 upsertSetting masks any value whose key ends with _encrypted - H-07 archiveClient cascade fires per-interest audit rows - H-08 createSalesTransporter applies SMTP_TIMEOUTS - H-09 AppShell stable children — viewport flip across breakpoint no longer destroys in-progress form drafts - H-10 portal documents page swaps Unicode glyph status icons for Lucide CheckCircle2/XCircle/Circle + aria-labels - H-12 list components swap alert(...) for toast.warning(...) - H-13 5 icon-only buttons gain aria-label - H-14 parseBody treats empty bodies as {} - H-15 admin layout renders a 403 panel instead of silent bounce - H-11 not applicable — mobile-search-overlay IS a mobile bottom-sheet MEDIUM (28+): - M-MT01-05 defense-in-depth port_id/parent-id filters on UPDATE/DELETE WHEREs across custom-fields, notes (all 6 entity types x update + delete), client-contacts, yacht ownerClient lookup, webhook reads - M-D01 documents-hub realtime event-name typo (file:created -> uploaded) - M-EM01 portal-auth emails thread through portId - M-EM02 sendEmail accepts cc/bcc params - M-EM04 notification_digest catalog key - M-IN01 portal presigned download URLs use 4h TTL - M-IN02 OpenAI client lazy-instantiated - M-IN04 stale pdfme refs updated to pdf-lib AcroForm - M-IN05 umami.testConnection returns tagged union - M-L01 reservations tenure_type unified with berths - M-L02 report-generators canonicalize stage values - M-AU01 audit log placeholder copy fixed - M-AU04 outcome_set / outcome_cleared distinct audit verbs - M-NEW-2 activity feed entity name+type separator - M-R01 portal allowlist narrowed + portal_session backstop in proxy - M-SC02 companies archived partial index - M-SC04 audit_logs.searchText documented as DB-managed - M-S01 storage_s3_access_key_encrypted admin field - M-U01 audit log empty state uses <EmptyState> - M-U09 invoice delete dialog -> <AlertDialog> - M-U10 toast.success on ClientForm + InterestForm create/edit - M-U11 settings-form-card logo preview alt text - M-U14 mobile topbar title on clients/yachts/interests/berths - M-U15 Invoices in mobile More-sheet LOW (6/8): - L-AU01 severity defaults for security-relevant verbs - L-AU02 +13 missing actions in admin audit filter - L-AU03 +7 missing entity types in admin audit filter - L-AU04 dead listAuditLogs stubbed - L-D02 CLAUDE.md Owner-wins chain tightened Bonus — Document detail polish (#67 partial, 3/6 deliverables): - state-aware action button per signer - watcher Add UI with display-name resolution - cleanSignerName cleanup Prior session work bundled in: - Documenso v2 webhook + envelope-ID normalization + sequential signing - SigningProgress UI redesign (avatars, per-signer state, timestamps) - env->admin settings registry + RegistryDrivenForm + encrypted creds - Embedded-signing card + Test connection + setup help - Dev-mode EMAIL_REDIRECT_TO banner - Pipeline rules admin page - Sales email config card - Audit log details Sheet - EOI tab: Finalising badge, absolute timestamps, sequential indicator - Notes pipeline_stage_at_creation (migration 0069) - Documenso numeric ID dual-key webhook (migration 0068) - Dimensions criterion copy (migration 0067) Tests: 1374/1374 vitest pass. tsc clean. lint clean. See docs/AUDIT-FIX-WAVE-2026-05-18.md for the full progress report and the user-input items still pending. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 13:28:50 +02:00
// Append CC / VIEWER slots after the canonical trio so their signing
// order doesn't collide with 1/2/3. Documenso doesn't require
// signingOrder uniqueness across non-signing roles but we still hand
// out monotonic numbers (4, 5, …) for predictability.
...(options.extraRecipients ?? []).map((extra, idx) => ({
id: extra.id,
name: extra.name,
email: extra.email,
role: extra.role,
signingOrder: extra.signingOrder ?? 4 + idx,
})),
],
};
}