1750e265e77c0595cec064fd63c954e1c0142d6c
53 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 15a139e86f |
feat(berths): website auto-promote toggle + manual-override soft-pin priority
- website_berth_autopromote_enabled (default OFF): a website registration for a specific, currently-available berth auto-creates a prospect (client + optional yacht + interest) and links the berth is_specific_interest=true, flipping the public map to Under Offer; general/residence/contact submissions stay capture-only. Marks the submission converted so a rep never double-creates it. - derivePublicStatus now honours a manual pin (soft pin): a manually-set status wins over the interest-derived Under Offer, but a real permanent tenancy or an explicit sold still override it. - berth rules engine respects a manual pin EXCEPT for sale triggers (-> sold), so a confirmed sale still wins but soft auto-changes never stomp a pin. - Reset-to-automatic action (service + API POST /berths/[id]/status/reset + UI) to drop a manual pin; lock badge on every manual override (list + detail); divergence banner prompting reset when a pinned-Available berth has a deal. - migration stage map updated to the §4b signed-off mapping: GQI -> enquiry unless it named a berth/size marker (-> qualified); SQI -> qualified. Tests: +public-berths soft-pin cases, +website-intake-promote helpers, +migration GQI marker rule. 1582 unit/integration green; tsc clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
|||
| 3d9084c94b |
feat(reports): parseOperationalFilters pure parser (Area scope)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
|||
| b690fb8d56 |
feat(reports): Financial report (Initiative 1 Phase 4)
Builds the Financial report on the canonical payments + expenses tables (the CRM records money received; it does not invoice — invoices module is off, dev DB has zero invoice rows). The invoice-centric spec is reframed onto the payments model: "outstanding AR" → expected-deposit shortfall on active deals; "AR aging" → outstanding deposits bucketed by deal age. Service (financial.service.ts): - 7 KPIs: revenue collected (net of refunds), deposits, balance, pipeline expected, outstanding deposits, expenses, net contribution - 6 chart datasets: revenue by month (deposit/balance), collection funnel (EOI→deposit→contract→won), expected-deposit aging, cash flow (inflow vs outflow), expense breakdown by category - 4 tables: outstanding deposits, recent payments, refund log, expense ledger - every money figure normalised to port currency via a shared resolvePortCurrency/normalizeAmount helper (new reports/currency.ts) UI (financial-report-client.tsx): KPI strip + recharts (stacked bar / horizontal bar / line / donut) + month/quarter/year toggle + branded empty states; DateRangePicker + Templates + Export wired. Un-hidden the Financial card on the reports landing. Plumbing: added '1y' (trailing 12mo) preset to the shared range system (financial trends want a year); added 'financial'/'marketing' to the report-template kind enum for template parity. TDD: 6 financial-math unit tests (aging buckets, month keys/range, net contribution). tsc clean; full unit suite green except pre-existing Redis/storage-dependent integration tests. Browser-verified against live data: API 200, KPIs correct ($5,849 expenses / -$5,849 net, $0 revenue correct given 0 payment rows), expense ledger + breakdown populate, payment-derived sections show graceful empty states. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
|||
| b97f6e945c |
feat(reports): rep + source multi-select filters on Sales report
Closes the two cross-cutting filter gaps in launch-readiness (rep multi-select + source multi-select). The Sales detail tables can now be narrowed by assigned rep and lead source alongside the existing stage / lead-category / outcome filters. - service: thread `assignedTo` + `sources` through the 5 filtered Sales queries (rep-performance, stalled, closing-this-month, recent-wins, lost-reason); add `getRepFilterOptions` for the rep dropdown's stable option list (distinct assigned reps port-wide, window-independent). - route: extract param parsing into a pure, unit-tested `parseSalesFilters` helper (source allowlisted against SOURCES; assignedTo passed through as free user-id list); return `repOptions` in the payload. - ui: static Source filter (SOURCES) + dynamic "Assigned to" filter (from payload repOptions, hidden until loaded); decouple the query builder from dynamic options via a stable FILTER_KEYS list. TDD: 8 new parseSalesFilters unit tests (allowlist drop, free-list passthrough, combine). tsc clean; 12/12 reports unit tests; browser- verified both filters fire `source=`/`assignedTo=` → 200. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
|||
| 681b94a8ef |
feat(reports): prior-period comparison toggle on the Sales report
Adds a "Compare to prior period" toggle to the Sales report header. When on, the API recomputes the KPI window for the equal-length window immediately preceding the selected range (previousPeriodBounds) behind `?compare=1`, and the five window-derived KPI tiles (Won, Lost, Win rate, Avg time-to-close, New leads) render colour-correct "vs prior" deltas. Point-in-time tiles (Active interests, Pipeline value) have no prior-window analogue and intentionally show no delta. The prior-window query runs in parallel with the main batch and resolves to null when the toggle is off (zero cost). Toggle state persists in the saved-template config. Closes the spec's "period comparison on every report" gap for Sales; Operational already rendered period-start deltas. Pure helpers TDD'd: previousPeriodBounds (range.ts) + computeSalesKpiComparison (sales-comparison.ts), 7 unit tests. tsc + lint clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
|||
| fe5f98db23 |
feat(automate-signing): one-click invitation kickoff + auto cascade + completion broadcast
Phase 3 of the comprehensive UAT round. Implements the Automate
Signing feature per the 2026-05-26 locked decisions.
P3.1 — documents.automation_mode schema
Migration 0088 adds the column with a CHECK constraint enforcing
the three-value enum: manual / sequential_auto / concurrent_auto.
Drizzle schema picks it up; default 'manual' preserves existing
behaviour.
P3.2 — Automate Signing orchestrator service
New src/lib/services/signing-automation.service.ts. enableSigningAutomation
resolves the mode from the envelope's signing order (SEQUENTIAL ->
sequential_auto fires first signer only; PARALLEL -> concurrent_auto
fires all signers in one parallel dispatch), updates documents.automationMode,
and dispatches invitations via the same sendSigningInvitation path
the manual route uses (so the email a recipient sees is identical
regardless of trigger). ensureSigningUrls recovers v2 signing URLs
if they're missing on the local signer rows. Hard guards: envelope
must exist, status in {draft, sent, partially_signed}, ≥2 signers.
disableSigningAutomation reverts to manual; idempotent.
P3.3 — Webhook cascade
The existing sendCascadingInviteForNextSigner in documents.service.ts
already fires the next pending signer on every recipient_signed event
(mode-independent). handleDocumentCompleted already sends the signed
PDF to all recipients via sendSigningCompleted on completion. So
"automate" really means "kick off the first invitation"; the rest
is mode-independent existing behaviour. Doc comment in the new
service explains the interaction.
P3.4 — ActiveEoiCard Automate signing button + banner
- DocumentRow type extended with automationMode + documensoId.
- New automateMutation hits POST /api/v1/documents/[id]/automate;
pauseAutomationMutation hits DELETE.
- "Automate signing" button visible when totalCount ≥ 2 AND doc has
documensoId AND envelope is in-flight AND mode === 'manual'.
- "Automating sequentially/concurrently · N of M signed" banner
renders when automation is active, with a Pause button that
reverts to manual.
- Per-row Send invitation / Send reminder buttons in SigningProgress
stay visible per the locked decision (manual override during auto).
P3.5 — Automate Signing API route + tests
- POST /api/v1/documents/[id]/automate (enables) + DELETE (disables).
- Permission: documents.send_for_signing (mirrors the manual
send-invitation route).
- vitest covering: NotFound on missing doc, Conflict on missing
envelope, Conflict on completed status, Conflict on already-
automated, Conflict on <2 signers, disable is idempotent when
already manual. All 7 cases pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| b6c27b506d |
feat(documenso-audit-phase-1): persist documensoId early + preflight + state machine + reconciliation + tests
Phase 1 of the comprehensive Documenso upload audit per the 2026-05-26 locked-decisions block in docs/superpowers/audits/active-uat.md. P1.1 — persist documensoId immediately after create Was set only at the late `status: 'sent'` commit. Any throw between documensoCreate and the late update left an orphaned Documenso envelope the CRM had no link to. Now the UPDATE runs right after documensoCreate succeeds; rollback paths can find and void the envelope. P1.2 — pre-flight validation hard-blocks Submit UploadForSigningDialog computes a submissionErrors memo over recipients + fields. Submit button disabled when errors > 0. Inline amber summary lists every issue (missing email, invalid email, missing name, field assigned to non-existent recipient, no fields placed). Service layer mirrors the same email + name checks so direct API hits reject early. No override path per locked decision. P1.3 — cancel/delete affordance audit + sweep Document-list per-row Delete + Send for Signing actions now: - Wrapped in PermissionGate (documents.delete + send_for_signing). - Surface toast on success + toastError on failure (were silently swallowing errors). - Use a broader predicate-based query invalidation so every doc list across the app refreshes, not just the local key. EOI tab Regenerate + Cancel EOI buttons + reservation/contract tab Cancel buttons wrapped in PermissionGate (documents.edit, the cancel route's auth check). P1.4 — Documenso webhook URL auto-PATCH (env-gated) scripts/update-documenso-webhook.ts written. Reads DEV_AUTO_UPDATE_DOCUMENSO_WEBHOOK env flag (when 1, runs; otherwise no-op). Lists every webhook on the Documenso instance via v2 (with v1 fallback), identifies webhooks pointing at trycloudflare.com hosts OR /api/webhooks/documenso paths, PATCHes them to the new tunnel URL. scripts/tunnel-url.sh chains the script after the URL print so a re-tunnel auto-rotates the webhook (when flag set). P1.5 — state-machine refactor with rollbackTo() helper custom-document-upload.service.ts: - Single try around create → send → place steps. - state.step tracks which step is current; state.documensoDocId records the envelope id once we have it. - rollbackTo(reason) composes the recovery: status='cancelled' on the CRM row, documensoVoidSafe on the envelope when applicable. Idempotent — calling twice is safe. - Removes three independent try/catches. P1.6 — recipient ↔ Documenso identity reconciliation After documensoSend, validates every distinct email we sent appears in sentDoc.recipients. If Documenso silently dropped one, a ConflictError fires before field placement so the rollback path triggers. Explicit message names the missing emails for the rep. P1.7 — vitest extension + per-failure audit-log entries - 5 new vitest cases (blank email, whitespace email, malformed email, blank name, duplicate-emails-OK semantic). - rollbackTo writes a structured audit_log entry with failedStep, documensoEnvelopeId, errorClass, errorMessage. Post-mortem investigation has structured data instead of just logger lines. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| dd25ccfb53 |
fix(tenancies-audit): resolve findings from 7-agent system-wide rename audit
MUST-FIX:
- src/app/api/v1/admin/users/[id]/permission-overrides/route.ts:70 — the
PUT allowlist still gated `reservations: {view,create,activate,cancel}`.
Stale: would reject valid `tenancies.{view,manage,cancel}` writes and
silently accept ghost `reservations.*` writes that never land. Replaced.
- src/lib/services/alert-rules.ts:68 — `reservation.no_agreement` alert
emitted `entityType: 'reservation'`. Every other tenancy-related
audit/socket/dashboard label is `'berth_tenancy'`. Inconsistent dedupe
+ activity-feed label miss.
- tests/e2e/exhaustive/08-portal.spec.ts:6 — hardcoded /portal/my-reservations
navigates to a 404 every run.
- tests/e2e/exhaustive/03-reservations.spec.ts — entire spec renamed to
03-tenancies.spec.ts; tab + button locators updated to match renamed UI.
SHOULD-FIX (consistency):
- src/components/clients/client-detail.tsx — useRealtimeInvalidation only
caught 3 of the 4 berth_tenancy:* events; added the `:created` listener.
- src/lib/services/client-merge.service.ts — MergeResult.movedRows.reservations
+ snapshot.reservations + local loserReservations / movedReservations
renamed to tenancies / loserTenancies / movedTenancies. No external
consumers grep-confirmed.
- src/lib/services/gdpr-bundle-builder.ts — GdprBundle.reservations field
renamed to .tenancies; user-facing HTML section "Reservations" → "Tenancies";
local reservationRows → tenancyRows.
- 6 UI copy strings: gdpr-export-button, bulk-archive-wizard,
bulk-hard-delete-dialog, hard-delete-dialog, admin-sections-browser ×2,
admin/import/page, won-status-panel — all "reservations" prose updated
to "tenancies" (occupancy-record sense).
- tests/integration/api/tenancies.test.ts — handler import aliases
`createReservationHandler` etc renamed to `createTenancyHandler` etc.
- tests/unit/services/berth-tenancies.test.ts — local helper makeReservation
→ makeTenancyLocal (avoids shadow of the renamed factory).
- scripts/audit-permissions.ts — stale allowlist entry for
/berth-reservations/[id]/route.ts removed (path no longer exists).
- docs/runbooks/permission-audit.md — stale row for same path removed.
- docs/tenancies-design.md — fixed factual error
("tenancies.service.ts" → "berth-tenancies.service.ts").
Verified: tsc clean, 1493/1493 vitest.
Dev-server note: the running `next dev` process started before P2 and
shows Turbopack cached compile errors against the renamed schema files.
Source is correct (./tenancies); restart `next dev` to clear the cache.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| bfb29ab619 |
feat(tenancies-p4): public-map status flip via active permanent tenancy
- derivePublicStatus gains optional hasActivePermanentTenancy flag; precedence updated to "sold > under_offer > available" where Sold can come from EITHER berths.status='sold' (admin set) OR an active permanent-class tenancy (only when module enabled). - Permanent-class tenure types defined in one place (isPermanentTenureType): permanent | fee_simple | strata_lot. Seasonal / fixed_term tenancies do NOT flip — they fall through to the existing under_offer / available precedence. - /api/public/berths (list) + /api/public/berths/[mooringNumber] (single) both gate the lookup on isTenanciesModuleEnabled(portId). Disabled module = lookup skipped entirely, preserving pre-module behaviour for ports that haven't opted in. - 8 new unit tests covering: flip from available, flip from under_offer, explicit sold idempotency, false-flag fallthrough, default-omit pre- module behaviour, permanent-class membership for each tenure type, and null/undefined/unknown rejection. Verified: tsc clean, 1493/1493 vitest. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| ccc775dc66 |
feat(tenancies-p2): rename berth_reservations → berth_tenancies (schema + perms + UI)
73-file atomic rename per docs/tenancies-design.md:
- Migration 0085: rename table + indexes + FK constraints; rename
documents.reservation_id → tenancy_id; migrate jsonb permission maps
(reservations resource → tenancies; collapse create+activate → manage);
rewrite historical audit_logs.entity_type='berth_reservation' →
'berth_tenancy'. FK renames wrapped in DO blocks so dev DBs that pre-date
the FK additions don't abort.
- Schema: berthReservations → berthTenancies; BerthReservation type →
BerthTenancy; indexes idx_br_* / idx_brr_* → idx_bt_*.
- RolePermissions: resource { view, create, activate, cancel } collapses to
{ view, manage, cancel }; all 8 default seed bundles + role-form + matrix
updated.
- Service: berth-reservations.service.ts → berth-tenancies.service.ts;
endReservation → endTenancy; listReservations → listTenancies.
- API: /api/v1/berth-reservations → /api/v1/tenancies (+ nested [id]);
/api/v1/berths/[id]/reservations → /api/v1/berths/[id]/tenancies.
- Validators: reservations.ts → tenancies.ts; RESERVATION_STATUSES →
TENANCY_STATUSES; endReservationSchema → endTenancySchema.
- Routes: /{portSlug}/berth-reservations → /{portSlug}/tenancies;
/portal/my-reservations → /portal/my-tenancies.
- Components: src/components/reservations/* → src/components/tenancies/*;
BerthReservationsTab → BerthTenanciesTab; ClientReservationsTab →
ClientTenanciesTab; ReservationList → TenancyList.
- Socket events: berth_reservation:* → berth_tenancy:*; payload
reservationId → tenancyId.
- Webhook events: berth_reservation.* → berth_tenancy.*.
- Portal: getPortalUserReservations → getPortalUserTenancies;
PortalReservation → PortalTenancy; PortalDashboard.counts.activeReservations
→ activeTenancies; PortalNav label "Reservations" → "Tenancies".
- Dossier: DossierReservation → DossierTenancy; reservationDecisions →
tenancyDecisions across smart-archive-dialog + bulk-archive routes.
- Documents schema: documents.reservationId → documents.tenancyId
(TS + DB column + index + FK constraint).
- Activity feed label berth_reservation → berth_tenancy (matched against
migrated historical audit rows).
KEPT (separate concepts):
- Reservation Agreement document type (the contract sent to clients).
- "Reservation" pipeline stage name.
- {{reservation.*}} merge tokens in template authoring.
- interest.reservationStatus / reservationDocStatus / dateReservationSent
fields (track agreement signing on the deal).
- reservation-agreement-context.ts service (builds merge context for the
Reservation Agreement doc; only its DB imports were renamed).
Verified: tsc clean, 1480/1480 vitest passing, migration applied.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| 221ae5784e |
chore(autonomous-session): consolidate uncommitted work from prior session
Bundles the prior autonomous-session output that was sitting unstaged: - Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances) - country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk after the per-subpath dynamic-import approach silently failed in webpack) - Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index, redirects (ocr to ai, reports to dashboard, invitations to users), docs/admin-ia-proposal.md - Per-template email tester (registry + endpoint + UI on Email admin page) - Cancel-document mode picker (delete-from-Documenso vs keep-for-audit) - Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers - Customize-widgets per-region sortables at xl+ (charts/rails/feed); single flat sortable below xl when the layout stacks; per-viewport saved orders - Audit doc updates capturing each shipped item - Lint fixes: react-compiler immutability in DonutChart (reduce instead of let-reassign), set-state-in-effect disables in CountryFlag and UploadForSigning preview-bytes effect, unused 'confirm' destructures in interest contract + reservation tabs, unescaped apostrophe in test-template card copy |
|||
| c14f80a4f7 |
feat(uat-batch): Group Q — platform refactors
Q58, Q59, Q61 from the 2026-05-21 plan. Q57 + Q60 (sweep-scope) parked.
Shipped:
Q58 SelectTrigger size variant. <SelectTrigger> now accepts
`size?: 'default' | 'sm'`. Default = `h-11` so the trigger
matches <Input>'s h-11 default and the 8px height mismatch
called out in the UAT vanishes platform-wide. Existing call
sites that need the legacy compact look (FilterBar, dense
table headers) opt back in via `size="sm"`. Nothing breaks —
the default render flips height without touching any other
styling.
Q59 Table density min-widths + nowrap. DataTable cells now
default to `whitespace-nowrap` so long values (URLs, names,
addresses) don't wrap into 4-5 lines and inflate row height.
Columns that need wrapping override via the column def's
`meta.wrap = true`. Min-width comes from
`column.getSize?.()` when set so a column doesn't shrink-
wrap below readability — opt-in per column rather than a
sweeping width change.
Q61 Error message audit foundation — Documenso 401/403 path
enriched. <PortDocumensoConfig> gains `apiKeySource` +
`apiUrlSource` ('port' | 'global' | 'env' | 'default' |
'none'). `getPortDocumensoConfig` populates them based on
which layer of the resolver chain produced the value.
documenso-client's <ResolvedCreds> exposes the source flags;
the 401/403 branch surfaces them in the
`DOCUMENSO_AUTH_FAILURE` internalMessage so operators see
"api key source: env, port: <id>" instead of the prior
generic `path → 401` body. Solves the Documenso diagnosis
loop that prompted the platform-wide error audit. Same
pattern can extend to other integration error paths in
follow-ups (S3, Redis, IMAP) — the resolver-source helper
lives on PortConfig now.
Q60 Tooltip audit primitive already shipped — <FieldLabel> in
`ui/field-label.tsx` is the canonical surface with an Info
icon + Tooltip slot. One adopter live (custom-field-form);
remaining admin-form sweep is the lift that's parked.
Deferred:
Q57 recharts → ECharts migration (6-10h). Pure visual port of
8 chart components; safer as a focused session with
per-chart visual review. Pre-reqs (ECharts deps + the
transpilePackages config + the d3-geo install) are in place
so the migration can be picked up cleanly.
Verified: tsc clean, vitest 1454/1454.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| b4bf9cca3f |
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>
|
|||
| b3f87563c6 |
feat(audit-cleanup): finish all 15 outstanding items from verified backlog
Audit cleanup completion plan, all tiers shipped: Tier 1 (security + data integrity) - A.7 RTBF true wipe: redact email_messages body/subject/addresses for threads owned by deleted client; redact document_sends.recipient_email; collect file storage keys + delete blobs post-commit. - A.8 user_permission_overrides FK: documented inline why cascade is correct (not set-null as audit suggested) — overrides have no value without their user. - W2.14 PII redaction: camelCase normalization in audit.ts + error-events.service.ts isSensitiveKey; added city/postal/country/ birth fragments. firstName/lastName/dateOfBirth/postalCode etc. now caught in BOTH masker paths. 12 new test cases lock the coverage. Tier 2 (Documenso completion + refactor) - C.2: documentEvents.recipient_email column + partial unique index for per-recipient webhook dedup (migration 0075). handleDocumentSigned now sets recipient_email on insert. - Phase 2: completion_cc_emails distribution. handleDocumentCompleted reads documents.completionCcEmails, filters out signer-duplicates case-insensitively, fans signed PDF out to non-signer recipients. - C.4: extracted createPublicInterest() service from the 346-line api/public/interests route. Route becomes a thin shell (rate-limit, port resolution, audit log, email fan-out). The trio creation logic is now unit-testable without an HTTP fixture. - Phase 4: POST /api/v1/document-templates/[id]/detect-fields wired to document-field-detector.detectFields(). Sparkles "Auto-detect" button added to template-editor.tsx — maps DetectedField → marker with best-guess merge token (DATE / NAME / EMAIL); user retags. Tier 3 (reporting + recommender snapshot lockfiles) - W7.reports: extracted rollupStageRevenue / rollupStageCounts / computeTotalForecast / computeOccupancyRate / rollupBerthStatusCounts into src/lib/services/report-math.ts (pure functions). 16 new tests including an inline-snapshot lockfile on a representative 7-stage forecast. report-generators.ts now delegates. - W7.recommender: 18 new toMatchSnapshot tripwires on classifyTier boundaries + computeHeat at canonical input points. Tier 4 (rolling) - W6.attach: fixed outdated CLAUDE.md claim — threshold banner is informational and never depended on IMAP; bounce monitoring (the IMAP poller) is separate. - D.1 + D.2: documented deferral inline with full why-not-build-it reasoning so a future engineer sees the rationale. - G.1: representative formatDate sweep (audit-log-list, user-list, document-templates merge tokens, document-signing email). Rest of the ~100 sites stay rolling. Quality gates: 1420/1420 vitest (46 new tests above baseline of 1374), tsc clean, 0 lint errors. Plan: docs/superpowers/plans/2026-05-18-audit-cleanup-completion.md Migration: 0075_c2_document_events_recipient_email.sql (applied to dev DB). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| 4b5f85cb7d |
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>
|
|||
| 98211066a5 |
fix(legacy-stage): purge 9-stage enum keys from rank tables and stale copy
L-001 hunt landed these:
- src/lib/services/clients.service.ts — stageRank used pre-refactor
9-stage names exclusively (`contract_signed`, `deposit_10pct`, …).
Every modern 7-stage interest fell to rank 0, making client-list
"most-progressed deal" sort effectively random. Modern values now
own the canonical ranks; legacy aliases map to their 7-stage
equivalents so historical audit data still sorts.
- src/lib/services/berth-recommender.service.ts — STAGE_ORDER had
the same 9-stage shape. LATE_STAGE_THRESHOLD pointed at the (now
nonexistent) `deposit_10pct` slot. Reworked to the 7-stage scale;
threshold now at `deposit_paid` (5).
- Stale comments referencing `deposit_10pct` in schema (clients,
financial) and client-archive services updated to current copy.
- Smart-archive dialog rendered `i.pipelineStage` as raw enum; now
routes through `stageLabelFor` (the new helper added with A2).
Test fixture updates: berth-recommender.test.ts numeric inputs
re-mapped to the new 7-stage scale (eoi_signed=5 → eoi=3, etc.).
1373/1373 vitest pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| e933e32dbd |
feat(schema): berths.archived_at + clients.source_inquiry_id + email_bounces
Step 3 schema additions per PRE-DEPLOY-PLAN § 1.4. berths.archived_at (+ archived_by, archive_reason) — soft-delete column so retired moorings can be hidden from the public feed and admin lists without losing historical interest joins. Partial index `idx_berths_active` on (port_id) WHERE archived_at IS NULL keeps the active-only list path fast. Already wired: - /api/public/berths and /api/public/berths/[mooringNumber] now filter out archived rows. - berths.service.listBerths defaults to active-only with an ?includeArchived=true escape hatch for the archive bin. clients.source_inquiry_id — text column with ON DELETE SET NULL FK to website_submissions(id). Preserves the linkage from a website inquiry to the client that came out of the "Convert to client" triage flow (P-4.5). Drives the conversion-funnel-by-source chart (Step 6). The Drizzle column ships without `.references()` to avoid the cross-file circular import; the FK lives in the migration SQL. email_bounces table — bounce-monitoring storage. The DSN poller worker (forthcoming, depends on this table existing) writes one row per parsed bounce; consumers join via (original_send_type, original_send_id). Three secondary indexes cover the expected access patterns (port + recent bounces; lookup by bounced address; lookup by original send). Schema additions plus the migration SQL are ready for `pnpm db:push` (or the migration runner once its journal is backfilled — separate concern, journal currently stops at 0042 despite migrations through 0065 existing on disk). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| bded8b21f1 |
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
|
|||
| 4d1fbcd469 |
feat(documenso-phase-5): pin transformSigningUrl + document website-side coordination
Phase 5 is mostly coordination + verification rather than a code build — the embedded signing pages live in a different repo. What lands here: 1. transformSigningUrl hardening — routes through extractSigningToken so a bare URL like `https://sig.example.com` no longer produces the malformed `<host>/sign/<role>/sig.example.com`. The token validator (≥8 URL-safe chars) rejects malformed tails so the function falls back to returning the raw URL. 2. 10 unit tests pin the role-segment mapping so a future refactor can't silently break the contract with the marketing website's /sign/[type]/[token] page. Covers: - all five SignerRole → URL segment mappings - trailing-slash normalization on the host - null host fallback (single-tenant / staging) - rejection of non-token-shaped tails 3. docs/documenso-integration-audit.md updated with: - Phase 2/3/4/7 landed-work summary (replacing the old "deferred" list that was now stale) - Phase 5 coordination tracker for the marketing-website side (the four edits the website team needs to make — listed here so the CRM stays the source of truth on the contract) - Phase 6 polish backlog (auto-send delay, document expiration, per-document message, reminder display, failed-webhook UI, field metadata panel, zoom controls, recipient drag-reorder) Tests: 21 new transformSigningUrl + signers tests across two files; full suite 1340 → 1350 ✅; tsc clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| 7bf587de90 |
feat(documenso-phase-4): recipient configurator + field placement UI
Phase 4 lands the visual half of the Documenso build — the upload-
for-signing dialog the Contract + Reservation tabs hand off to. Four
files of new code; the existing tab placeholders point at it.
Files added:
- lib/services/document-field-detector.ts — Phase 4c auto-detect
scanner. Uses pdfjs-dist to extract per-page text + positions, then
matches anchor patterns (Signature, Date, Initials, Email, Name,
underscore-runs) and produces percent-coordinate DetectedField
rows. Recipient label inference walks ±100pt of each match for
Buyer/Seller/Client/Witness/Notary keywords. Returns [] when the
PDF is image-only; UI falls back to manual placement without an
error. 6 unit tests pin the matching + coordinate math.
- app/api/v1/documents/auto-detect-fields/route.ts — multipart POST
endpoint that delegates to detectFields(). Permission-gated by
documents.send_for_signing.
- app/api/v1/documents/signing-defaults/route.ts — GET endpoint that
surfaces just the per-port developer + approver display name/email
+ sendMode flag. No secrets exposed; lets the dialog prefill the
recipient configurator without an admin-scoped settings read.
- components/documents/upload-for-signing-dialog.tsx — the Phase 4
UI. Three-step state machine inside a single Dialog:
1. select-file: drop/click PDF picker + title input
2. configure-recipients: client + developer + approver prefilled,
rep can add/remove/reorder + change role (SIGNER/APPROVER/CC)
3. place-fields: react-pdf renders the source PDF; auto-detect
runs in the background on file load and seeds the overlay;
rep places, drags, resizes, deletes, reassigns fields via the
palette + side panel. Native DOM drag (no dnd-kit dependency
added — the coordinate math stays obvious).
Send fires POST /api/v1/interests/[id]/upload-for-signing (Phase 3
service); success toast reflects port sendMode (auto fires the
invite immediately, manual leaves it for the rep).
Files modified:
- components/interests/interest-contract-tab.tsx + reservation-tab.tsx:
swap the ComingSoonDialog placeholder for the real
UploadForSigningDialog with the matching documentType prop. The
placeholder ComingSoonDialog helper is deleted from both.
- scripts/tsc-staged.mjs: pull src/types/**/*.d.ts into the temp
staged-only tsconfig so side-effect CSS imports (e.g.
react-pdf/dist/Page/AnnotationLayer.css) resolve via the existing
declare-module shim. Without this fix the staged compile reports
TS2882 even though the full tsc --noEmit pass passes.
Design choices noted in code comments:
- Native drag over dnd-kit: the field overlay's percent-based
coordinate math is short enough that adding a drag library adds
complexity without saving lines.
- Auto-detect on file-load (not on demand): runs immediately so the
rep doesn't have to click a second button — empty result drops
back to manual placement silently.
- Per-recipient color swatches indexed by signingOrder.
- Recipient seed via useMemo + user-event handler instead of
useEffect → setRecipients (Wave 3 set-state-in-effect avoidance).
Server-side, Phase 3 plumbing handles the rest: tenant guard, magic-
byte verify, Documenso round-trip with per-port v1/v2 routing,
recipient signingToken capture for Phase 2 webhook cascade, auto-
send when port.sendMode === 'auto'.
Tests: 1334 → 1340 ✅ (6 new for the detector); tsc clean.
Deferred polish (Phase 6):
- Per-field metadata side panel for DROPDOWN/RADIO option lists
- Pinch-zoom + zoom-out controls on the field-placement canvas
- Recipient drag-reorder via dnd-kit
- Required toggle per field
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| 33d0426911 |
feat(documenso-phase-3): custom document upload-to-Documenso
Backend foundation for the Contract + Reservation signing flows. The
existing tab placeholders point at a "send for signing" CTA that had
no code behind it; this commit lands the service + endpoint that the
Phase 4 drag-drop UI will POST to.
Files added:
- lib/services/custom-document-upload.service.ts — orchestrates the
full PDF → Documenso → local-state-update flow:
1. Magic-byte verifies the PDF (defense vs. mislabelled bytes —
same posture as berth-pdf + brochures).
2. Stores the source PDF via getStorageBackend(), works on s3 +
filesystem backends. Auto-files into the client's entity folder
when resolvable.
3. Inserts the documents row (status=draft → sent), with the file
FK + interest link + clientId snapshot.
4. Documenso round-trip via createDocument → sendDocument →
placeFields. Per-port apiVersion drives v1 vs v2 (existing
client handles both — v1: /api/v1/documents; v2: envelope/create
multipart). meta.signingOrder + redirectUrl flow through.
5. Captures recipient signingUrl + token into document_signers so
the Phase 2 cascade picks them up.
6. Auto-send first invitation when port.eoi_send_mode === 'auto';
stamps invitedAt to suppress duplicate cascades.
7. Advances pipeline stage to contract_sent.
- app/api/v1/interests/[id]/upload-for-signing/route.ts — multipart
POST endpoint. Zod-validates recipients (≤20), fields (≤200), PDF
size (≤50MB), all 11 Documenso field types. Permission-gated by
documents.send_for_signing + interests.edit (matches the
external-eoi precedent — the auto-advance side-effect is
interest-mutating).
Files modified: none — keeps the existing tab placeholders as the
entry point; Phase 4 builds the drag-drop UI on top.
Validation contract pinned by 8 unit tests covering: empty recipient
list, empty field list, empty/oversized PDF, non-PDF magic bytes,
out-of-range + negative recipientIndex, duplicate signingOrder.
The heavy paths (storage put, Documenso HTTP, signer update) are
exercised by the existing realapi Playwright project — no new
realapi specs added because the contract-upload UI doesn't exist yet
to drive them.
Verified against Documenso API spec (v1 OpenAPI + v2 docs via
Context7): recipients[].token is on the Recipient model in both
versions; webhook payloads echo the same shape so the Phase 2 token-
match handler works against custom-uploaded docs without changes.
Tests: 1326 → 1334 ✅; tsc clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| 3dc4c6ff14 |
feat(documenso-phase-2): webhook handler enhancement — cascade + completion fan-out
Closes the silence after the first signing invitation. Three real improvements on top of the existing webhook plumbing, all aligned with the Documenso v1.32 + v2 webhook payload shape (verified against the official OpenAPI spec + Context7 docs): 1. Cascading "your turn" emails — when DOCUMENT_SIGNED / DOCUMENT_ RECIPIENT_COMPLETED / RECIPIENT_SIGNED fires for a recipient, handleRecipientSigned now resolves the next pending signer in signing order and sends them the branded sendSigningInvitation() email with the embedded-host-wrapped URL. Stamps invitedAt so a duplicate webhook retry doesn't re-send. 2. On-completion PDF distribution — handleDocumentCompleted now re- reads the just-committed signedFileId, resolves all signers, and fires sendSigningCompleted() to every recipient with the signed PDF attached. resolveAttachments in lib/email already pulls bytes through getStorageBackend() so this works under both the s3/minio and filesystem backends without changes. Failures fall through to logger.error rather than throwing — the document is already marked completed and the admin can re-trigger manually. 3. Token-based recipient matching — Documenso v1 + v2 webhook recipients carry a `token` field (per the OpenAPI spec); same token appears in the document-create response. Captured at send time into the existing document_signers.signing_token column (already in schema from Phase 1) and used by handleRecipientSigned + handleDocumentOpened before falling back to email match. Robust against the case where one email serves multiple roles on a contract — which is the documented gap in the legacy nocodb-based handler. Supporting changes: - New helper module lib/services/documenso-signers.ts with extractSigningToken() (URL-tail fallback), DOC_TYPE_LABEL map, and nextPendingSigner() picker. 11 unit tests cover the token-regex, the helper picks the lowest pending signing-order, and rejects declined/signed correctly. - documenso-client normalizeDocument now reads `token` from both `recipients[]` and the legacy capital-R `Recipient[]` array Documenso v1.32 sometimes ships in webhooks. - documents.service signer-update at send time prefers the explicit token field, falling back to extractSigningToken(signingUrl) for any v2 deployment whose distribute response omits it. Out of scope for Phase 2 (per the build plan): - Custom-doc upload-to-Documenso path (Phase 3) - Recipient + field-placement UI (Phase 4) - DNS-rebinding hardening + circuit-breaker (deferred-refactor list) - Auto-reminder cron — manual "Send reminder" button + auto-reminder toggle remain manual until Phase 6 polish Tests: 1315/1315 vitest ✅ + 11 new tests for documenso-signers ✅; tsc clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| d3960af340 |
feat: warm-up deps — ts-reset, web-vitals, RHF devtool, query-broadcast
Four low-risk adds before the Zod 4 / drizzle-zod headliner: - @total-typescript/ts-reset: tightens TS stdlib types globally (JSON.parse → unknown, fetch().json() → unknown, .filter(Boolean) narrows, Set literals respect typed Set targets). Caught 179 latent type errors; fixed all production sites (8 files) and added `any` cast escape hatch in test files (ESLint exemption scoped to tests/). - web-vitals + /api/v1/internal/vitals endpoint + WebVitalsReporter client component: establishes Core Web Vitals baseline (LCP/INP/CLS/ FCP/TTFB) via navigator.sendBeacon. Required before optimisation work. - @hookform/devtools + FormDevtool wrapper: dev-only RHF state inspector, lazy-loaded via next/dynamic so the chunk is excluded from prod bundles entirely. - @tanstack/query-broadcast-client-experimental: cross-tab cache sync via BroadcastChannel — wired in query-provider.tsx, 1-liner. Audit doc updated with sections 35 + 36 (PDF stack overhaul + comprehensive second-pass package sweep) covering ~20 package adoption candidates and 4-5 deprecation candidates. Verified: tsc clean, vitest 1293/1293 pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| 04a594963f |
feat: round 2 — stage prompts, berth header, EOI inline edit, measurement units
Berth surfaces - New compact mooring-chip header (colored plate + status pill, dock-label in tooltip) replaces the redundant "Berth B1 / Sold / B DOCK" stack - Berth list gains a "Latest deal stage" column showing the most-advanced pipeline stage of any active linked interest (server-aggregated, ranks by PIPELINE_STAGES index) - "Linked prospect" Select on the status-change dialog rebuilt as a Command combobox: search, recent-first sort, stage-coloured pills Pipeline UX - Reverting an interest to Open with linked berths now prompts: keep the links, unlink and reset, or cancel. Silent when no berths are linked - Activity feed + entity-activity feed normalise enum field values via STAGE_LABELS / formatSource: "deposit_10pct → contract_sent" reads as "10% Deposit → Contract Sent" EOI generate dialog - Inline-editable rows for client name, nationality (country combobox), and yacht name — pencil affordance saves directly via clients/yachts PATCH - Replaces the single "Edit on client's page" link with two contextual links framed by short copy explaining what's inline vs what needs the canonical page - Backend EoiContext now includes client.id + yacht.id so the dialog can PATCH without an extra round-trip Company form - New "Connections" section lets the rep attach members (clients) and yachts during create. Yacht attach uses the existing transfer endpoint so audit log + ownership history capture the change - Inline "+ New client" / "+ New yacht" buttons open the canonical forms stacked over the company sheet - After save, the form chains to a yacht pull-in prompt (if any attached client owns yachts not yet linked) and an optional "Create interest" step pre-filled with the first attached client Admin - /admin landing gains a searchable index — typed query flattens groups into a result list matching label + description + group title - "Documenso & EOI" card relabelled to "EOI signing service" (consistent with the user-facing language rename from round 1) Measurement units (migration 0053) - interests gains desired_*_m columns + desired_*_unit discriminators so the rep's literal entry (ft OR m) is preserved verbatim instead of being reconstructed from a single canonical column on every render - yachts + berths gain matching *_unit columns alongside their existing ft + m pairs; defaults to 'ft' so legacy rows still render normally - Interest form POST/PATCH now sends both ft + m + unit; computed m is derived from the ft canonical to keep the recommender SQL unchanged Misc - Active-deals tile + topbar type their Link href as `Route` instead of `any` - Unused REPORT_TYPE_LABELS const dropped from generate-report-form - Test fixtures (fill-eoi-form, documenso-payload, public-berths) updated to include the new id + unit fields on the EoiContext / Berth shapes Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| 3ffee79f3f |
feat(ui): broad consistency sweep — sources, dates, comboboxes, milestones
Mobile + responsive - berth-form full-width on phones (was 480px fixed → overflowed iPhone) - currency-input switched to inputMode=decimal with live thousands separator - client-form Country/Timezone/Source/Preferred-Contact full-width <sm - contacts row restructured so Primary toggle + Remove get their own strip - customize-dashboard footer stacks vertically on mobile; Done full-width - interest-form client/berth pickers no longer cmdk-filter on UUID (typing "Carlos" now returns Carlos Vega instead of "No clients found") Data + consistency - SOURCES + SOURCE_LABELS + formatSource() in lib/constants; 9 surfaces now resolve interest/client source from one place - INTEREST_OUTCOMES adds lost_other (picker, badge, timeline) - Berth options natural-sort A1 → A2 → … → A10 via lib/utils/mooring-sort - archiver downgraded ^8 → ^7.0.1 so the GDPR export route compiles - TableBody last-row uses border-b-0 (not border-0); colored left-accent on the bottom berth row now renders - Hide Invite-to-Portal until port setting === true (was !== false default-show) - OwnerPicker primer query resolves entity name on first paint (no more UUID flash before the popover opens) Terminology - Replaced user-facing "Documenso" with "signing service" / "Generated EOI" / "Manual EOI" in 8 components (admin/internal references kept) - Plainer status-change copy on berth-detail-header Forms + editing - InlineEditableField gained a `date` variant (native picker); applied to company incorporation date and ready for other YYYY-MM-DD plaintext fields - Inline source picker on interest-tabs detail (was free text) - TagPicker self-hides when port has no tags AND nothing is selected - New ReminderDaysInput with preset chips (1d / 3d / 1wk / 2wk / 1mo / custom) - Compose dialog follow-up is now a toggle that reveals datetime picker Pipeline milestones - changeStageSchema accepts optional milestoneDate; service stamps it on the matching date column instead of always using now - MilestoneAdvanceButton popover collects a back-date before stage advance - Applied to every "Mark X manually" surface on the interest overview EOI / linked-berths polish - Add-bypass row aligned inline with toggle descriptions - Tooltips on "Specifically pitching" / "Mark in EOI bundle" explain their legal vs. public-map consequences Surfaces - Companies list now has the column picker + persisted hidden-column prefs - NotesList aggregate flag enabled on clients, companies, residential_clients (yachts already aggregated) ft/m unit toggle (interim, before drift fix) - "Berth size desired" gets a section-level ft/m toggle; per-field hint shows the converted value. Storage stays canonical-ft for now; the drift-safe persistence migration is the next step. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| d597e158fe |
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>
|
|||
| 72ab7180cf |
feat(public-berths): expose booleans, metric variants, timestamps
Bring the public berth feed to verbatim NocoDB parity (all fields except Price, which is held pending an explicit policy decision per the audit follow-ups Q4). Adds: - Berth Approved (boolean) - Water Depth (number) - Width Is Minimum / Water Depth Is Minimum (boolean) - Length / Width / Draft / Water Depth / Nominal Boat Size (Metric) - CreatedAt / UpdatedAt (ISO strings, useful for cache invalidation) Booleans pass through as nullable to preserve NocoDB's tri-state checkbox semantics (true / false / unset). Test fixtures cover the new fields end-to-end including the null-passthrough case. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
| 5c8c12ba1f |
feat: autonomous backlog push — admin UX overhaul + storage parity + residential parity + Documenso Phase 1
Massive multi-area push driven by docs/admin-ux-backlog.md. Every byte
path now goes through getStorageBackend() so signed EOIs, contracts,
brochures, berth PDFs, files, avatars, branding logos, and DB backups
all work identically on S3 and filesystem backends.
USER SETTINGS (rebuild)
- Country + Timezone selectors with cross-defaulting
- Browser-detected timezone banner ("Looks like you're in Europe/Paris…")
- Email change with verification flow (user_email_changes table,
OLD-address cancel link + NEW-address confirm link)
+ EMAIL_CHANGE_INSTANT=true dev shortcut
- Password reset triggered via better-auth requestPasswordReset
- Profile photo upload + crop (square 256×256) via shared
<ImageCropperDialog> + /api/v1/me/avatar
BRANDING
- Shared <ImageCropperDialog> using react-easy-crop
- Logo upload + crop in /admin/branding (writes via
/api/v1/admin/settings/image -> storage backend)
- Email header/footer HTML defaults injectable via "Insert default"
- SettingsFormCard new field types: timezone (combobox), image-upload
STORAGE ADMIN OVERHAUL
- S3 config form FIRST, swap action SECOND
- Test connection before any switch
- Two-button switch: "Switch + migrate" vs "Switch only" with
warning modals
- runMigration() honours skipMigration flag
- /api/ready + system-monitoring health check use the active
storage backend instead of always probing MinIO
- Filesystem backend already had full feature parity — verified
BACKUP MANAGEMENT (real)
- New backup_jobs table (id / status / trigger / size / storage_path)
- runBackup() service spawns pg_dump --format=custom, streams to
active storage backend via getStorageBackend().put()
- /admin/backup page: trigger, history, download .dump for restore
- Super-admin gated
AI ADMIN PANEL
- /admin/ai consolidates master switch + monthly token cap +
provider credentials
- Per-feature settings (OCR, berth-PDF parser, recommender)
linked from the same page
ONBOARDING WIZARD
- /admin/onboarding now real with auto-checked steps
- Reads each setting key + lists endpoint (roles/users/tags) to
decide completion
- Manual checkboxes for steps without an auto-detect signal
- Progress bar + Mark done/Mark incomplete buttons
- State persisted in system_settings.onboarding_manual_status
RESIDENTIAL PARITY (full)
- New residential_client_notes + residential_interest_notes tables
(mirror marina-side shape)
- Polymorphic notes.service.ts extended (verifyParent, listForEntity,
create, update, delete) for residential_clients/_interests
- <NotesList> component accepts the new entity types
- 4 new note endpoints (GET/POST/PATCH/DELETE for clients + interests)
- 2 new activity endpoints (residential clients + interests)
- residential-client-tabs.tsx + residential-interest-tabs.tsx use
DetailLayout (Overview / Interests / Notes / Activity)
- residential-client-detail-header.tsx mirrors marina-side strip
- useBreadcrumbHint wired into both detail components
- Configurable Assigned-to dropdown (residential_interests.view perm)
CONFIGURABLE RESIDENTIAL STAGES
- residential-stages.service.ts with list / save / orphan-check
- /api/v1/residential/stages GET/PUT
- /admin/residential-stages admin UI with reassign-on-remove modal
- Validators relaxed from z.enum to z.string
DOCUMENSO PHASE 1
- Schema: document_signers.invited_at / opened_at /
last_reminder_sent_at / signing_token (+ idx_ds_signing_token)
- Schema: documents.completion_cc_emails (text[]) +
auto_reminder_interval_days (int)
- transformSigningUrl() now maps SignerRole -> URL segment via
ROLE_TO_URL_SEGMENT (approver->cc, witness->witness) — fixes
Risk #5 where approver invites landed on /sign/error
- POST /api/v1/documents/[id]/send-invitation with auto-pick of
next pending signer
- Per-port settings: documenso_developer_label / _approver_label
+ documenso_developer_user_id / _approver_user_id (Phase 7
Project Director RBAC binding fields)
ADMIN UX RAPID-FIRE
- Sidebar collapse removed (always-expanded design)
- Audit log: input sizes (h-9), date pickers w-44, action cell
sub-label so single-row entries aren't blank
- Sales email config: token list <details> + tooltips on
threshold + body fields
- Custom Settings card: long-form description
- Reminder digest timezone uses TimezoneCombobox
- Port form: currency dropdown (10 common currencies) + timezone
combobox + brand color picker
- Permissions count badge opens modal with granted/denied per
resource
- Role names display-normalized via prettifyRoleName
- Tag form: native input type=color
- Custom Fields page: amber heads-up about non-integration
- Settings manager: select field type + fallthrough_policy as dropdown
- Storage admin S3 fields ship as proper password + boolean
LIST PAGES
- Residential client list: clickable email/phone (mailto/tel/wa.me)
- Residential interests + Documents Hub search inputs sized h-9
CURRENCY API
- scripts/test-currency-api.ts verifies live Frankfurter fetch
-> DB upsert -> getRate -> convert. Inverse-rate drift <=0.001
TESTS
- 1185/1185 vitest passing
- tsc clean
- eslint 0 errors (16 pre-existing warnings)
Note: WEBSITE_INTAKE_SECRET added to .env.example but committed
separately due to pre-commit hook policy on .env* files.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|||
| 267c2b6d1f |
feat(search): full-platform search overhaul + view tracking + notes bucket
Service rewrite covers 14 entity buckets (clients, residential clients, yachts, companies, interests, residential interests, berths, invoices, expenses, documents, files, reminders, brochures, tags, notes, navigation) with prefix tsquery + trigram fallback, phone-digit normalization, and JOINs to client_contacts for email matching. New `notes` bucket searches across the four note tables (client, interest, yacht, company) via UNION + parent-entity label resolution (berth mooring for interests, name for yachts/companies). Renders at the bottom of the dropdown so broad-content matches don't crowd entity-specific hits — per the user's "low-noise" preference. Recently-viewed tracking persists last 20 entity views per user in Redis sorted set; CommandSearch surfaces them as the dropdown's default state and applies affinity ranking when the user types. ID-resolve endpoint accepts pasted UUIDs (or invoice numbers like `INV-2025-001`) and routes the rep straight to the entity, skipping the normal search bucket. Audit search service gains `entityIds[]` array filter for the new loadClientActivityAggregated() path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
|||
|
|
fc7595faf8 |
fix(audit-tier-2): error-surface hygiene — toastError + CodedError sweep
Two mechanical sweeps closing the audit's HIGH §16 + MED §11 findings: * 38 client components / 56 toast.error sites converted to toastError(err) so the new admin error inspector becomes usable from user-reported issues — every failed inline-edit, save, send, archive, upload, etc. now carries the request-id + error-code (Copy ID action). * 26 service files / 62 bare-Error throws converted to CodedError or the existing AppError subclasses. Adds new error codes: DOCUMENSO_UPSTREAM_ERROR (502), DOCUMENSO_AUTH_FAILURE (502), DOCUMENSO_TIMEOUT (504), OCR_UPSTREAM_ERROR (502), IMAP_UPSTREAM_ERROR (502), UMAMI_UPSTREAM_ERROR (502), UMAMI_NOT_CONFIGURED (409), and INSERT_RETURNING_EMPTY (500) for post-insert returning-empty guards. * Five vitest assertions updated to match the new user-facing wording (client-merge "already been merged", expense/interest "couldn't find that …", documenso "signing service didn't respond"). Test status: 1168/1168 vitest, tsc clean. Refs: docs/audit-comprehensive-2026-05-05.md HIGH §16 (auditor-H Issue 1) + MED §11 (auditor-G Issue 1). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
6a609ecf94 |
fix(audit-tier-1): timeouts, lifecycle, per-port Documenso, FK constraints
Closes the second wave of HIGH-priority audit findings: * fetchWithTimeout helper (new src/lib/fetch-with-timeout.ts) wraps Documenso, OCR, currency, Umami, IMAP, etc. — a hung upstream can no longer pin a worker concurrency slot indefinitely. OpenAI client passes timeout: 30_000. ImapFlow gets socket / greeting / connection timeouts. * SIGTERM / SIGINT handler in src/server.ts drains in-flight HTTP, closes Socket.io, and disconnects Redis before exit; compose stop_grace_period bumped to 30s. Adds closeSocketServer() helper. * env.ts gains zod-validated PORT and MULTI_NODE_DEPLOYMENT, and filesystem.ts now reads from env (a typo can no longer silently disable the multi-node guard). * Per-port Documenso template + recipient IDs land in system_settings with env fallback (PortDocumensoConfig now exposes eoiTemplateId, clientRecipientId, developerRecipientId, approvalRecipientId). document-templates.ts uses the per-port config and threads portId into documensoGenerateFromTemplate(). * Migration 0042 wires the eleven HIGH-tier missing FK constraints (documents/files/interests/reminders/berth_waiting_list/ form_submissions) plus polymorphic CHECK round 2 (yacht_ownership_history.owner_type, document_sends.document_kind), invoices.billing_entity_id NOT EMPTY, and clients.merged_into self-FK. Drizzle schema columns updated to .references(...) where possible so the misleading "FK wired in relations.ts" comments are gone. Test status: 1168/1168 vitest, tsc clean. Refs: docs/audit-comprehensive-2026-05-05.md HIGH §§5,6,7,8,9,10 + MED §§14,15,16,18. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
249ffe3e4a |
feat(berths): per-berth PDF storage (versioned) + reverse parser
Phase 6b of the berth-recommender refactor (see
docs/berth-recommender-and-pdf-plan.md §3.2, §3.3, §4.7b, §11.1, §14.6).
Builds on the Phase 6a pluggable storage backend (commit
|
||
|
|
e00e812199 |
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>
|
||
|
|
b1e787e55c |
feat(recommender): SQL ranking + tier ladder + heat scoring
Plan §4.4 + §13: pure SQL recommender, no AI. Single CTE chain (feasible -> aggregates) + JS-side tier classification, fall-through cooldown filter, heat scoring, and fit ranking. Per-port settings via system_settings layered over global + DEFAULT_RECOMMENDER_SETTINGS. Tier ladder (default): A : no interest history B : lost-only history (still recommendable + boosted by heat) C : active interest in early stage (open..eoi_signed) D : active interest at deposit_10pct or beyond (hidden by default) Heat (only for tier B): recency weight 30 full @ <=30 days, decays to 0 @ 365 days furthest stage weight 40 full when prior reached deposit interest count weight 15 saturates at 5+ EOI count weight 15 saturates at 3+ Multi-port isolation enforced (§14.10 critical): the SQL filters by port_id AND the entry-point function rejects cross-port interest lookups with an explicit error. Fall-through policy supports immediate_with_heat (default), cooldown, and never_auto_recommend. 15 unit tests covering tier classification, heat saturation, weight tuning, zero-weight guard. Smoke-tested end-to-end via scripts/dev-recommender-smoke.ts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
fb1116f1d4 |
feat(berths): public berths API + health env-match endpoint
Adds the read-only public-website data feed promised by plan §4.5 and
§7.3. The marketing site's `getBerths()` swap is now a one-line URL
change against the existing 5-min TTL behaviour.
- src/app/api/public/berths/route.ts: GET / unauth, returns the full
port-nimara berth list as { list, pageInfo } in the verbatim NocoDB
shape ("Mooring Number", "Side Pontoon", quoted-key fields). Cache:
s-maxage=300 + stale-while-revalidate=60. portSlug query param lets
future ports opt in.
- src/app/api/public/berths/[mooringNumber]/route.ts: GET single. Up-
front regex validation (^[A-Z]+\\d+$) rejects malformed lookups with
400 + cache-control:no-store before hitting the DB. 404 + no-store
when not found.
- src/app/api/public/health/route.ts: returns { status, env, appUrl,
timestamp } so the marketing site can refuse to start when its
CRM_PUBLIC_URL points at a different deployment env (§14.8 critical
env-mismatch protection).
- src/lib/services/public-berths.ts: pure mapper with derivePublicStatus
("sold" wins; otherwise specific-interest junction OR
status='under_offer' -> "Under Offer"; else "Available").
- 11 unit tests covering numeric coercion, status derivation,
archived-berth handling, missing-map-data omission, and the
status-precedence rule that "sold" trumps the specific-interest
signal.
Smoke-tested: /api/public/berths -> 117 rows, A1 correctly shows
"Under Offer" (has interest_berths.is_specific_interest=true link),
INVALID -> 400, Z99 -> 404. Total tests: 996 -> 1007.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
6e3d910c76 |
refactor(interests): migrate callers to interest_berths junction + drop berth_id
Phase 2b of the berth-recommender refactor (plan §3.4). Every caller of the legacy `interests.berth_id` column now reads / writes through the `interest_berths` junction via the helper service introduced in Phase 2a; the column itself is dropped in a final migration. Service-layer changes - interests.service: filter `?berthId=X` becomes EXISTS-against-junction; list enrichment uses `getPrimaryBerthsForInterests`; create/update/ linkBerth/unlinkBerth all dispatch through the junction helpers, with createInterest's row insert + junction write sharing a single transaction. - clients / dashboard / report-generators / search: leftJoin chains pivot through `interest_berths` filtered by `is_primary=true`. - eoi-context / document-templates / berth-rules-engine / portal / record-export / queue worker: read primary via `getPrimaryBerth(...)`. - interest-scoring: berthLinked is now derived from any junction row count. - dedup/migration-apply + public interest route: write a primary junction row alongside the interest insert when a berth is provided. API contract preserved: list/detail responses still emit `berthId` and `berthMooringNumber`, derived from the primary junction row, so frontend consumers (interest-form, interest-detail-header) need no changes. Schema + migration - Drop `interestsRelations.berth` and `idx_interests_berth`. - Replace `berthsRelations.interests` with `interestBerths`. - Migration 0029_puzzling_romulus drops `interests.berth_id` + the index. - Tests that previously inserted `interests.berthId` now seed a primary junction row alongside the interest. Verified: vitest 995 passing (1 unrelated pre-existing flake in maintenance-cleanup.test.ts), tsc clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
18119644ae |
feat(berths): nocodb berth import script + helpers + unit tests
Idempotent NocoDB Berths -> CRM `berths` import script with full re-run safety. Re-running picks up NocoDB additions/edits without clobbering CRM-side overrides (compares updated_at vs last_imported_at, 1-second tolerance for sub-second clock drift). --force overrides the edit guard. Mitigates the §14.1 critical/high cases: - Mooring collisions: unique (port_id, mooring_number) on the table. - Concurrent runs: pg_advisory_xact_lock on a stable BIGINT key. - Numeric-with-units inputs: parseDecimalWithUnit() strips trailing ft/m/kw/v/usd/$ markers before parsing. - Metric drift: NocoDB's metric formula columns are ignored; metric values recomputed from imperial via 0.3048 + round-to-2-decimals to match NocoDB's `precision: 2` columns and avoid spurious diffs. - Map Data shape: zod-validated; failures are skipped rather than aborting the import. - Status enum mapping: NocoDB display strings -> CRM snake_case. - NocoDB row deleted: reported as "orphaned in CRM"; never auto- deleted (rep decides via admin UI in a future phase). Pure helpers (parseDecimalWithUnit, mapStatus, parseMapData, extractNumerics, mapRow, buildPlan) live in src/lib/services/berth-import.ts so vitest can exercise the mapping logic without triggering the script's top-level db connection. 40 new unit tests (956 -> 996 passing). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
05be89ec6f |
feat(berths): normalize mooring numbers to canonical form
Sweep CRM mooring numbers from the legacy hyphen+padded form ("A-01")
to the canonical bare form ("A1") used by NocoDB, the public website,
the per-berth PDFs, and the Documenso EOI templates. Drift was
introduced by the original load-berths-to-port-nimara.ts seed; this
gates the Phase 3 public-website cutover where /berths/A1 URLs would
404 against a CRM still storing "A-01".
- 0024 data migration: idempotent regexp_replace + post-update sanity
check that surfaces any non-conforming rows for manual triage.
- Invert normalizeLegacyMooring in dedup/migration-apply: it now
canonicalizes ("D-32" -> "D32") instead of legacy-izing.
- Update tiptap-to-pdfme example tokens, EOI fixture moorings, and
smoke-test seed moorings.
- Refresh seed-data/berths.json to canonical form; drop the now-
redundant legacyMooringNumber field.
- Delete scripts/load-berths-to-port-nimara.ts (superseded in 0c).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
d197f8b321 |
feat(eoi): align prerequisites with EOI document structure
Match the gate to the actual EOI's structure (Section 2 vs Section 3) so
the rep can generate the document the moment they have what they need —
and not before.
Required (Section 2 — top paragraph):
- Client name
- Client primary email
- Client primary address
Optional (Section 3 — left blank when absent):
- Linked yacht (name, dimensions)
- Linked berth (mooring number)
Previously the dialog blocked generation unless yacht AND berth were both
linked, which was overzealous — early-stage EOIs are routinely sent before
a specific berth is pinned down.
- eoi-context.ts: yacht and berth are now nullable in the returned
context. The hard ValidationError is now driven by the EOI's Section
2 fields (name/email/address) rather than yacht/berth presence. The
owner block falls back to the interest's client when no yacht is
linked, so signing parties remain resolvable.
- documenso-payload.ts + fill-eoi-form.ts: Section 3 form values
render as empty strings when yacht or berth are absent, so the
rendered PDF leaves those template inputs blank.
- document-templates.ts: yacht.* and berth.* tokens fall back to
empty strings; the legacy-fallback catch handler also recognises
the new "missing required client details" error.
- interests.service.ts: getInterestById now also returns
`clientPrimaryEmail` and `clientHasAddress` so the Documents tab
can compute the EOI prerequisites checklist client-side without an
extra fetch.
- eoi-generate-dialog.tsx: prereqs split into two groups visually —
Required (with red ✗ when missing) and Optional (with grey – when
absent). The Generate button only requires the Required block to
pass. A small amber banner surfaces when Required is incomplete so
the rep knows where to add the missing data.
Tests: 835/835 pass. Replaces the obsolete "throws on missing yacht/
berth" tests with parity coverage for the new behaviour ("builds a
valid context when yacht/berth missing", "throws when client email/
address missing"). Adds a payload test for the empty-Section-3 case.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
27cdbcc695 |
chore(i18n): drop legacy free-text country/nationality columns
Test-data only — no production migration needed (per earlier decision).
Schema is now ISO-only; readers convert ISO codes to localized names where
human-readable output is required (EOI documents, invoices, portal).
Migration 0016 drops:
- clients.nationality
- companies.incorporation_country
- client_addresses.{state_province, country}
- company_addresses.{state_province, country}
Code paths that previously read free-text values now read the ISO column
and pass through `getCountryName()` / `getSubdivisionName()` for rendering.
Document templates ({{client.nationality}}), portal client view, EOI/
reservation-agreement contexts, and invoice billing addresses all updated.
Public yacht-interest endpoint (/api/public/interests) drops the legacy
fields from its insert path and writes ISO codes only. The Zod validators
no longer accept the legacy fields — older website builds posting raw
'incorporationCountry' / 'country' / 'stateProvince' will get 400s.
Server-side phone normalization is unchanged.
Seed data updated to use ISO codes (GB/FR/ES/GR/SE/IT/GH/MC/PA), spread
across continents to keep test fixtures realistic.
Test assertions updated to match the new render shape (e.g.
'United States' not 'US', 'California' not 'CA').
Vitest: 741 -> 741 (unchanged count; assertions updated, no new tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
978df1c4d7 |
feat(reminders): cadence-aware framework with auto/manual modes
isReminderDue now keys off doc.remindersDisabled and the effective cadence (per-doc override → template default), dropping the implicit interests.reminderEnabled gate so non-EOI docs auto-remind correctly. sendReminderIfAllowed gains an options bag — auto:true keeps the 9-16 window + cadence cooldown for the cron, auto:false bypasses both for manual UI sends. signerId targets a specific pending signer (must be next in sequential mode). 7 unit tests cover the cadence math. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
da44e8ecbe |
feat(documenso): version-aware field placement + void abstractions
Adds DOCUMENSO_API_VERSION env (default v1) plus per-port override. Introduces placeFields, placeDefaultSignatureFields, and voidDocument that hide v1 (per-field POST, pixel coords) vs v2 (bulk POST, percent + fieldMeta) differences. cancelDocument now voids in Documenso first and treats transient void failures as recoverable so the CRM stays the system of record. 16 unit specs cover dispatch, layout math, idempotent 404, and v1 pixel conversion. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
7200c31486 | feat(eoi): add Documenso template payload builder | ||
|
|
71d7daf1ae |
feat(search): index yachts and companies alongside clients
Extend the global search service to include yacht and company results using ILIKE matching on name, hull number, registration, legal name, and tax ID. Results are tenant-scoped and exclude archived rows. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
3b0421aa81 | fix(tests): use dynamic imports in portal.test.ts to avoid env validation | ||
|
|
a14dc8143c |
feat(portal): surface yachts, memberships, reservations for portal users
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
b053a6388e | feat(eoi): shared context builder + tests | ||
|
|
b1133c4e87 |
feat(reservations): service + validators + exclusivity tests
Adds the berth_reservations service covering the full lifecycle
(pending -> active -> ended/cancelled) with tenant scoping, DB-enforced
exclusivity on the idx_br_active partial unique index, and
client-or-company-member cross-checks for yacht ownership.
- validators: createPending / activate / end / cancel / list schemas
- service: createPending, activate, endReservation, cancel, getById,
listReservations — with narrow 23505/idx_br_active catch that
re-queries the conflicting active reservation
- socket events: berth_reservation:{created,activated,ended,cancelled}
- tests: unit (lifecycle, tenant, membership cross-check),
integration (concurrent-activate ConflictError + re-activate after end)
|
||
|
|
15a79e7990 |
feat(company-memberships): service + validators + tests
Adds company-membership service with six operations (add, update, end, setPrimary, listByCompany, listByClient), the corresponding Zod validators, three socket events, and a unit-test suite covering the portId-scoping rules, the unique_cm_exact conflict path, and the atomic setPrimary transaction. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
037f2544e8 | feat(companies): service + validators + unit tests |