When the in-app pathway is used for EOI templates, we now load the same
source PDF that the Documenso template uploads and fill its AcroForm
fields with values from EoiContext via pdf-lib. Field names mirror the
Documenso template's formValues keys exactly (Name, Email, Address,
Yacht Name, Length, Width, Draft, Berth Number + Lease_10 / Purchase
checkboxes), so both pathways produce equivalent legal documents — only
the renderer differs.
The form is left interactive (not flattened) so a recipient can still
adjust values before signing. Non-EOI templates (welcome letters,
acknowledgments, etc.) keep using the existing HTML→pdfme path.
Adds:
- pdf-lib direct dep
- src/lib/pdf/fill-eoi-form.ts — load + fill helpers, EOI_TEMPLATE_PDF_PATH
env override
- assets/ + README documenting the expected source PDF
- next.config outputFileTracingIncludes so the asset is bundled in the
standalone build
Tests: 8 new (4 fill-form unit + 2 source-PDF route + 2 fallback);
645/645 green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
generateAndSign now accepts a `pathway` parameter:
- `inapp` (existing): resolve in-app template -> pdfme -> MinIO -> Documenso
createDocument + sendDocument.
- `documenso-template` (new): build EOI context from interestId, assemble
the Documenso template payload, and call Documenso's
/api/v1/templates/{id}/generate-document. Documenso owns the PDF; we
still record a documents row for tracking.
Adds generateDocumentFromTemplate helper to the Documenso client and new
env vars (DOCUMENSO_TEMPLATE_ID_EOI + client/developer/approval recipient
IDs) with defaults matching the legacy flow. Covered by 6 new integration
tests (637/637 green).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Task 11.4. Extends resolveTemplate to use buildEoiContext when interestId
is provided, populating the new yacht.*, company.*, owner.* token scopes
from the shared EOI context. Legacy non-EOI templates still resolve via
direct client/berth/port lookups. Deprecated client.yachtName /
client.companyName / client.yacht*Ft tokens are removed from the catalog;
PR 12 will drop the backing columns. berth.mooringNumber is relaxed to
required:false so welcome-letter-style templates without a berth context
no longer trip the required-merge-field check.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a new per-port document_templates row of type 'eoi' containing an
HTML EOI / Letter of Intent body with {{section.field}} merge tokens
that mirror the EoiContext shape. Enables the in-app pdfme PDF path as
an alternative to the Documenso template flow.
- New getStandardEoiTemplateHtml() returns the Letter-sized HTML body
with Applicant / Yacht / Owner / Berth / Interest / Signatures blocks
- STANDARD_EOI_MERGE_FIELDS exported for resolveTemplate wiring (11.4)
- seed-data.ts inserts one document_templates row per port inside the
existing withTransaction block, between ownership transfers and
interests, using SEED_USER_ID for audit consistency
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires the billingEntityType/billingEntityId columns (added in PR 1) through
the invoice validator and service. Clients can now be billed as either a
client or a company; clientName becomes a snapshot derived from the entity
at create time.
- createInvoiceSchema: replace clientName with billingEntity {type,id}
- listInvoicesSchema: add billingEntityType/billingEntityId filters
- createInvoice: resolveBillingEntity helper (tenant-scoped; tx-aware)
falls back to entity primary email/address when not supplied
- listInvoices: honor new billing-entity filters
- updateInvoice: unchanged — billing entity is fixed after create
- invoice wizard step 1: temporary billing-entity id input (Task 10.2
replaces this with a proper picker)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Switch recommendations engine to read yacht dimensions (lengthFt, widthFt,
draftFt) from the yachts table via interest.yachtId instead of from the
deprecated client fields. Cross-tenant safety is maintained by scoping the
yacht lookup to the same portId. Falls back gracefully to null dimensions
when interest.yachtId is null or yacht is not found.
- Modified: src/lib/services/recommendations.ts — replaced client.yacht*Ft
fields with yacht table lookups via interest.yachtId
- Created: tests/integration/recommendations-yacht-dims.test.ts — 4 tests
covering happy path, null-yacht fallback, cross-tenant safety, and
dimension-based scoring
All 594 tests passing, tsc clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Restructures the public interest endpoint to create the yacht as a
first-class row (owned by the new client, or by a newly upserted
company when a company block is provided) and writes the yacht_id
onto the new interest. All writes now run inside a single
transaction instead of the previous unwrapped sequence.
The public validator gains structured `yacht` (required) and
`company` (optional) sub-objects; legacy flat fields remain in the
schema for backward compatibility but are silently ignored.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add yachtId (optional) to createInterestSchema + listInterestsSchema
(updateInterestSchema inherits it via partial() automatically).
- Add assertYachtBelongsToClient helper that accepts direct client
ownership OR company-represented clients with an active membership
in the owning company.
- createInterest + updateInterest validate yacht ownership whenever
yachtId is supplied/changed.
- changeInterestStage rejects moving out of stage=open with yachtId
null (ValidationError).
- listInterests filter supports yachtId.
- Integration tests cover all 7 paths; validator test for yachtId.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- ClientData in client-detail.tsx now reflects the stripped shape from
Task 8.2 (drop companyName/isProxy/proxy*/yacht*/berthSizeDesired) and
gains yachts / companies / activeReservations arrays.
- client-tabs.tsx: Overview trimmed (personal, contacts, source, tags);
three new count-badged tabs (Yachts, Companies, Reservations).
- New client-yachts-tab.tsx renders owned yachts + Add yacht CTA (TODO:
YachtForm preset-owner wiring for v2).
- New client-companies-tab.tsx renders memberships with Primary badge and
since-date; management still lives on the company detail page.
- New client-reservations-tab.tsx maps activeReservations into ReservationRow
shape and delegates to <ReservationList showBerth />.
- client-columns.tsx drops companyName column (TODO: add Yachts count +
Primary company once list endpoint joins those).
- client-filters.tsx drops isProxy filter.
- Wire realtime invalidations for yacht:ownership_transferred,
company_membership:added/ended, and berth_reservation:*.
Remove deprecated companyName, isProxy, proxyType, actualOwnerName, yacht
dimensions, and berthSizeDesired fields from createClientSchema and the
isProxy filter from listClientsSchema. First step of PR 8; cascading TS
errors in clients.service.ts and client-form.tsx are addressed in 8.2/8.3.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the Task 5.3 stub with a real YachtTransferDialog backed by
OwnerPicker, a date input, reason select, and notes textarea. Submits to
POST /api/v1/yachts/{id}/transfer, invalidates yacht + ownership-history
queries on success, and surfaces API errors (same-owner 400, cross-tenant
404, no-permission 403) as form-level messages. Transfer button is now
gated by PermissionGate resource="yachts" action="transfer".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements Task 5.3: server page passes yachtId to a client YachtDetail,
which fetches via TanStack Query and renders the shared DetailLayout with
Overview / Ownership History / Interests / Reservations / Notes / Tags
tabs. Header shows name, dimensions, polymorphic owner link, status badge,
and Edit / Transfer / Archive actions. Transfer is a stub dialog pending
Task 5.5; Notes tab is a placeholder because NotesList does not yet support
entityType='yachts'.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sheet-based react-hook-form + zod component for yacht CRUD.
CREATE mode uses OwnerPicker to set the yacht's owner (required
by createYachtSchema). EDIT mode hides the picker and shows a
notice directing users to the Transfer button, matching the
service-layer guard that blocks owner mutation via PATCH.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Task 5.1 of the data-model refactor. Adds:
- OwnerPicker: polymorphic combobox that toggles between client and
company autocomplete via a type switch inside the popover. Uses
/api/v1/clients/options (search=) and /api/v1/companies/autocomplete
(q=).
- YachtPicker: yacht autocomplete against /api/v1/yachts/autocomplete
with optional ownerFilter prop to scope to a given client/company.
Both components use TanStack Query with debounced (300ms) input via the
existing use-debounce hook, and apiFetch which attaches X-Port-Id.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Split seed into orchestrator (seed.ts) + per-port fixture builder
(seed-data.ts). Creates three ports (Port Nimara, Marina Azzurra,
Harbor Royale) and seeds each with a realistic multi-cardinality
dataset: 12 berths (5 available / 5 reserved / 2 sold), 8 clients
with contacts and primary addresses, 3 companies (2 active / 1
dissolved) with billing addresses, memberships exercising dual-
company ownership and ended state, 12 yachts (7 client-owned /
5 company-owned) plus matching open ownership-history rows, 3
completed ownership transfers per port (client <-> company), 15
interests spanning all pipeline stages, and 8 reservations (5
active on distinct berths / 2 ended / 1 cancelled). Seed wraps
per-port work in withTransaction and is idempotent: re-running
detects existing company rows and skips.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add Task 3.6 routes:
- POST /api/v1/berths/:id/reservations — creates a pending reservation;
the URL berthId is authoritative and any body-supplied berthId is
ignored.
- GET /api/v1/berths/:id/reservations — list filtered by URL berthId.
- GET /api/v1/berth-reservations/:id — fetch scoped to tenant.
- PATCH /api/v1/berth-reservations/:id — action-based dispatch
(activate | end | cancel) via a discriminated union. Because the
required permission depends on the action, PATCH is wrapped with
withAuth only and calls requirePermission inside the handler.
- DELETE /api/v1/berth-reservations/:id — alias for cancel (204).
Cross-tenant berths return 404 on both POST and GET via an explicit
pre-check.
Tests cover happy paths, invalid transitions, 404/400/403 cases, the
URL-vs-body berthId precedence, and per-action permission gating.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add yacht list + create routes, export RouteHandler type and inner
handlers so tests can invoke them directly with a mock AuthContext.
New tests/helpers/route-tester.ts provides makeMockCtx/makeMockRequest
reusable by subsequent Task 3.x routes.
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)
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>
Adds `listYachts`, `listYachtsForOwner`, and `autocomplete` to the
yacht service so UIs can page/filter yachts per port, look up all
yachts tied to a given client/company, and power search-as-you-type.
`listYachts` delegates to the shared port-scoped `buildListQuery`,
supporting search over name/hullNumber/registration plus ownerType,
ownerId and status filters; `autocomplete` caps at 10 results and is
tenant-scoped; `listYachtsForOwner` returns all yachts whose current
owner matches, newest first. Extends `makeYacht` factory to accept
flat `name`, `status`, `hullNumber`, `registration` overrides.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>