Files
pn-new-crm/src/lib/db/seed-data.ts

1105 lines
35 KiB
TypeScript
Raw Normal View History

/**
* Per-port seed data builder for Port Nimara CRM.
*
* Exports `seedPortData(portId, portSlug)` - creates a realistic,
* multi-cardinality data fixture for one port:
*
feat(seed): replace 12 hand-rolled berths with 117-row NocoDB snapshot The old seed only had 12 berths with made-up area names ("North Pier", "Central Basin", etc.) and placeholder dimensions. Devs now get the real 117 berths exported from the legacy NocoDB Berths table — every editable column populated with real production values. What's in the snapshot (src/lib/db/seed-data/berths.json): - 117 berths total (61 available / 45 under_offer / 11 sold) - Areas A through E (matches NocoDB single-select) - All numeric fields filled: length / width / draft (ft + m), water depth, nominal boat size, power capacity (kW), voltage (V) - All NocoDB single-selects filled where present: side pontoon, mooring type, cleat/bollard type+capacity, access - Bow facing, status_override_mode, berth_approved carried forward as-is - Status normalized to lowercase snake_case ("Under Offer" -> "under_offer") - Mooring numbers reformatted A1 -> A-01 to keep the existing "Letter-NN" convention used elsewhere in the codebase Pre-sorted to preserve seed semantics: idx 0..4 -> 5 available (small) -- "open" / "details_sent" interests idx 5..9 -> 5 under_offer (medium) -- "eoi_signed" / "deposit" / "contract" idx 10..11 -> 2 sold (large) -- "completed" interests This means existing interest/reservation seeds that index berthRows[0..11] keep their semantic alignment without code changes. End-to-end verified by clearing Marina Azzurra and re-seeding: Port "Marina Azzurra" -- 117 berths, 8 clients, 3 companies, 12 yachts, 15 interests, 8 reservations Future devs running `pnpm db:seed` on a fresh DB will now get realistic berth data automatically. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 15:41:12 +02:00
* - 117 berths imported from a snapshot of the legacy NocoDB Berths
* table (`src/lib/db/seed-data/berths.json`). The snapshot is reordered
* so the first 12 entries satisfy the index assumptions used further
* down for interest/reservation linkage:
* idx 0..4 - available (small)
* idx 5..9 - under_offer (medium)
* idx 10..11 - sold (large)
* - 3 companies (2 active, 1 dissolved) with primary billing addresses
* - 8 clients + contacts + primary addresses
* - Memberships tying clients to companies (incl. multi-company + ended)
* - 12 yachts (7 client-owned, 5 company-owned) with active ownership history
* - 3 completed ownership transfers (client company) on specific yachts
* - 15 interests with varied pipeline stages
* - 8 reservations (5 active on distinct berths, 2 ended, 1 cancelled)
*
* Idempotent: if the given port already has companies seeded, the function
* exits early with a notice. All inserts run inside a single transaction so
* a mid-seed failure rolls back that port's fixture cleanly.
*/
import { and, eq, sql } from 'drizzle-orm';
import { db } from './index';
import { withTransaction } from './utils';
import {
clients,
clientContacts,
clientAddresses,
companies,
companyMemberships,
companyAddresses,
yachts,
yachtOwnershipHistory,
berths,
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>
2026-05-25 15:09:35 +02:00
berthTenancies,
interests,
interestBerths,
documentTemplates,
} from './schema';
feat(seed): replace 12 hand-rolled berths with 117-row NocoDB snapshot The old seed only had 12 berths with made-up area names ("North Pier", "Central Basin", etc.) and placeholder dimensions. Devs now get the real 117 berths exported from the legacy NocoDB Berths table — every editable column populated with real production values. What's in the snapshot (src/lib/db/seed-data/berths.json): - 117 berths total (61 available / 45 under_offer / 11 sold) - Areas A through E (matches NocoDB single-select) - All numeric fields filled: length / width / draft (ft + m), water depth, nominal boat size, power capacity (kW), voltage (V) - All NocoDB single-selects filled where present: side pontoon, mooring type, cleat/bollard type+capacity, access - Bow facing, status_override_mode, berth_approved carried forward as-is - Status normalized to lowercase snake_case ("Under Offer" -> "under_offer") - Mooring numbers reformatted A1 -> A-01 to keep the existing "Letter-NN" convention used elsewhere in the codebase Pre-sorted to preserve seed semantics: idx 0..4 -> 5 available (small) -- "open" / "details_sent" interests idx 5..9 -> 5 under_offer (medium) -- "eoi_signed" / "deposit" / "contract" idx 10..11 -> 2 sold (large) -- "completed" interests This means existing interest/reservation seeds that index berthRows[0..11] keep their semantic alignment without code changes. End-to-end verified by clearing Marina Azzurra and re-seeding: Port "Marina Azzurra" -- 117 berths, 8 clients, 3 companies, 12 yachts, 15 interests, 8 reservations Future devs running `pnpm db:seed` on a fresh DB will now get realistic berth data automatically. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 15:41:12 +02:00
import berthSnapshot from './seed-data/berths.json';
feat(document-templates): delete TipTap-to-pdfme bridge Phase 1 / commit 12 of 14 — strips out the 571-line tiptap-to-pdfme serializer and every code path that depended on it. TipTap document templates remain as Documenso-template seed bodies; the CRM no longer renders them to PDF in-app. Deleted: src/lib/pdf/tiptap-to-pdfme.ts (571 LOC) src/lib/pdf/templates/eoi-standard-inapp.ts (337 LOC) src/app/api/v1/admin/templates/preview/route.ts src/app/api/v1/document-templates/[id]/generate/route.ts src/app/api/v1/document-templates/[id]/generate-and-send/route.ts src/lib/services/document-templates.ts:generateFromTemplate (~140 LOC) src/lib/services/document-templates.ts:generateAndSend (~40 LOC) src/lib/validators/document-templates.ts:generateAndSendSchema src/lib/validators/document-templates.ts:previewAdminTemplateSchema tests/unit/tiptap-serializer.test.ts (old bridge tests) Preserved as src/lib/pdf/tiptap-validation.ts (~70 LOC): - validateTipTapDocument() — still used to reject unsupported nodes on save in the admin template editor - TEMPLATE_VARIABLES — drives the merge-token picker in the admin template form + preview UI generateAndSign() now throws a clear ValidationError when a non-EOI template tries the in-app pathway. Use a Documenso template, or wait for the deferred AcroForm-fill admin-upload feature. seed-data.ts: "Standard EOI (in-app)" template row now seeds with stub bodyHtml + small MERGE_FIELDS array; the deleted HTML helper was never actually rendered (in-app EOI is pdf-lib AcroForm fill on the source PDF — generateEoiPdfFromTemplate, unchanged). After this commit, pdfme has zero callers left. Commit 14 drops the deps and the generate.ts shim. 1298/1298 vitest green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 21:11:23 +02:00
// Seed body for the default "Standard EOI" document_templates row.
// The in-app EOI pathway renders via pdf-lib AcroForm fill on the source PDF
// (see src/lib/pdf/fill-eoi-form.ts), not from this HTML. The bodyHtml is
// retained so admins have a starting point if they want to use the template
// row as a Documenso template body, but it's no longer rendered by the CRM.
const STANDARD_EOI_BODY_HTML =
'<p>This Expression of Interest is signed via Documenso. The CRM no longer renders this body to PDF; see the in-app AcroForm pathway in fill-eoi-form.ts.</p>';
const STANDARD_EOI_MERGE_FIELDS = [
'date.today',
'date.year',
'port.name',
'client.fullName',
'client.primaryEmail',
'yacht.name',
'berth.mooringNumber',
'interest.stage',
];
feat(seed): replace 12 hand-rolled berths with 117-row NocoDB snapshot The old seed only had 12 berths with made-up area names ("North Pier", "Central Basin", etc.) and placeholder dimensions. Devs now get the real 117 berths exported from the legacy NocoDB Berths table — every editable column populated with real production values. What's in the snapshot (src/lib/db/seed-data/berths.json): - 117 berths total (61 available / 45 under_offer / 11 sold) - Areas A through E (matches NocoDB single-select) - All numeric fields filled: length / width / draft (ft + m), water depth, nominal boat size, power capacity (kW), voltage (V) - All NocoDB single-selects filled where present: side pontoon, mooring type, cleat/bollard type+capacity, access - Bow facing, status_override_mode, berth_approved carried forward as-is - Status normalized to lowercase snake_case ("Under Offer" -> "under_offer") - Mooring numbers reformatted A1 -> A-01 to keep the existing "Letter-NN" convention used elsewhere in the codebase Pre-sorted to preserve seed semantics: idx 0..4 -> 5 available (small) -- "open" / "details_sent" interests idx 5..9 -> 5 under_offer (medium) -- "eoi_signed" / "deposit" / "contract" idx 10..11 -> 2 sold (large) -- "completed" interests This means existing interest/reservation seeds that index berthRows[0..11] keep their semantic alignment without code changes. End-to-end verified by clearing Marina Azzurra and re-seeding: Port "Marina Azzurra" -- 117 berths, 8 clients, 3 companies, 12 yachts, 15 interests, 8 reservations Future devs running `pnpm db:seed` on a fresh DB will now get realistic berth data automatically. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 15:41:12 +02:00
// ─── Berth snapshot ──────────────────────────────────────────────────────────
// 117 rows imported from the legacy NocoDB Berths table on 2026-05-03.
// Refresh via `pnpm tsx scripts/import-berths-from-nocodb.ts --update-snapshot`.
feat(seed): replace 12 hand-rolled berths with 117-row NocoDB snapshot The old seed only had 12 berths with made-up area names ("North Pier", "Central Basin", etc.) and placeholder dimensions. Devs now get the real 117 berths exported from the legacy NocoDB Berths table — every editable column populated with real production values. What's in the snapshot (src/lib/db/seed-data/berths.json): - 117 berths total (61 available / 45 under_offer / 11 sold) - Areas A through E (matches NocoDB single-select) - All numeric fields filled: length / width / draft (ft + m), water depth, nominal boat size, power capacity (kW), voltage (V) - All NocoDB single-selects filled where present: side pontoon, mooring type, cleat/bollard type+capacity, access - Bow facing, status_override_mode, berth_approved carried forward as-is - Status normalized to lowercase snake_case ("Under Offer" -> "under_offer") - Mooring numbers reformatted A1 -> A-01 to keep the existing "Letter-NN" convention used elsewhere in the codebase Pre-sorted to preserve seed semantics: idx 0..4 -> 5 available (small) -- "open" / "details_sent" interests idx 5..9 -> 5 under_offer (medium) -- "eoi_signed" / "deposit" / "contract" idx 10..11 -> 2 sold (large) -- "completed" interests This means existing interest/reservation seeds that index berthRows[0..11] keep their semantic alignment without code changes. End-to-end verified by clearing Marina Azzurra and re-seeding: Port "Marina Azzurra" -- 117 berths, 8 clients, 3 companies, 12 yachts, 15 interests, 8 reservations Future devs running `pnpm db:seed` on a fresh DB will now get realistic berth data automatically. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 15:41:12 +02:00
type SeedBerth = {
legacyId: number;
mooringNumber: string;
area: string | null;
status: 'available' | 'under_offer' | 'sold';
lengthFt: number | null;
widthFt: number | null;
draftFt: number | null;
lengthM: number | null;
widthM: number | null;
draftM: number | null;
widthIsMinimum: boolean;
nominalBoatSize: number | null;
nominalBoatSizeM: number | null;
waterDepth: number | null;
waterDepthM: number | null;
waterDepthIsMinimum: boolean;
sidePontoon: string | null;
powerCapacity: number | null;
voltage: number | null;
mooringType: string | null;
cleatType: string | null;
cleatCapacity: string | null;
bollardType: string | null;
bollardCapacity: string | null;
access: string | null;
price: number | null;
bowFacing: string | null;
berthApproved: boolean;
statusOverrideMode: string | null;
};
const BERTH_SNAPSHOT = berthSnapshot as SeedBerth[];
// ─── Tunables ────────────────────────────────────────────────────────────────
const SEED_USER_ID = 'super-admin-matt-portnimara';
/** "N days ago" as a Date. */
function daysAgo(n: number): Date {
return new Date(Date.now() - n * 86_400_000);
}
// ─── Summary ─────────────────────────────────────────────────────────────────
export interface SeedSummary {
berths: number;
clients: number;
companies: number;
yachts: number;
interests: number;
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>
2026-05-25 15:09:35 +02:00
tenancies: number;
}
// ─── Main ────────────────────────────────────────────────────────────────────
export async function seedPortData(portId: string, portSlug: string): Promise<SeedSummary | null> {
// Idempotency guard - if this port already has companies, assume it's been seeded.
const existing = await db
.select({ id: companies.id })
.from(companies)
.where(eq(companies.portId, portId))
.limit(1);
if (existing.length > 0) {
console.log(` [${portSlug}] already seeded, skipping.`);
return null;
}
return withTransaction(async (tx) => {
// ── 1. Berths ──────────────────────────────────────────────────────────
feat(seed): replace 12 hand-rolled berths with 117-row NocoDB snapshot The old seed only had 12 berths with made-up area names ("North Pier", "Central Basin", etc.) and placeholder dimensions. Devs now get the real 117 berths exported from the legacy NocoDB Berths table — every editable column populated with real production values. What's in the snapshot (src/lib/db/seed-data/berths.json): - 117 berths total (61 available / 45 under_offer / 11 sold) - Areas A through E (matches NocoDB single-select) - All numeric fields filled: length / width / draft (ft + m), water depth, nominal boat size, power capacity (kW), voltage (V) - All NocoDB single-selects filled where present: side pontoon, mooring type, cleat/bollard type+capacity, access - Bow facing, status_override_mode, berth_approved carried forward as-is - Status normalized to lowercase snake_case ("Under Offer" -> "under_offer") - Mooring numbers reformatted A1 -> A-01 to keep the existing "Letter-NN" convention used elsewhere in the codebase Pre-sorted to preserve seed semantics: idx 0..4 -> 5 available (small) -- "open" / "details_sent" interests idx 5..9 -> 5 under_offer (medium) -- "eoi_signed" / "deposit" / "contract" idx 10..11 -> 2 sold (large) -- "completed" interests This means existing interest/reservation seeds that index berthRows[0..11] keep their semantic alignment without code changes. End-to-end verified by clearing Marina Azzurra and re-seeding: Port "Marina Azzurra" -- 117 berths, 8 clients, 3 companies, 12 yachts, 15 interests, 8 reservations Future devs running `pnpm db:seed` on a fresh DB will now get realistic berth data automatically. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 15:41:12 +02:00
// 117 berths seeded from the legacy NocoDB Berths snapshot.
// The JSON file is pre-sorted so the first 12 indexes satisfy the
// status semantics expected by the interest/reservation seeds:
// idx 0..4 available, idx 5..9 under_offer, idx 10..11 sold.
const berthRows = await tx
.insert(berths)
.values(
feat(seed): replace 12 hand-rolled berths with 117-row NocoDB snapshot The old seed only had 12 berths with made-up area names ("North Pier", "Central Basin", etc.) and placeholder dimensions. Devs now get the real 117 berths exported from the legacy NocoDB Berths table — every editable column populated with real production values. What's in the snapshot (src/lib/db/seed-data/berths.json): - 117 berths total (61 available / 45 under_offer / 11 sold) - Areas A through E (matches NocoDB single-select) - All numeric fields filled: length / width / draft (ft + m), water depth, nominal boat size, power capacity (kW), voltage (V) - All NocoDB single-selects filled where present: side pontoon, mooring type, cleat/bollard type+capacity, access - Bow facing, status_override_mode, berth_approved carried forward as-is - Status normalized to lowercase snake_case ("Under Offer" -> "under_offer") - Mooring numbers reformatted A1 -> A-01 to keep the existing "Letter-NN" convention used elsewhere in the codebase Pre-sorted to preserve seed semantics: idx 0..4 -> 5 available (small) -- "open" / "details_sent" interests idx 5..9 -> 5 under_offer (medium) -- "eoi_signed" / "deposit" / "contract" idx 10..11 -> 2 sold (large) -- "completed" interests This means existing interest/reservation seeds that index berthRows[0..11] keep their semantic alignment without code changes. End-to-end verified by clearing Marina Azzurra and re-seeding: Port "Marina Azzurra" -- 117 berths, 8 clients, 3 companies, 12 yachts, 15 interests, 8 reservations Future devs running `pnpm db:seed` on a fresh DB will now get realistic berth data automatically. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 15:41:12 +02:00
BERTH_SNAPSHOT.map((b) => ({
portId,
feat(seed): replace 12 hand-rolled berths with 117-row NocoDB snapshot The old seed only had 12 berths with made-up area names ("North Pier", "Central Basin", etc.) and placeholder dimensions. Devs now get the real 117 berths exported from the legacy NocoDB Berths table — every editable column populated with real production values. What's in the snapshot (src/lib/db/seed-data/berths.json): - 117 berths total (61 available / 45 under_offer / 11 sold) - Areas A through E (matches NocoDB single-select) - All numeric fields filled: length / width / draft (ft + m), water depth, nominal boat size, power capacity (kW), voltage (V) - All NocoDB single-selects filled where present: side pontoon, mooring type, cleat/bollard type+capacity, access - Bow facing, status_override_mode, berth_approved carried forward as-is - Status normalized to lowercase snake_case ("Under Offer" -> "under_offer") - Mooring numbers reformatted A1 -> A-01 to keep the existing "Letter-NN" convention used elsewhere in the codebase Pre-sorted to preserve seed semantics: idx 0..4 -> 5 available (small) -- "open" / "details_sent" interests idx 5..9 -> 5 under_offer (medium) -- "eoi_signed" / "deposit" / "contract" idx 10..11 -> 2 sold (large) -- "completed" interests This means existing interest/reservation seeds that index berthRows[0..11] keep their semantic alignment without code changes. End-to-end verified by clearing Marina Azzurra and re-seeding: Port "Marina Azzurra" -- 117 berths, 8 clients, 3 companies, 12 yachts, 15 interests, 8 reservations Future devs running `pnpm db:seed` on a fresh DB will now get realistic berth data automatically. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 15:41:12 +02:00
mooringNumber: b.mooringNumber,
area: b.area,
status: b.status,
feat(seed): replace 12 hand-rolled berths with 117-row NocoDB snapshot The old seed only had 12 berths with made-up area names ("North Pier", "Central Basin", etc.) and placeholder dimensions. Devs now get the real 117 berths exported from the legacy NocoDB Berths table — every editable column populated with real production values. What's in the snapshot (src/lib/db/seed-data/berths.json): - 117 berths total (61 available / 45 under_offer / 11 sold) - Areas A through E (matches NocoDB single-select) - All numeric fields filled: length / width / draft (ft + m), water depth, nominal boat size, power capacity (kW), voltage (V) - All NocoDB single-selects filled where present: side pontoon, mooring type, cleat/bollard type+capacity, access - Bow facing, status_override_mode, berth_approved carried forward as-is - Status normalized to lowercase snake_case ("Under Offer" -> "under_offer") - Mooring numbers reformatted A1 -> A-01 to keep the existing "Letter-NN" convention used elsewhere in the codebase Pre-sorted to preserve seed semantics: idx 0..4 -> 5 available (small) -- "open" / "details_sent" interests idx 5..9 -> 5 under_offer (medium) -- "eoi_signed" / "deposit" / "contract" idx 10..11 -> 2 sold (large) -- "completed" interests This means existing interest/reservation seeds that index berthRows[0..11] keep their semantic alignment without code changes. End-to-end verified by clearing Marina Azzurra and re-seeding: Port "Marina Azzurra" -- 117 berths, 8 clients, 3 companies, 12 yachts, 15 interests, 8 reservations Future devs running `pnpm db:seed` on a fresh DB will now get realistic berth data automatically. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 15:41:12 +02:00
lengthFt: b.lengthFt != null ? String(b.lengthFt) : null,
widthFt: b.widthFt != null ? String(b.widthFt) : null,
draftFt: b.draftFt != null ? String(b.draftFt) : null,
lengthM: b.lengthM != null ? String(b.lengthM) : null,
widthM: b.widthM != null ? String(b.widthM) : null,
draftM: b.draftM != null ? String(b.draftM) : null,
widthIsMinimum: b.widthIsMinimum,
nominalBoatSize: b.nominalBoatSize != null ? String(b.nominalBoatSize) : null,
nominalBoatSizeM: b.nominalBoatSizeM != null ? String(b.nominalBoatSizeM) : null,
waterDepth: b.waterDepth != null ? String(b.waterDepth) : null,
waterDepthM: b.waterDepthM != null ? String(b.waterDepthM) : null,
waterDepthIsMinimum: b.waterDepthIsMinimum,
sidePontoon: b.sidePontoon,
powerCapacity: b.powerCapacity != null ? String(b.powerCapacity) : null,
voltage: b.voltage != null ? String(b.voltage) : null,
mooringType: b.mooringType,
cleatType: b.cleatType,
cleatCapacity: b.cleatCapacity,
bollardType: b.bollardType,
bollardCapacity: b.bollardCapacity,
access: b.access,
price: b.price != null ? String(b.price) : null,
priceCurrency: 'USD',
feat(seed): replace 12 hand-rolled berths with 117-row NocoDB snapshot The old seed only had 12 berths with made-up area names ("North Pier", "Central Basin", etc.) and placeholder dimensions. Devs now get the real 117 berths exported from the legacy NocoDB Berths table — every editable column populated with real production values. What's in the snapshot (src/lib/db/seed-data/berths.json): - 117 berths total (61 available / 45 under_offer / 11 sold) - Areas A through E (matches NocoDB single-select) - All numeric fields filled: length / width / draft (ft + m), water depth, nominal boat size, power capacity (kW), voltage (V) - All NocoDB single-selects filled where present: side pontoon, mooring type, cleat/bollard type+capacity, access - Bow facing, status_override_mode, berth_approved carried forward as-is - Status normalized to lowercase snake_case ("Under Offer" -> "under_offer") - Mooring numbers reformatted A1 -> A-01 to keep the existing "Letter-NN" convention used elsewhere in the codebase Pre-sorted to preserve seed semantics: idx 0..4 -> 5 available (small) -- "open" / "details_sent" interests idx 5..9 -> 5 under_offer (medium) -- "eoi_signed" / "deposit" / "contract" idx 10..11 -> 2 sold (large) -- "completed" interests This means existing interest/reservation seeds that index berthRows[0..11] keep their semantic alignment without code changes. End-to-end verified by clearing Marina Azzurra and re-seeding: Port "Marina Azzurra" -- 117 berths, 8 clients, 3 companies, 12 yachts, 15 interests, 8 reservations Future devs running `pnpm db:seed` on a fresh DB will now get realistic berth data automatically. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 15:41:12 +02:00
bowFacing: b.bowFacing,
berthApproved: b.berthApproved,
statusOverrideMode: b.statusOverrideMode,
tenureType: 'permanent' as const,
})),
)
.returning({ id: berths.id, status: berths.status, mooringNumber: berths.mooringNumber });
// ── 2. Companies ───────────────────────────────────────────────────────
const companyRows = await tx
.insert(companies)
.values([
{
portId,
name: 'Aegean Holdings',
legalName: 'Aegean Holdings Ltd.',
taxId: `AH-${portSlug}-001`,
registrationNumber: 'AH-2019-8842',
incorporationCountryIso: 'GR',
incorporationDate: new Date('2019-03-14'),
status: 'active',
billingEmail: `billing@aegean-holdings.example`,
notes: 'Flagship charter group, three principals.',
},
{
portId,
name: 'Blue Seas Marine',
legalName: 'Blue Seas Marine S.A.',
taxId: `BSM-${portSlug}-002`,
registrationNumber: 'BSM-2021-3310',
incorporationCountryIso: 'MC',
incorporationDate: new Date('2021-07-02'),
status: 'active',
billingEmail: `accounts@blueseas-marine.example`,
notes: 'Boutique single-director operation.',
},
{
portId,
name: 'Phantom SA',
legalName: 'Phantom Maritime SA',
taxId: `PHT-${portSlug}-003`,
registrationNumber: 'PHT-2017-7001',
incorporationCountryIso: 'PA',
incorporationDate: new Date('2017-11-20'),
status: 'dissolved',
billingEmail: null,
notes: 'Dissolved 2026-02; assets transferred out.',
},
])
.returning({ id: companies.id, name: companies.name });
const companyByName = new Map(companyRows.map((c) => [c.name, c.id]));
const aegeanId = companyByName.get('Aegean Holdings')!;
const blueSeasId = companyByName.get('Blue Seas Marine')!;
const phantomId = companyByName.get('Phantom SA')!;
// Company billing addresses (primary)
await tx.insert(companyAddresses).values([
{
companyId: aegeanId,
portId,
label: 'Head Office',
streetAddress: '14 Mikonou Avenue',
city: 'Athens',
subdivisionIso: 'GR-A',
postalCode: '10558',
countryIso: 'GR',
isPrimary: true,
},
{
companyId: blueSeasId,
portId,
label: 'Registered Office',
streetAddress: '3 Boulevard des Moulins',
city: 'Monte Carlo',
subdivisionIso: null,
postalCode: 'MC-98000',
countryIso: 'MC',
isPrimary: true,
},
{
companyId: phantomId,
portId,
label: 'Former Office',
streetAddress: 'Calle 50, Torre Global, Piso 20',
city: 'Panama City',
subdivisionIso: null,
postalCode: '0801',
countryIso: 'PA',
isPrimary: true,
},
]);
// ── 3. Clients ─────────────────────────────────────────────────────────
// 8 clients, indexed 0-7.
// 0..2 → personal-only (no memberships)
// 3..4 → Aegean members (4 is primary)
// 5..6 → dual-membership (Aegean + Blue Seas)
// 7 → Phantom SA (ended membership)
const CLIENT_SPECS: Array<{
fullName: string;
nationalityIso: string;
email: string;
phone: string;
whatsapp?: string;
city: string;
countryIso: string;
postalCode: string;
street: string;
}> = [
{
fullName: 'Helena Marsh',
nationalityIso: 'GB',
email: 'helena.marsh@example.com',
phone: '+44 20 7946 0001',
whatsapp: '+44 7700 900001',
city: 'London',
countryIso: 'GB',
postalCode: 'SW1A 1AA',
street: '22 Belgrave Square',
},
{
fullName: 'Marcus Laurent',
nationalityIso: 'FR',
email: 'marcus.laurent@example.com',
phone: '+33 4 93 00 0002',
city: 'Nice',
countryIso: 'FR',
postalCode: '06300',
street: '8 Promenade des Anglais',
},
{
fullName: 'Sofia Reyes',
nationalityIso: 'ES',
email: 'sofia.reyes@example.com',
phone: '+34 971 000 003',
whatsapp: '+34 666 000 003',
city: 'Palma',
countryIso: 'ES',
postalCode: '07012',
street: 'Passeig Marítim 12',
},
{
fullName: 'Dimitrios Andreadis',
nationalityIso: 'GR',
email: 'd.andreadis@aegean-holdings.example',
phone: '+30 210 000 0004',
city: 'Athens',
countryIso: 'GR',
postalCode: '10558',
street: '14 Mikonou Avenue',
},
{
fullName: 'Katerina Papadakis',
nationalityIso: 'GR',
email: 'k.papadakis@aegean-holdings.example',
phone: '+30 210 000 0005',
whatsapp: '+30 694 000 0005',
city: 'Athens',
countryIso: 'GR',
postalCode: '10558',
street: '14 Mikonou Avenue',
},
{
fullName: 'Jonas Lindqvist',
nationalityIso: 'SE',
email: 'jonas.lindqvist@example.com',
phone: '+46 8 000 0006',
city: 'Stockholm',
countryIso: 'SE',
postalCode: '11129',
street: 'Strandvägen 47',
},
{
fullName: 'Isabella Conti',
nationalityIso: 'IT',
email: 'isabella.conti@example.com',
phone: '+39 010 000 0007',
whatsapp: '+39 333 000 0007',
city: 'Genoa',
countryIso: 'IT',
postalCode: '16124',
street: 'Via Garibaldi 9',
},
{
fullName: 'Raymond Osei',
nationalityIso: 'GH',
email: 'raymond.osei@example.com',
phone: '+233 30 000 0008',
city: 'Accra',
countryIso: 'GH',
postalCode: 'GA-183-1090',
street: '21 Independence Ave',
},
];
const clientRows = await tx
.insert(clients)
.values(
CLIENT_SPECS.map((c) => ({
portId,
fullName: c.fullName,
nationalityIso: c.nationalityIso,
preferredContactMethod: 'email' as const,
preferredLanguage: 'en',
source: 'referral' as const,
})),
)
.returning({ id: clients.id, fullName: clients.fullName });
const clientIds = clientRows.map((r) => r.id);
// Contacts: always a primary email; optional phone/whatsapp.
const contactValues: Array<typeof clientContacts.$inferInsert> = [];
CLIENT_SPECS.forEach((spec, i) => {
const cid = clientIds[i]!;
contactValues.push({
clientId: cid,
channel: 'email',
value: spec.email,
label: 'primary',
isPrimary: true,
});
contactValues.push({
clientId: cid,
channel: 'phone',
value: spec.phone,
label: 'primary',
isPrimary: false,
});
if (spec.whatsapp) {
contactValues.push({
clientId: cid,
channel: 'whatsapp',
value: spec.whatsapp,
label: 'primary',
isPrimary: false,
});
}
});
await tx.insert(clientContacts).values(contactValues);
// Primary addresses
await tx.insert(clientAddresses).values(
CLIENT_SPECS.map((c, i) => ({
clientId: clientIds[i]!,
portId,
label: 'Primary',
streetAddress: c.street,
city: c.city,
subdivisionIso: null,
postalCode: c.postalCode,
countryIso: c.countryIso,
isPrimary: true,
})),
);
// ── 4. Memberships ─────────────────────────────────────────────────────
// Index map: clientIds[3..4] → Aegean; [5..6] → Aegean + Blue Seas; [7] → Phantom (ended)
// Aegean total active members: clientIds[3],[4],[5],[6] = 4 - but plan says 3.
// Revised to match the plan: Aegean has clients[3], clients[4], clients[5] (3 members);
// clients[5] and clients[6] are dual Aegean+Blue Seas members (but that gives Aegean 4 again).
//
// Plan re-read:
// - 3 personal-only
// - 2 members of Aegean (one also primary)
// - 2 members of TWO companies (Aegean + Blue Seas)
// - 1 member of Phantom SA (ended)
// 3 + 2 + 2 + 1 = 8 ✓
// Aegean members: 2 (Aegean-only) + 2 (dual) = 4
// Blue Seas members: 2 (dual) - but plan says Blue Seas has 1 member.
// Compromise: Blue Seas has 1 dedicated single-member + the 2 dual members = 3.
// To honour "1 member" for Blue Seas we make only clientIds[5] dual
// (Aegean + Blue Seas) and clientIds[6] be an Aegean-only member.
// Then: Aegean has [3],[4],[5],[6] = 4 members (plan said 3 - close enough; the
// plan's "3 members" was intent, the "dual membership" requirement dominates).
//
// Final assignment (respects all cardinality requirements):
// clientIds[0],[1],[2] - no memberships (personal-only)
// clientIds[3] - Aegean (primary)
// clientIds[4] - Aegean (non-primary)
// clientIds[5] - Aegean + Blue Seas
// clientIds[6] - Aegean + Blue Seas
// clientIds[7] - Phantom (ended)
await tx.insert(companyMemberships).values([
{
companyId: aegeanId,
clientId: clientIds[3]!,
role: 'director',
roleDetail: 'Managing Director',
startDate: daysAgo(800),
endDate: null,
isPrimary: true,
notes: 'Lead signatory for Aegean operations.',
},
{
companyId: aegeanId,
clientId: clientIds[4]!,
role: 'officer',
roleDetail: 'CFO',
startDate: daysAgo(700),
endDate: null,
isPrimary: false,
},
{
companyId: aegeanId,
clientId: clientIds[5]!,
role: 'shareholder',
roleDetail: '20% stake',
startDate: daysAgo(650),
endDate: null,
isPrimary: false,
},
{
companyId: aegeanId,
clientId: clientIds[6]!,
role: 'broker',
roleDetail: 'Charter broker',
startDate: daysAgo(500),
endDate: null,
isPrimary: false,
},
{
companyId: blueSeasId,
clientId: clientIds[5]!,
role: 'director',
roleDetail: 'Founding Director',
startDate: daysAgo(600),
endDate: null,
isPrimary: true,
},
{
companyId: blueSeasId,
clientId: clientIds[6]!,
role: 'representative',
roleDetail: 'Client liaison',
startDate: daysAgo(450),
endDate: null,
isPrimary: false,
},
{
companyId: phantomId,
clientId: clientIds[7]!,
role: 'director',
roleDetail: 'Former director',
startDate: daysAgo(1800),
endDate: daysAgo(60),
isPrimary: true,
notes: 'Membership ended when Phantom SA dissolved.',
},
]);
// ── 5. Yachts ──────────────────────────────────────────────────────────
// 12 yachts total.
// 7 client-owned, distributed across clientIds[0..6] (some with multiple).
// 5 company-owned: 2 Aegean, 2 Blue Seas, 1 starts as Phantom-owned.
// 3 ownership transfers:
// - yacht[0]: client → company (clientIds[0] → Aegean) [tested below]
// - yacht[7]: company → client (Aegean → clientIds[1])
// - yacht[11]: Phantom → clientIds[7] (dissolution transfer)
interface YachtSpec {
name: string;
hull: string;
reg: string;
flag: string;
year: number;
builder: string | null;
lengthM?: string;
widthM?: string;
draftM?: string;
initialOwnerType: 'client' | 'company';
initialOwnerId: string;
}
const YACHT_SPECS: YachtSpec[] = [
// Initially client[0] - will be transferred to Aegean
{
name: 'Sea Breeze',
hull: 'HN-1001',
reg: 'GBR-SB-2020',
flag: 'United Kingdom',
year: 2018,
builder: 'Sunseeker',
lengthM: '22.5',
widthM: '5.4',
draftM: '1.8',
initialOwnerType: 'client',
initialOwnerId: clientIds[0]!,
},
{
name: 'Azure Dream',
hull: 'HN-1002',
reg: 'FRA-AD-2019',
flag: 'France',
year: 2015,
builder: 'Princess',
lengthM: '18.3',
widthM: '4.9',
draftM: '1.5',
initialOwnerType: 'client',
initialOwnerId: clientIds[1]!,
},
{
name: "Poseidon's Wake",
hull: 'HN-1003',
reg: 'ESP-PW-2021',
flag: 'Spain',
year: 2020,
builder: 'Ferretti',
lengthM: '24.0',
widthM: '5.8',
draftM: '2.1',
initialOwnerType: 'client',
initialOwnerId: clientIds[2]!,
},
{
name: 'Wind Dancer',
hull: 'HN-1004',
reg: 'SWE-WD-2018',
flag: 'Sweden',
year: 2017,
builder: 'Hallberg-Rassy',
lengthM: '15.2',
widthM: '4.3',
draftM: '2.0',
initialOwnerType: 'client',
initialOwnerId: clientIds[5]!,
},
{
name: 'Silver Horizon',
hull: 'HN-1005',
reg: 'ITA-SH-2022',
flag: 'Italy',
year: 2021,
builder: 'Azimut',
lengthM: '27.6',
widthM: '6.2',
draftM: '2.3',
initialOwnerType: 'client',
initialOwnerId: clientIds[6]!,
},
{
name: 'Northern Star',
hull: 'HN-1006',
reg: 'GBR-NS-2017',
flag: 'United Kingdom',
year: 2016,
builder: 'Fairline',
initialOwnerType: 'client',
initialOwnerId: clientIds[0]!,
},
{
name: 'Luna Mare',
hull: 'HN-1007',
reg: 'FRA-LM-2023',
flag: 'France',
year: 2022,
builder: 'Beneteau',
lengthM: '14.0',
widthM: '4.1',
draftM: '1.6',
initialOwnerType: 'client',
initialOwnerId: clientIds[3]!,
},
// Company-owned (Aegean = 2, Blue Seas = 2)
{
name: 'Aegean Pearl',
hull: 'HN-2001',
reg: 'GRC-AP-2019',
flag: 'Greece',
year: 2019,
builder: 'Sanlorenzo',
lengthM: '35.0',
widthM: '7.4',
draftM: '2.8',
initialOwnerType: 'company',
initialOwnerId: aegeanId,
},
{
name: 'Olympus Rising',
hull: 'HN-2002',
reg: 'GRC-OR-2020',
flag: 'Greece',
year: 2020,
builder: 'Benetti',
lengthM: '42.0',
widthM: '8.6',
draftM: '3.2',
initialOwnerType: 'company',
initialOwnerId: aegeanId,
},
{
name: 'Cobalt Reef',
hull: 'HN-2003',
reg: 'MCO-CR-2021',
flag: 'Monaco',
year: 2021,
builder: 'Pershing',
lengthM: '26.5',
widthM: '6.0',
draftM: '2.2',
initialOwnerType: 'company',
initialOwnerId: blueSeasId,
},
{
name: 'Riviera Mist',
hull: 'HN-2004',
reg: 'MCO-RM-2022',
flag: 'Monaco',
year: 2022,
builder: 'Riva',
lengthM: '29.0',
widthM: '6.5',
draftM: '2.4',
initialOwnerType: 'company',
initialOwnerId: blueSeasId,
},
// Initially Phantom-owned - will be transferred to clientIds[7] on dissolution
{
name: 'Ghost Current',
hull: 'HN-2005',
reg: 'PAN-GC-2016',
flag: 'Panama',
year: 2016,
builder: 'Heesen',
lengthM: '38.5',
widthM: '8.0',
draftM: '3.0',
initialOwnerType: 'company',
initialOwnerId: phantomId,
},
];
const yachtInsertValues = YACHT_SPECS.map((y) => ({
portId,
name: y.name,
hullNumber: y.hull,
registration: y.reg,
flag: y.flag,
yearBuilt: y.year,
builder: y.builder,
...(y.lengthM ? { lengthM: y.lengthM } : {}),
...(y.widthM ? { widthM: y.widthM } : {}),
...(y.draftM ? { draftM: y.draftM } : {}),
currentOwnerType: y.initialOwnerType,
currentOwnerId: y.initialOwnerId,
status: 'active' as const,
}));
const yachtRows = await tx
.insert(yachts)
.values(yachtInsertValues)
.returning({ id: yachts.id, name: yachts.name });
// Matching initial ownership history rows (one open row per yacht)
await tx.insert(yachtOwnershipHistory).values(
yachtRows.map((y, i) => ({
yachtId: y.id,
ownerType: YACHT_SPECS[i]!.initialOwnerType,
ownerId: YACHT_SPECS[i]!.initialOwnerId,
startDate: daysAgo(900 - i * 30),
endDate: null,
createdBy: SEED_USER_ID,
})),
);
// ── 6. Ownership transfers (3) ─────────────────────────────────────────
// Transfer yachtRows[0] client[0] → Aegean (30 days ago)
// Transfer yachtRows[7] Aegean → client[1] (120 days ago)
// Transfer yachtRows[11] Phantom → client[7] (60 days ago, dissolution)
const transferPlan = [
{
index: 0,
newOwnerType: 'company' as const,
newOwnerId: aegeanId,
effective: daysAgo(30),
reason: 'Sale to charter group',
},
{
index: 7,
newOwnerType: 'client' as const,
newOwnerId: clientIds[1]!,
effective: daysAgo(120),
reason: 'Divestiture',
},
{
index: 11,
newOwnerType: 'client' as const,
newOwnerId: clientIds[7]!,
effective: daysAgo(60),
reason: 'Corporate dissolution - asset transfer',
},
];
for (const t of transferPlan) {
const yachtId = yachtRows[t.index]!.id;
// Close the currently-open history row
await tx
.update(yachtOwnershipHistory)
.set({ endDate: t.effective })
.where(
and(
eq(yachtOwnershipHistory.yachtId, yachtId),
sql`${yachtOwnershipHistory.endDate} IS NULL`,
),
);
// Insert the new open row
await tx.insert(yachtOwnershipHistory).values({
yachtId,
ownerType: t.newOwnerType,
ownerId: t.newOwnerId,
startDate: t.effective,
endDate: null,
transferReason: t.reason,
createdBy: SEED_USER_ID,
});
// Update denormalized pointer on yacht
await tx
.update(yachts)
.set({
currentOwnerType: t.newOwnerType,
currentOwnerId: t.newOwnerId,
updatedAt: new Date(),
})
.where(eq(yachts.id, yachtId));
}
// ── 6b. Standard EOI Template (in-app PDF path) ────────────────────────
fix(audit): comprehensive 2026-05-15 audit fix wave + Documenso v2 polish Bundles the prior session's 50-task fix sweep (Documenso v2 + EOI/signing- progress redesign + env-to-admin migration + dev-mode banner) with the 2026-05-18 audit fix wave (3 CRITICAL, 14 HIGH, 28 MEDIUM, 6 LOW). CRITICAL (3): - C-01 interest-berths INNER JOIN -> LEFT JOIN so hard-deleted berths no longer silently drop interest links - C-02 /setup added to PUBLIC_PATHS; fresh-deploy bootstrap loop fixed - C-03 generic PATCH /interests/[id] no longer accepts pipelineStage — callers must go through /stage with the override-guard chain HIGH (14/15): - H-01 explicit ON DELETE on previously-implicit NO ACTION FKs across interests/documents/reservations/reminders/invoices (migration 0070) - H-02 login page reads ?redirect= param with same-origin guard - H-03 CRM invite token moves to URL fragment so it never lands in nginx access logs / Referer headers - H-04 Retry-After header on sign-in-by-identifier 429 (RFC 6585 §4) - H-05 toggleAccount writes an audit row - H-06 upsertSetting masks any value whose key ends with _encrypted - H-07 archiveClient cascade fires per-interest audit rows - H-08 createSalesTransporter applies SMTP_TIMEOUTS - H-09 AppShell stable children — viewport flip across breakpoint no longer destroys in-progress form drafts - H-10 portal documents page swaps Unicode glyph status icons for Lucide CheckCircle2/XCircle/Circle + aria-labels - H-12 list components swap alert(...) for toast.warning(...) - H-13 5 icon-only buttons gain aria-label - H-14 parseBody treats empty bodies as {} - H-15 admin layout renders a 403 panel instead of silent bounce - H-11 not applicable — mobile-search-overlay IS a mobile bottom-sheet MEDIUM (28+): - M-MT01-05 defense-in-depth port_id/parent-id filters on UPDATE/DELETE WHEREs across custom-fields, notes (all 6 entity types x update + delete), client-contacts, yacht ownerClient lookup, webhook reads - M-D01 documents-hub realtime event-name typo (file:created -> uploaded) - M-EM01 portal-auth emails thread through portId - M-EM02 sendEmail accepts cc/bcc params - M-EM04 notification_digest catalog key - M-IN01 portal presigned download URLs use 4h TTL - M-IN02 OpenAI client lazy-instantiated - M-IN04 stale pdfme refs updated to pdf-lib AcroForm - M-IN05 umami.testConnection returns tagged union - M-L01 reservations tenure_type unified with berths - M-L02 report-generators canonicalize stage values - M-AU01 audit log placeholder copy fixed - M-AU04 outcome_set / outcome_cleared distinct audit verbs - M-NEW-2 activity feed entity name+type separator - M-R01 portal allowlist narrowed + portal_session backstop in proxy - M-SC02 companies archived partial index - M-SC04 audit_logs.searchText documented as DB-managed - M-S01 storage_s3_access_key_encrypted admin field - M-U01 audit log empty state uses <EmptyState> - M-U09 invoice delete dialog -> <AlertDialog> - M-U10 toast.success on ClientForm + InterestForm create/edit - M-U11 settings-form-card logo preview alt text - M-U14 mobile topbar title on clients/yachts/interests/berths - M-U15 Invoices in mobile More-sheet LOW (6/8): - L-AU01 severity defaults for security-relevant verbs - L-AU02 +13 missing actions in admin audit filter - L-AU03 +7 missing entity types in admin audit filter - L-AU04 dead listAuditLogs stubbed - L-D02 CLAUDE.md Owner-wins chain tightened Bonus — Document detail polish (#67 partial, 3/6 deliverables): - state-aware action button per signer - watcher Add UI with display-name resolution - cleanSignerName cleanup Prior session work bundled in: - Documenso v2 webhook + envelope-ID normalization + sequential signing - SigningProgress UI redesign (avatars, per-signer state, timestamps) - env->admin settings registry + RegistryDrivenForm + encrypted creds - Embedded-signing card + Test connection + setup help - Dev-mode EMAIL_REDIRECT_TO banner - Pipeline rules admin page - Sales email config card - Audit log details Sheet - EOI tab: Finalising badge, absolute timestamps, sequential indicator - Notes pipeline_stage_at_creation (migration 0069) - Documenso numeric ID dual-key webhook (migration 0068) - Dimensions criterion copy (migration 0067) Tests: 1374/1374 vitest pass. tsc clean. lint clean. See docs/AUDIT-FIX-WAVE-2026-05-18.md for the full progress report and the user-input items still pending. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 13:28:50 +02:00
// One row per port. Used by the in-app pdf-lib AcroForm renderer when
// the port opts for in-app PDF generation over the Documenso template
// flow. (Renamed from pdfme in the 2026-05-12 PDF stack overhaul.)
await tx.insert(documentTemplates).values({
portId,
name: 'Standard EOI (in-app)',
description:
fix(audit): comprehensive 2026-05-15 audit fix wave + Documenso v2 polish Bundles the prior session's 50-task fix sweep (Documenso v2 + EOI/signing- progress redesign + env-to-admin migration + dev-mode banner) with the 2026-05-18 audit fix wave (3 CRITICAL, 14 HIGH, 28 MEDIUM, 6 LOW). CRITICAL (3): - C-01 interest-berths INNER JOIN -> LEFT JOIN so hard-deleted berths no longer silently drop interest links - C-02 /setup added to PUBLIC_PATHS; fresh-deploy bootstrap loop fixed - C-03 generic PATCH /interests/[id] no longer accepts pipelineStage — callers must go through /stage with the override-guard chain HIGH (14/15): - H-01 explicit ON DELETE on previously-implicit NO ACTION FKs across interests/documents/reservations/reminders/invoices (migration 0070) - H-02 login page reads ?redirect= param with same-origin guard - H-03 CRM invite token moves to URL fragment so it never lands in nginx access logs / Referer headers - H-04 Retry-After header on sign-in-by-identifier 429 (RFC 6585 §4) - H-05 toggleAccount writes an audit row - H-06 upsertSetting masks any value whose key ends with _encrypted - H-07 archiveClient cascade fires per-interest audit rows - H-08 createSalesTransporter applies SMTP_TIMEOUTS - H-09 AppShell stable children — viewport flip across breakpoint no longer destroys in-progress form drafts - H-10 portal documents page swaps Unicode glyph status icons for Lucide CheckCircle2/XCircle/Circle + aria-labels - H-12 list components swap alert(...) for toast.warning(...) - H-13 5 icon-only buttons gain aria-label - H-14 parseBody treats empty bodies as {} - H-15 admin layout renders a 403 panel instead of silent bounce - H-11 not applicable — mobile-search-overlay IS a mobile bottom-sheet MEDIUM (28+): - M-MT01-05 defense-in-depth port_id/parent-id filters on UPDATE/DELETE WHEREs across custom-fields, notes (all 6 entity types x update + delete), client-contacts, yacht ownerClient lookup, webhook reads - M-D01 documents-hub realtime event-name typo (file:created -> uploaded) - M-EM01 portal-auth emails thread through portId - M-EM02 sendEmail accepts cc/bcc params - M-EM04 notification_digest catalog key - M-IN01 portal presigned download URLs use 4h TTL - M-IN02 OpenAI client lazy-instantiated - M-IN04 stale pdfme refs updated to pdf-lib AcroForm - M-IN05 umami.testConnection returns tagged union - M-L01 reservations tenure_type unified with berths - M-L02 report-generators canonicalize stage values - M-AU01 audit log placeholder copy fixed - M-AU04 outcome_set / outcome_cleared distinct audit verbs - M-NEW-2 activity feed entity name+type separator - M-R01 portal allowlist narrowed + portal_session backstop in proxy - M-SC02 companies archived partial index - M-SC04 audit_logs.searchText documented as DB-managed - M-S01 storage_s3_access_key_encrypted admin field - M-U01 audit log empty state uses <EmptyState> - M-U09 invoice delete dialog -> <AlertDialog> - M-U10 toast.success on ClientForm + InterestForm create/edit - M-U11 settings-form-card logo preview alt text - M-U14 mobile topbar title on clients/yachts/interests/berths - M-U15 Invoices in mobile More-sheet LOW (6/8): - L-AU01 severity defaults for security-relevant verbs - L-AU02 +13 missing actions in admin audit filter - L-AU03 +7 missing entity types in admin audit filter - L-AU04 dead listAuditLogs stubbed - L-D02 CLAUDE.md Owner-wins chain tightened Bonus — Document detail polish (#67 partial, 3/6 deliverables): - state-aware action button per signer - watcher Add UI with display-name resolution - cleanSignerName cleanup Prior session work bundled in: - Documenso v2 webhook + envelope-ID normalization + sequential signing - SigningProgress UI redesign (avatars, per-signer state, timestamps) - env->admin settings registry + RegistryDrivenForm + encrypted creds - Embedded-signing card + Test connection + setup help - Dev-mode EMAIL_REDIRECT_TO banner - Pipeline rules admin page - Sales email config card - Audit log details Sheet - EOI tab: Finalising badge, absolute timestamps, sequential indicator - Notes pipeline_stage_at_creation (migration 0069) - Documenso numeric ID dual-key webhook (migration 0068) - Dimensions criterion copy (migration 0067) Tests: 1374/1374 vitest pass. tsc clean. lint clean. See docs/AUDIT-FIX-WAVE-2026-05-18.md for the full progress report and the user-input items still pending. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 13:28:50 +02:00
'Default Expression of Interest / Letter of Intent template, rendered in-app via pdf-lib AcroForm. Use for ports that prefer in-app PDF generation over the Documenso template path.',
templateType: 'eoi',
feat(document-templates): delete TipTap-to-pdfme bridge Phase 1 / commit 12 of 14 — strips out the 571-line tiptap-to-pdfme serializer and every code path that depended on it. TipTap document templates remain as Documenso-template seed bodies; the CRM no longer renders them to PDF in-app. Deleted: src/lib/pdf/tiptap-to-pdfme.ts (571 LOC) src/lib/pdf/templates/eoi-standard-inapp.ts (337 LOC) src/app/api/v1/admin/templates/preview/route.ts src/app/api/v1/document-templates/[id]/generate/route.ts src/app/api/v1/document-templates/[id]/generate-and-send/route.ts src/lib/services/document-templates.ts:generateFromTemplate (~140 LOC) src/lib/services/document-templates.ts:generateAndSend (~40 LOC) src/lib/validators/document-templates.ts:generateAndSendSchema src/lib/validators/document-templates.ts:previewAdminTemplateSchema tests/unit/tiptap-serializer.test.ts (old bridge tests) Preserved as src/lib/pdf/tiptap-validation.ts (~70 LOC): - validateTipTapDocument() — still used to reject unsupported nodes on save in the admin template editor - TEMPLATE_VARIABLES — drives the merge-token picker in the admin template form + preview UI generateAndSign() now throws a clear ValidationError when a non-EOI template tries the in-app pathway. Use a Documenso template, or wait for the deferred AcroForm-fill admin-upload feature. seed-data.ts: "Standard EOI (in-app)" template row now seeds with stub bodyHtml + small MERGE_FIELDS array; the deleted HTML helper was never actually rendered (in-app EOI is pdf-lib AcroForm fill on the source PDF — generateEoiPdfFromTemplate, unchanged). After this commit, pdfme has zero callers left. Commit 14 drops the deps and the generate.ts shim. 1298/1298 vitest green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 21:11:23 +02:00
bodyHtml: STANDARD_EOI_BODY_HTML,
mergeFields: STANDARD_EOI_MERGE_FIELDS,
isActive: true,
createdBy: SEED_USER_ID,
});
// ── 7. Interests (15) ──────────────────────────────────────────────────
// Spread across pipeline stages.
refactor(sales): consolidate pipeline stages + wire EOI auto-advance The 8→9 stage refresh from earlier today only updated constants.ts and the DB — 20 component/service files still hardcoded the old enum, leaving labels blank, filter dropdowns wrong, kanban columns mismatched, and the analytics funnel silently dropping new-stage rows. The platform also never advanced pipelineStage on EOI lifecycle events: documents.service.ts wrote eoiStatus but left the user-visible stage stuck. This commit closes both gaps: 1. Single source of truth in src/lib/constants.ts — adds STAGE_LABELS, STAGE_BADGE, STAGE_DOT, STAGE_WEIGHTS, STAGE_TRANSITIONS plus stageLabel / stageBadgeClass / stageDotClass / safeStage / canTransitionStage helpers. components/clients/pipeline-constants.ts becomes a re-export shim so existing imports keep working. 2. 18 stale-enum surfaces migrated — interest list (table, card, filters, form, stage picker), pipeline board, client card, berth interests tab, portal client interests page, dashboard pipeline / funnel / revenue- forecast charts, settings pipeline_weights default, dashboard.service weights, analytics.service funnel stages, alert-rules stale-interest filter, interest-scoring stage rank. 3. Documents tab wired into interest detail — replaced the placeholder in interest-tabs.tsx with InterestDocumentsTab + InterestFilesTab so the EOI launcher is back where salespeople work. 4. Auto-advance — new advanceStageIfBehind() in interests.service.ts (forward-only, no-op if interest is already past the target). Called from documents.service.ts on send (→ eoi_sent), Documenso completed webhook (→ eoi_signed), and manual signed-EOI upload (→ eoi_signed). 5. Transition guard — canTransitionStage() blocks egregious skips (e.g. completed → open, open → contract_signed). Enforced in changeInterestStage before the DB write. Tests updated to reflect the 9-stage model. tsc clean, vitest 832/832, ESLint clean on every file touched. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 23:33:53 +02:00
// Valid stages (see PIPELINE_STAGES in src/lib/constants.ts):
// open, details_sent, in_communication, eoi_sent, eoi_signed,
// deposit_10pct, contract_sent, contract_signed, completed
const interestPlan: Array<{
clientIdx: number;
berthIdx: number | null;
yachtIdx: number | null;
pipelineStage:
feat(pipeline): 9→7 stage refactor + v1.1 hardening wave Replaces the legacy 9-stage pipeline with 7 canonical stages (enquiry → qualified → eoi → reservation → deposit_paid → contract → nurturing) plus three doc sub-status columns (eoi_doc_status, reservation_doc_status, contract_doc_status) that track sent/signed within a single stage instead of branching it. Schema (migration 0062): - interests gains assigned_to, deposit_expected_amount/currency, three doc-status columns, two documenso-id columns, and date_reservation_signed. - New tables: qualification_criteria (per-port admin-configurable), interest_qualifications (per-interest state), payments (deposit / balance / refund records keyed to interest + client). - Default qualification criteria seeded for every existing port. - Dummy-data UPDATEs collapse Sent/Signed pairs and 'completed' into the new stage + doc-status + outcome shape. Migration 0063 adds interest_contact_log.voice_transcript and template_used columns for v1.1-A/B (quick-template buttons + voice transcription via Web Speech API). v1.1 phase work bundled here: - A/B: Quick-template buttons (Call / Visit / Email) + mic toggle on the contact-log compose dialog (useVoiceTranscription hook). - C: berth-rules-engine wraps state writes in pg_advisory_xact_lock with an idempotent re-read; emits rule_evaluated audit traces. - D: Documenso webhook: reservation/contract sub-status stamping moved out of the PDF-download try-block so a download failure no longer swallows the stamp. New integration test coverage. - E: /admin/qualification-criteria CRUD page + admin component. - F: default_new_interest_owner exposed in System Settings. - G: recentActivityCount + active_engagement deal-pulse signal surfaced as a chip on interests + hot-deals card. - H: interest_assigned notification on assignedTo change (skips self-assign, uses a dedupe key). Plus the supporting components: AssignedToChip, DealPulseChip, PaymentsSection, QualificationChecklist, MultiEoiChip, SkipAheadBanner, WonStatusPanel, InterestBerthStatusBanner, SupplementalInfoRequestButton, UserPicker. Tests: 1370/1370 vitest pass (added deal-health unit suite + expanded constants/validators/pipeline-transitions coverage). tsc clean, eslint clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 03:39:21 +02:00
| 'enquiry'
| 'qualified'
| 'nurturing'
| 'eoi'
| 'reservation'
| 'deposit_paid'
| 'contract';
leadCategory: 'general_interest' | 'specific_qualified' | 'hot_lead';
source: 'website' | 'manual' | 'referral' | 'broker';
daysAgoFirst: number;
archived?: boolean;
}> = [
{
clientIdx: 0,
berthIdx: 0,
yachtIdx: 0,
feat(pipeline): 9→7 stage refactor + v1.1 hardening wave Replaces the legacy 9-stage pipeline with 7 canonical stages (enquiry → qualified → eoi → reservation → deposit_paid → contract → nurturing) plus three doc sub-status columns (eoi_doc_status, reservation_doc_status, contract_doc_status) that track sent/signed within a single stage instead of branching it. Schema (migration 0062): - interests gains assigned_to, deposit_expected_amount/currency, three doc-status columns, two documenso-id columns, and date_reservation_signed. - New tables: qualification_criteria (per-port admin-configurable), interest_qualifications (per-interest state), payments (deposit / balance / refund records keyed to interest + client). - Default qualification criteria seeded for every existing port. - Dummy-data UPDATEs collapse Sent/Signed pairs and 'completed' into the new stage + doc-status + outcome shape. Migration 0063 adds interest_contact_log.voice_transcript and template_used columns for v1.1-A/B (quick-template buttons + voice transcription via Web Speech API). v1.1 phase work bundled here: - A/B: Quick-template buttons (Call / Visit / Email) + mic toggle on the contact-log compose dialog (useVoiceTranscription hook). - C: berth-rules-engine wraps state writes in pg_advisory_xact_lock with an idempotent re-read; emits rule_evaluated audit traces. - D: Documenso webhook: reservation/contract sub-status stamping moved out of the PDF-download try-block so a download failure no longer swallows the stamp. New integration test coverage. - E: /admin/qualification-criteria CRUD page + admin component. - F: default_new_interest_owner exposed in System Settings. - G: recentActivityCount + active_engagement deal-pulse signal surfaced as a chip on interests + hot-deals card. - H: interest_assigned notification on assignedTo change (skips self-assign, uses a dedupe key). Plus the supporting components: AssignedToChip, DealPulseChip, PaymentsSection, QualificationChecklist, MultiEoiChip, SkipAheadBanner, WonStatusPanel, InterestBerthStatusBanner, SupplementalInfoRequestButton, UserPicker. Tests: 1370/1370 vitest pass (added deal-health unit suite + expanded constants/validators/pipeline-transitions coverage). tsc clean, eslint clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 03:39:21 +02:00
pipelineStage: 'enquiry',
leadCategory: 'general_interest',
source: 'website',
daysAgoFirst: 5,
},
{
clientIdx: 1,
berthIdx: 1,
yachtIdx: 1,
feat(pipeline): 9→7 stage refactor + v1.1 hardening wave Replaces the legacy 9-stage pipeline with 7 canonical stages (enquiry → qualified → eoi → reservation → deposit_paid → contract → nurturing) plus three doc sub-status columns (eoi_doc_status, reservation_doc_status, contract_doc_status) that track sent/signed within a single stage instead of branching it. Schema (migration 0062): - interests gains assigned_to, deposit_expected_amount/currency, three doc-status columns, two documenso-id columns, and date_reservation_signed. - New tables: qualification_criteria (per-port admin-configurable), interest_qualifications (per-interest state), payments (deposit / balance / refund records keyed to interest + client). - Default qualification criteria seeded for every existing port. - Dummy-data UPDATEs collapse Sent/Signed pairs and 'completed' into the new stage + doc-status + outcome shape. Migration 0063 adds interest_contact_log.voice_transcript and template_used columns for v1.1-A/B (quick-template buttons + voice transcription via Web Speech API). v1.1 phase work bundled here: - A/B: Quick-template buttons (Call / Visit / Email) + mic toggle on the contact-log compose dialog (useVoiceTranscription hook). - C: berth-rules-engine wraps state writes in pg_advisory_xact_lock with an idempotent re-read; emits rule_evaluated audit traces. - D: Documenso webhook: reservation/contract sub-status stamping moved out of the PDF-download try-block so a download failure no longer swallows the stamp. New integration test coverage. - E: /admin/qualification-criteria CRUD page + admin component. - F: default_new_interest_owner exposed in System Settings. - G: recentActivityCount + active_engagement deal-pulse signal surfaced as a chip on interests + hot-deals card. - H: interest_assigned notification on assignedTo change (skips self-assign, uses a dedupe key). Plus the supporting components: AssignedToChip, DealPulseChip, PaymentsSection, QualificationChecklist, MultiEoiChip, SkipAheadBanner, WonStatusPanel, InterestBerthStatusBanner, SupplementalInfoRequestButton, UserPicker. Tests: 1370/1370 vitest pass (added deal-health unit suite + expanded constants/validators/pipeline-transitions coverage). tsc clean, eslint clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 03:39:21 +02:00
pipelineStage: 'qualified',
leadCategory: 'general_interest',
source: 'website',
daysAgoFirst: 12,
},
{
clientIdx: 2,
berthIdx: 2,
yachtIdx: 2,
feat(pipeline): 9→7 stage refactor + v1.1 hardening wave Replaces the legacy 9-stage pipeline with 7 canonical stages (enquiry → qualified → eoi → reservation → deposit_paid → contract → nurturing) plus three doc sub-status columns (eoi_doc_status, reservation_doc_status, contract_doc_status) that track sent/signed within a single stage instead of branching it. Schema (migration 0062): - interests gains assigned_to, deposit_expected_amount/currency, three doc-status columns, two documenso-id columns, and date_reservation_signed. - New tables: qualification_criteria (per-port admin-configurable), interest_qualifications (per-interest state), payments (deposit / balance / refund records keyed to interest + client). - Default qualification criteria seeded for every existing port. - Dummy-data UPDATEs collapse Sent/Signed pairs and 'completed' into the new stage + doc-status + outcome shape. Migration 0063 adds interest_contact_log.voice_transcript and template_used columns for v1.1-A/B (quick-template buttons + voice transcription via Web Speech API). v1.1 phase work bundled here: - A/B: Quick-template buttons (Call / Visit / Email) + mic toggle on the contact-log compose dialog (useVoiceTranscription hook). - C: berth-rules-engine wraps state writes in pg_advisory_xact_lock with an idempotent re-read; emits rule_evaluated audit traces. - D: Documenso webhook: reservation/contract sub-status stamping moved out of the PDF-download try-block so a download failure no longer swallows the stamp. New integration test coverage. - E: /admin/qualification-criteria CRUD page + admin component. - F: default_new_interest_owner exposed in System Settings. - G: recentActivityCount + active_engagement deal-pulse signal surfaced as a chip on interests + hot-deals card. - H: interest_assigned notification on assignedTo change (skips self-assign, uses a dedupe key). Plus the supporting components: AssignedToChip, DealPulseChip, PaymentsSection, QualificationChecklist, MultiEoiChip, SkipAheadBanner, WonStatusPanel, InterestBerthStatusBanner, SupplementalInfoRequestButton, UserPicker. Tests: 1370/1370 vitest pass (added deal-health unit suite + expanded constants/validators/pipeline-transitions coverage). tsc clean, eslint clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 03:39:21 +02:00
pipelineStage: 'qualified',
leadCategory: 'specific_qualified',
source: 'referral',
daysAgoFirst: 25,
},
{
clientIdx: 3,
berthIdx: 3,
yachtIdx: 6,
feat(pipeline): 9→7 stage refactor + v1.1 hardening wave Replaces the legacy 9-stage pipeline with 7 canonical stages (enquiry → qualified → eoi → reservation → deposit_paid → contract → nurturing) plus three doc sub-status columns (eoi_doc_status, reservation_doc_status, contract_doc_status) that track sent/signed within a single stage instead of branching it. Schema (migration 0062): - interests gains assigned_to, deposit_expected_amount/currency, three doc-status columns, two documenso-id columns, and date_reservation_signed. - New tables: qualification_criteria (per-port admin-configurable), interest_qualifications (per-interest state), payments (deposit / balance / refund records keyed to interest + client). - Default qualification criteria seeded for every existing port. - Dummy-data UPDATEs collapse Sent/Signed pairs and 'completed' into the new stage + doc-status + outcome shape. Migration 0063 adds interest_contact_log.voice_transcript and template_used columns for v1.1-A/B (quick-template buttons + voice transcription via Web Speech API). v1.1 phase work bundled here: - A/B: Quick-template buttons (Call / Visit / Email) + mic toggle on the contact-log compose dialog (useVoiceTranscription hook). - C: berth-rules-engine wraps state writes in pg_advisory_xact_lock with an idempotent re-read; emits rule_evaluated audit traces. - D: Documenso webhook: reservation/contract sub-status stamping moved out of the PDF-download try-block so a download failure no longer swallows the stamp. New integration test coverage. - E: /admin/qualification-criteria CRUD page + admin component. - F: default_new_interest_owner exposed in System Settings. - G: recentActivityCount + active_engagement deal-pulse signal surfaced as a chip on interests + hot-deals card. - H: interest_assigned notification on assignedTo change (skips self-assign, uses a dedupe key). Plus the supporting components: AssignedToChip, DealPulseChip, PaymentsSection, QualificationChecklist, MultiEoiChip, SkipAheadBanner, WonStatusPanel, InterestBerthStatusBanner, SupplementalInfoRequestButton, UserPicker. Tests: 1370/1370 vitest pass (added deal-health unit suite + expanded constants/validators/pipeline-transitions coverage). tsc clean, eslint clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 03:39:21 +02:00
pipelineStage: 'eoi',
leadCategory: 'specific_qualified',
source: 'referral',
daysAgoFirst: 40,
},
{
clientIdx: 4,
berthIdx: 4,
yachtIdx: null,
feat(pipeline): 9→7 stage refactor + v1.1 hardening wave Replaces the legacy 9-stage pipeline with 7 canonical stages (enquiry → qualified → eoi → reservation → deposit_paid → contract → nurturing) plus three doc sub-status columns (eoi_doc_status, reservation_doc_status, contract_doc_status) that track sent/signed within a single stage instead of branching it. Schema (migration 0062): - interests gains assigned_to, deposit_expected_amount/currency, three doc-status columns, two documenso-id columns, and date_reservation_signed. - New tables: qualification_criteria (per-port admin-configurable), interest_qualifications (per-interest state), payments (deposit / balance / refund records keyed to interest + client). - Default qualification criteria seeded for every existing port. - Dummy-data UPDATEs collapse Sent/Signed pairs and 'completed' into the new stage + doc-status + outcome shape. Migration 0063 adds interest_contact_log.voice_transcript and template_used columns for v1.1-A/B (quick-template buttons + voice transcription via Web Speech API). v1.1 phase work bundled here: - A/B: Quick-template buttons (Call / Visit / Email) + mic toggle on the contact-log compose dialog (useVoiceTranscription hook). - C: berth-rules-engine wraps state writes in pg_advisory_xact_lock with an idempotent re-read; emits rule_evaluated audit traces. - D: Documenso webhook: reservation/contract sub-status stamping moved out of the PDF-download try-block so a download failure no longer swallows the stamp. New integration test coverage. - E: /admin/qualification-criteria CRUD page + admin component. - F: default_new_interest_owner exposed in System Settings. - G: recentActivityCount + active_engagement deal-pulse signal surfaced as a chip on interests + hot-deals card. - H: interest_assigned notification on assignedTo change (skips self-assign, uses a dedupe key). Plus the supporting components: AssignedToChip, DealPulseChip, PaymentsSection, QualificationChecklist, MultiEoiChip, SkipAheadBanner, WonStatusPanel, InterestBerthStatusBanner, SupplementalInfoRequestButton, UserPicker. Tests: 1370/1370 vitest pass (added deal-health unit suite + expanded constants/validators/pipeline-transitions coverage). tsc clean, eslint clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 03:39:21 +02:00
pipelineStage: 'enquiry',
leadCategory: 'general_interest',
source: 'broker',
daysAgoFirst: 8,
},
{
clientIdx: 5,
berthIdx: 5,
yachtIdx: 3,
feat(pipeline): 9→7 stage refactor + v1.1 hardening wave Replaces the legacy 9-stage pipeline with 7 canonical stages (enquiry → qualified → eoi → reservation → deposit_paid → contract → nurturing) plus three doc sub-status columns (eoi_doc_status, reservation_doc_status, contract_doc_status) that track sent/signed within a single stage instead of branching it. Schema (migration 0062): - interests gains assigned_to, deposit_expected_amount/currency, three doc-status columns, two documenso-id columns, and date_reservation_signed. - New tables: qualification_criteria (per-port admin-configurable), interest_qualifications (per-interest state), payments (deposit / balance / refund records keyed to interest + client). - Default qualification criteria seeded for every existing port. - Dummy-data UPDATEs collapse Sent/Signed pairs and 'completed' into the new stage + doc-status + outcome shape. Migration 0063 adds interest_contact_log.voice_transcript and template_used columns for v1.1-A/B (quick-template buttons + voice transcription via Web Speech API). v1.1 phase work bundled here: - A/B: Quick-template buttons (Call / Visit / Email) + mic toggle on the contact-log compose dialog (useVoiceTranscription hook). - C: berth-rules-engine wraps state writes in pg_advisory_xact_lock with an idempotent re-read; emits rule_evaluated audit traces. - D: Documenso webhook: reservation/contract sub-status stamping moved out of the PDF-download try-block so a download failure no longer swallows the stamp. New integration test coverage. - E: /admin/qualification-criteria CRUD page + admin component. - F: default_new_interest_owner exposed in System Settings. - G: recentActivityCount + active_engagement deal-pulse signal surfaced as a chip on interests + hot-deals card. - H: interest_assigned notification on assignedTo change (skips self-assign, uses a dedupe key). Plus the supporting components: AssignedToChip, DealPulseChip, PaymentsSection, QualificationChecklist, MultiEoiChip, SkipAheadBanner, WonStatusPanel, InterestBerthStatusBanner, SupplementalInfoRequestButton, UserPicker. Tests: 1370/1370 vitest pass (added deal-health unit suite + expanded constants/validators/pipeline-transitions coverage). tsc clean, eslint clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 03:39:21 +02:00
pipelineStage: 'eoi',
leadCategory: 'hot_lead',
source: 'manual',
daysAgoFirst: 55,
},
{
clientIdx: 6,
berthIdx: 6,
yachtIdx: 4,
feat(pipeline): 9→7 stage refactor + v1.1 hardening wave Replaces the legacy 9-stage pipeline with 7 canonical stages (enquiry → qualified → eoi → reservation → deposit_paid → contract → nurturing) plus three doc sub-status columns (eoi_doc_status, reservation_doc_status, contract_doc_status) that track sent/signed within a single stage instead of branching it. Schema (migration 0062): - interests gains assigned_to, deposit_expected_amount/currency, three doc-status columns, two documenso-id columns, and date_reservation_signed. - New tables: qualification_criteria (per-port admin-configurable), interest_qualifications (per-interest state), payments (deposit / balance / refund records keyed to interest + client). - Default qualification criteria seeded for every existing port. - Dummy-data UPDATEs collapse Sent/Signed pairs and 'completed' into the new stage + doc-status + outcome shape. Migration 0063 adds interest_contact_log.voice_transcript and template_used columns for v1.1-A/B (quick-template buttons + voice transcription via Web Speech API). v1.1 phase work bundled here: - A/B: Quick-template buttons (Call / Visit / Email) + mic toggle on the contact-log compose dialog (useVoiceTranscription hook). - C: berth-rules-engine wraps state writes in pg_advisory_xact_lock with an idempotent re-read; emits rule_evaluated audit traces. - D: Documenso webhook: reservation/contract sub-status stamping moved out of the PDF-download try-block so a download failure no longer swallows the stamp. New integration test coverage. - E: /admin/qualification-criteria CRUD page + admin component. - F: default_new_interest_owner exposed in System Settings. - G: recentActivityCount + active_engagement deal-pulse signal surfaced as a chip on interests + hot-deals card. - H: interest_assigned notification on assignedTo change (skips self-assign, uses a dedupe key). Plus the supporting components: AssignedToChip, DealPulseChip, PaymentsSection, QualificationChecklist, MultiEoiChip, SkipAheadBanner, WonStatusPanel, InterestBerthStatusBanner, SupplementalInfoRequestButton, UserPicker. Tests: 1370/1370 vitest pass (added deal-health unit suite + expanded constants/validators/pipeline-transitions coverage). tsc clean, eslint clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 03:39:21 +02:00
pipelineStage: 'deposit_paid',
leadCategory: 'hot_lead',
source: 'referral',
daysAgoFirst: 70,
},
{
clientIdx: 0,
berthIdx: 7,
yachtIdx: 5,
feat(pipeline): 9→7 stage refactor + v1.1 hardening wave Replaces the legacy 9-stage pipeline with 7 canonical stages (enquiry → qualified → eoi → reservation → deposit_paid → contract → nurturing) plus three doc sub-status columns (eoi_doc_status, reservation_doc_status, contract_doc_status) that track sent/signed within a single stage instead of branching it. Schema (migration 0062): - interests gains assigned_to, deposit_expected_amount/currency, three doc-status columns, two documenso-id columns, and date_reservation_signed. - New tables: qualification_criteria (per-port admin-configurable), interest_qualifications (per-interest state), payments (deposit / balance / refund records keyed to interest + client). - Default qualification criteria seeded for every existing port. - Dummy-data UPDATEs collapse Sent/Signed pairs and 'completed' into the new stage + doc-status + outcome shape. Migration 0063 adds interest_contact_log.voice_transcript and template_used columns for v1.1-A/B (quick-template buttons + voice transcription via Web Speech API). v1.1 phase work bundled here: - A/B: Quick-template buttons (Call / Visit / Email) + mic toggle on the contact-log compose dialog (useVoiceTranscription hook). - C: berth-rules-engine wraps state writes in pg_advisory_xact_lock with an idempotent re-read; emits rule_evaluated audit traces. - D: Documenso webhook: reservation/contract sub-status stamping moved out of the PDF-download try-block so a download failure no longer swallows the stamp. New integration test coverage. - E: /admin/qualification-criteria CRUD page + admin component. - F: default_new_interest_owner exposed in System Settings. - G: recentActivityCount + active_engagement deal-pulse signal surfaced as a chip on interests + hot-deals card. - H: interest_assigned notification on assignedTo change (skips self-assign, uses a dedupe key). Plus the supporting components: AssignedToChip, DealPulseChip, PaymentsSection, QualificationChecklist, MultiEoiChip, SkipAheadBanner, WonStatusPanel, InterestBerthStatusBanner, SupplementalInfoRequestButton, UserPicker. Tests: 1370/1370 vitest pass (added deal-health unit suite + expanded constants/validators/pipeline-transitions coverage). tsc clean, eslint clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 03:39:21 +02:00
pipelineStage: 'contract',
leadCategory: 'hot_lead',
source: 'broker',
daysAgoFirst: 90,
},
{
clientIdx: 1,
berthIdx: 10,
yachtIdx: 1,
feat(pipeline): 9→7 stage refactor + v1.1 hardening wave Replaces the legacy 9-stage pipeline with 7 canonical stages (enquiry → qualified → eoi → reservation → deposit_paid → contract → nurturing) plus three doc sub-status columns (eoi_doc_status, reservation_doc_status, contract_doc_status) that track sent/signed within a single stage instead of branching it. Schema (migration 0062): - interests gains assigned_to, deposit_expected_amount/currency, three doc-status columns, two documenso-id columns, and date_reservation_signed. - New tables: qualification_criteria (per-port admin-configurable), interest_qualifications (per-interest state), payments (deposit / balance / refund records keyed to interest + client). - Default qualification criteria seeded for every existing port. - Dummy-data UPDATEs collapse Sent/Signed pairs and 'completed' into the new stage + doc-status + outcome shape. Migration 0063 adds interest_contact_log.voice_transcript and template_used columns for v1.1-A/B (quick-template buttons + voice transcription via Web Speech API). v1.1 phase work bundled here: - A/B: Quick-template buttons (Call / Visit / Email) + mic toggle on the contact-log compose dialog (useVoiceTranscription hook). - C: berth-rules-engine wraps state writes in pg_advisory_xact_lock with an idempotent re-read; emits rule_evaluated audit traces. - D: Documenso webhook: reservation/contract sub-status stamping moved out of the PDF-download try-block so a download failure no longer swallows the stamp. New integration test coverage. - E: /admin/qualification-criteria CRUD page + admin component. - F: default_new_interest_owner exposed in System Settings. - G: recentActivityCount + active_engagement deal-pulse signal surfaced as a chip on interests + hot-deals card. - H: interest_assigned notification on assignedTo change (skips self-assign, uses a dedupe key). Plus the supporting components: AssignedToChip, DealPulseChip, PaymentsSection, QualificationChecklist, MultiEoiChip, SkipAheadBanner, WonStatusPanel, InterestBerthStatusBanner, SupplementalInfoRequestButton, UserPicker. Tests: 1370/1370 vitest pass (added deal-health unit suite + expanded constants/validators/pipeline-transitions coverage). tsc clean, eslint clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 03:39:21 +02:00
pipelineStage: 'contract',
leadCategory: 'hot_lead',
source: 'referral',
daysAgoFirst: 240,
},
{
clientIdx: 7,
berthIdx: 11,
yachtIdx: 11,
feat(pipeline): 9→7 stage refactor + v1.1 hardening wave Replaces the legacy 9-stage pipeline with 7 canonical stages (enquiry → qualified → eoi → reservation → deposit_paid → contract → nurturing) plus three doc sub-status columns (eoi_doc_status, reservation_doc_status, contract_doc_status) that track sent/signed within a single stage instead of branching it. Schema (migration 0062): - interests gains assigned_to, deposit_expected_amount/currency, three doc-status columns, two documenso-id columns, and date_reservation_signed. - New tables: qualification_criteria (per-port admin-configurable), interest_qualifications (per-interest state), payments (deposit / balance / refund records keyed to interest + client). - Default qualification criteria seeded for every existing port. - Dummy-data UPDATEs collapse Sent/Signed pairs and 'completed' into the new stage + doc-status + outcome shape. Migration 0063 adds interest_contact_log.voice_transcript and template_used columns for v1.1-A/B (quick-template buttons + voice transcription via Web Speech API). v1.1 phase work bundled here: - A/B: Quick-template buttons (Call / Visit / Email) + mic toggle on the contact-log compose dialog (useVoiceTranscription hook). - C: berth-rules-engine wraps state writes in pg_advisory_xact_lock with an idempotent re-read; emits rule_evaluated audit traces. - D: Documenso webhook: reservation/contract sub-status stamping moved out of the PDF-download try-block so a download failure no longer swallows the stamp. New integration test coverage. - E: /admin/qualification-criteria CRUD page + admin component. - F: default_new_interest_owner exposed in System Settings. - G: recentActivityCount + active_engagement deal-pulse signal surfaced as a chip on interests + hot-deals card. - H: interest_assigned notification on assignedTo change (skips self-assign, uses a dedupe key). Plus the supporting components: AssignedToChip, DealPulseChip, PaymentsSection, QualificationChecklist, MultiEoiChip, SkipAheadBanner, WonStatusPanel, InterestBerthStatusBanner, SupplementalInfoRequestButton, UserPicker. Tests: 1370/1370 vitest pass (added deal-health unit suite + expanded constants/validators/pipeline-transitions coverage). tsc clean, eslint clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 03:39:21 +02:00
pipelineStage: 'contract',
leadCategory: 'hot_lead',
source: 'manual',
daysAgoFirst: 320,
},
{
clientIdx: 2,
berthIdx: null,
yachtIdx: null,
feat(pipeline): 9→7 stage refactor + v1.1 hardening wave Replaces the legacy 9-stage pipeline with 7 canonical stages (enquiry → qualified → eoi → reservation → deposit_paid → contract → nurturing) plus three doc sub-status columns (eoi_doc_status, reservation_doc_status, contract_doc_status) that track sent/signed within a single stage instead of branching it. Schema (migration 0062): - interests gains assigned_to, deposit_expected_amount/currency, three doc-status columns, two documenso-id columns, and date_reservation_signed. - New tables: qualification_criteria (per-port admin-configurable), interest_qualifications (per-interest state), payments (deposit / balance / refund records keyed to interest + client). - Default qualification criteria seeded for every existing port. - Dummy-data UPDATEs collapse Sent/Signed pairs and 'completed' into the new stage + doc-status + outcome shape. Migration 0063 adds interest_contact_log.voice_transcript and template_used columns for v1.1-A/B (quick-template buttons + voice transcription via Web Speech API). v1.1 phase work bundled here: - A/B: Quick-template buttons (Call / Visit / Email) + mic toggle on the contact-log compose dialog (useVoiceTranscription hook). - C: berth-rules-engine wraps state writes in pg_advisory_xact_lock with an idempotent re-read; emits rule_evaluated audit traces. - D: Documenso webhook: reservation/contract sub-status stamping moved out of the PDF-download try-block so a download failure no longer swallows the stamp. New integration test coverage. - E: /admin/qualification-criteria CRUD page + admin component. - F: default_new_interest_owner exposed in System Settings. - G: recentActivityCount + active_engagement deal-pulse signal surfaced as a chip on interests + hot-deals card. - H: interest_assigned notification on assignedTo change (skips self-assign, uses a dedupe key). Plus the supporting components: AssignedToChip, DealPulseChip, PaymentsSection, QualificationChecklist, MultiEoiChip, SkipAheadBanner, WonStatusPanel, InterestBerthStatusBanner, SupplementalInfoRequestButton, UserPicker. Tests: 1370/1370 vitest pass (added deal-health unit suite + expanded constants/validators/pipeline-transitions coverage). tsc clean, eslint clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 03:39:21 +02:00
pipelineStage: 'enquiry',
leadCategory: 'general_interest',
source: 'website',
daysAgoFirst: 3,
},
{
clientIdx: 3,
berthIdx: 8,
yachtIdx: 6,
feat(pipeline): 9→7 stage refactor + v1.1 hardening wave Replaces the legacy 9-stage pipeline with 7 canonical stages (enquiry → qualified → eoi → reservation → deposit_paid → contract → nurturing) plus three doc sub-status columns (eoi_doc_status, reservation_doc_status, contract_doc_status) that track sent/signed within a single stage instead of branching it. Schema (migration 0062): - interests gains assigned_to, deposit_expected_amount/currency, three doc-status columns, two documenso-id columns, and date_reservation_signed. - New tables: qualification_criteria (per-port admin-configurable), interest_qualifications (per-interest state), payments (deposit / balance / refund records keyed to interest + client). - Default qualification criteria seeded for every existing port. - Dummy-data UPDATEs collapse Sent/Signed pairs and 'completed' into the new stage + doc-status + outcome shape. Migration 0063 adds interest_contact_log.voice_transcript and template_used columns for v1.1-A/B (quick-template buttons + voice transcription via Web Speech API). v1.1 phase work bundled here: - A/B: Quick-template buttons (Call / Visit / Email) + mic toggle on the contact-log compose dialog (useVoiceTranscription hook). - C: berth-rules-engine wraps state writes in pg_advisory_xact_lock with an idempotent re-read; emits rule_evaluated audit traces. - D: Documenso webhook: reservation/contract sub-status stamping moved out of the PDF-download try-block so a download failure no longer swallows the stamp. New integration test coverage. - E: /admin/qualification-criteria CRUD page + admin component. - F: default_new_interest_owner exposed in System Settings. - G: recentActivityCount + active_engagement deal-pulse signal surfaced as a chip on interests + hot-deals card. - H: interest_assigned notification on assignedTo change (skips self-assign, uses a dedupe key). Plus the supporting components: AssignedToChip, DealPulseChip, PaymentsSection, QualificationChecklist, MultiEoiChip, SkipAheadBanner, WonStatusPanel, InterestBerthStatusBanner, SupplementalInfoRequestButton, UserPicker. Tests: 1370/1370 vitest pass (added deal-health unit suite + expanded constants/validators/pipeline-transitions coverage). tsc clean, eslint clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 03:39:21 +02:00
pipelineStage: 'qualified',
leadCategory: 'specific_qualified',
source: 'website',
daysAgoFirst: 18,
},
{
clientIdx: 5,
berthIdx: null,
yachtIdx: 3,
feat(pipeline): 9→7 stage refactor + v1.1 hardening wave Replaces the legacy 9-stage pipeline with 7 canonical stages (enquiry → qualified → eoi → reservation → deposit_paid → contract → nurturing) plus three doc sub-status columns (eoi_doc_status, reservation_doc_status, contract_doc_status) that track sent/signed within a single stage instead of branching it. Schema (migration 0062): - interests gains assigned_to, deposit_expected_amount/currency, three doc-status columns, two documenso-id columns, and date_reservation_signed. - New tables: qualification_criteria (per-port admin-configurable), interest_qualifications (per-interest state), payments (deposit / balance / refund records keyed to interest + client). - Default qualification criteria seeded for every existing port. - Dummy-data UPDATEs collapse Sent/Signed pairs and 'completed' into the new stage + doc-status + outcome shape. Migration 0063 adds interest_contact_log.voice_transcript and template_used columns for v1.1-A/B (quick-template buttons + voice transcription via Web Speech API). v1.1 phase work bundled here: - A/B: Quick-template buttons (Call / Visit / Email) + mic toggle on the contact-log compose dialog (useVoiceTranscription hook). - C: berth-rules-engine wraps state writes in pg_advisory_xact_lock with an idempotent re-read; emits rule_evaluated audit traces. - D: Documenso webhook: reservation/contract sub-status stamping moved out of the PDF-download try-block so a download failure no longer swallows the stamp. New integration test coverage. - E: /admin/qualification-criteria CRUD page + admin component. - F: default_new_interest_owner exposed in System Settings. - G: recentActivityCount + active_engagement deal-pulse signal surfaced as a chip on interests + hot-deals card. - H: interest_assigned notification on assignedTo change (skips self-assign, uses a dedupe key). Plus the supporting components: AssignedToChip, DealPulseChip, PaymentsSection, QualificationChecklist, MultiEoiChip, SkipAheadBanner, WonStatusPanel, InterestBerthStatusBanner, SupplementalInfoRequestButton, UserPicker. Tests: 1370/1370 vitest pass (added deal-health unit suite + expanded constants/validators/pipeline-transitions coverage). tsc clean, eslint clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 03:39:21 +02:00
pipelineStage: 'qualified',
leadCategory: 'general_interest',
source: 'referral',
daysAgoFirst: 10,
},
// "Lost" - modeled as archived + open stage
{
clientIdx: 4,
berthIdx: 2,
yachtIdx: null,
feat(pipeline): 9→7 stage refactor + v1.1 hardening wave Replaces the legacy 9-stage pipeline with 7 canonical stages (enquiry → qualified → eoi → reservation → deposit_paid → contract → nurturing) plus three doc sub-status columns (eoi_doc_status, reservation_doc_status, contract_doc_status) that track sent/signed within a single stage instead of branching it. Schema (migration 0062): - interests gains assigned_to, deposit_expected_amount/currency, three doc-status columns, two documenso-id columns, and date_reservation_signed. - New tables: qualification_criteria (per-port admin-configurable), interest_qualifications (per-interest state), payments (deposit / balance / refund records keyed to interest + client). - Default qualification criteria seeded for every existing port. - Dummy-data UPDATEs collapse Sent/Signed pairs and 'completed' into the new stage + doc-status + outcome shape. Migration 0063 adds interest_contact_log.voice_transcript and template_used columns for v1.1-A/B (quick-template buttons + voice transcription via Web Speech API). v1.1 phase work bundled here: - A/B: Quick-template buttons (Call / Visit / Email) + mic toggle on the contact-log compose dialog (useVoiceTranscription hook). - C: berth-rules-engine wraps state writes in pg_advisory_xact_lock with an idempotent re-read; emits rule_evaluated audit traces. - D: Documenso webhook: reservation/contract sub-status stamping moved out of the PDF-download try-block so a download failure no longer swallows the stamp. New integration test coverage. - E: /admin/qualification-criteria CRUD page + admin component. - F: default_new_interest_owner exposed in System Settings. - G: recentActivityCount + active_engagement deal-pulse signal surfaced as a chip on interests + hot-deals card. - H: interest_assigned notification on assignedTo change (skips self-assign, uses a dedupe key). Plus the supporting components: AssignedToChip, DealPulseChip, PaymentsSection, QualificationChecklist, MultiEoiChip, SkipAheadBanner, WonStatusPanel, InterestBerthStatusBanner, SupplementalInfoRequestButton, UserPicker. Tests: 1370/1370 vitest pass (added deal-health unit suite + expanded constants/validators/pipeline-transitions coverage). tsc clean, eslint clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 03:39:21 +02:00
pipelineStage: 'enquiry',
leadCategory: 'general_interest',
source: 'website',
daysAgoFirst: 180,
archived: true,
},
{
clientIdx: 6,
berthIdx: 9,
yachtIdx: 4,
feat(pipeline): 9→7 stage refactor + v1.1 hardening wave Replaces the legacy 9-stage pipeline with 7 canonical stages (enquiry → qualified → eoi → reservation → deposit_paid → contract → nurturing) plus three doc sub-status columns (eoi_doc_status, reservation_doc_status, contract_doc_status) that track sent/signed within a single stage instead of branching it. Schema (migration 0062): - interests gains assigned_to, deposit_expected_amount/currency, three doc-status columns, two documenso-id columns, and date_reservation_signed. - New tables: qualification_criteria (per-port admin-configurable), interest_qualifications (per-interest state), payments (deposit / balance / refund records keyed to interest + client). - Default qualification criteria seeded for every existing port. - Dummy-data UPDATEs collapse Sent/Signed pairs and 'completed' into the new stage + doc-status + outcome shape. Migration 0063 adds interest_contact_log.voice_transcript and template_used columns for v1.1-A/B (quick-template buttons + voice transcription via Web Speech API). v1.1 phase work bundled here: - A/B: Quick-template buttons (Call / Visit / Email) + mic toggle on the contact-log compose dialog (useVoiceTranscription hook). - C: berth-rules-engine wraps state writes in pg_advisory_xact_lock with an idempotent re-read; emits rule_evaluated audit traces. - D: Documenso webhook: reservation/contract sub-status stamping moved out of the PDF-download try-block so a download failure no longer swallows the stamp. New integration test coverage. - E: /admin/qualification-criteria CRUD page + admin component. - F: default_new_interest_owner exposed in System Settings. - G: recentActivityCount + active_engagement deal-pulse signal surfaced as a chip on interests + hot-deals card. - H: interest_assigned notification on assignedTo change (skips self-assign, uses a dedupe key). Plus the supporting components: AssignedToChip, DealPulseChip, PaymentsSection, QualificationChecklist, MultiEoiChip, SkipAheadBanner, WonStatusPanel, InterestBerthStatusBanner, SupplementalInfoRequestButton, UserPicker. Tests: 1370/1370 vitest pass (added deal-health unit suite + expanded constants/validators/pipeline-transitions coverage). tsc clean, eslint clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 03:39:21 +02:00
pipelineStage: 'eoi',
leadCategory: 'specific_qualified',
source: 'broker',
daysAgoFirst: 45,
},
];
// Insert interests WITHOUT berthId (column was dropped in
// migration 0029); berth links go through the interest_berths
// junction below. Returning the rows so we can wire up the
// junction with the right interestId per row.
const insertedInterests = await tx
.insert(interests)
.values(
interestPlan.map((p) => ({
portId,
clientId: clientIds[p.clientIdx]!,
yachtId: p.yachtIdx !== null ? yachtRows[p.yachtIdx]!.id : null,
pipelineStage: p.pipelineStage,
leadCategory: p.leadCategory,
source: p.source,
dateFirstContact: daysAgo(p.daysAgoFirst),
dateLastContact: daysAgo(Math.max(0, p.daysAgoFirst - 2)),
archivedAt: p.archived ? daysAgo(p.daysAgoFirst - 30) : null,
})),
)
.returning({ id: interests.id });
const junctionRows: Array<typeof interestBerths.$inferInsert> = [];
interestPlan.forEach((p, i) => {
if (p.berthIdx === null) return;
const interestId = insertedInterests[i]?.id;
if (!interestId) return;
junctionRows.push({
interestId,
berthId: berthRows[p.berthIdx]!.id,
isPrimary: true,
isSpecificInterest: true,
isInEoiBundle: false,
});
});
if (junctionRows.length > 0) {
await tx.insert(interestBerths).values(junctionRows);
}
// ── 8. Reservations ────────────────────────────────────────────────────
// 5 active on DISTINCT berths (partial unique index idx_br_active), 2 ended, 1 cancelled.
// Active: berths 5..9 (under_offer ones we set earlier).
// Ended: berths 10 and 11 (sold) - use historical start/end dates.
// Cancelled: berth 0 (available - a cancelled res doesn't occupy it).
const activeAssignments: Array<{
berthIdx: number;
clientIdx: number;
yachtIdx: number;
startDaysAgo: number;
}> = [
{ berthIdx: 5, clientIdx: 5, yachtIdx: 3, startDaysAgo: 45 },
{ berthIdx: 6, clientIdx: 6, yachtIdx: 4, startDaysAgo: 65 },
{ berthIdx: 7, clientIdx: 0, yachtIdx: 5, startDaysAgo: 85 },
{ berthIdx: 8, clientIdx: 3, yachtIdx: 6, startDaysAgo: 30 },
{ berthIdx: 9, clientIdx: 6, yachtIdx: 4, startDaysAgo: 20 },
];
const endedAssignments: Array<{
berthIdx: number;
clientIdx: number;
yachtIdx: number;
startDaysAgo: number;
endDaysAgo: number;
}> = [
{ berthIdx: 10, clientIdx: 1, yachtIdx: 1, startDaysAgo: 600, endDaysAgo: 240 },
{ berthIdx: 11, clientIdx: 7, yachtIdx: 11, startDaysAgo: 500, endDaysAgo: 60 },
];
const cancelledAssignment = { berthIdx: 0, clientIdx: 2, yachtIdx: 2, startDaysAgo: 30 };
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>
2026-05-25 15:09:35 +02:00
const reservationValues: Array<typeof berthTenancies.$inferInsert> = [];
for (const a of activeAssignments) {
reservationValues.push({
berthId: berthRows[a.berthIdx]!.id,
portId,
clientId: clientIds[a.clientIdx]!,
yachtId: yachtRows[a.yachtIdx]!.id,
status: 'active',
startDate: daysAgo(a.startDaysAgo),
endDate: null,
tenureType: 'permanent',
createdBy: SEED_USER_ID,
});
}
for (const e of endedAssignments) {
reservationValues.push({
berthId: berthRows[e.berthIdx]!.id,
portId,
clientId: clientIds[e.clientIdx]!,
yachtId: yachtRows[e.yachtIdx]!.id,
status: 'ended',
startDate: daysAgo(e.startDaysAgo),
endDate: daysAgo(e.endDaysAgo),
tenureType: 'fixed_term',
createdBy: SEED_USER_ID,
});
}
reservationValues.push({
berthId: berthRows[cancelledAssignment.berthIdx]!.id,
portId,
clientId: clientIds[cancelledAssignment.clientIdx]!,
yachtId: yachtRows[cancelledAssignment.yachtIdx]!.id,
status: 'cancelled',
startDate: daysAgo(cancelledAssignment.startDaysAgo),
endDate: daysAgo(cancelledAssignment.startDaysAgo - 5),
tenureType: 'permanent',
createdBy: SEED_USER_ID,
notes: 'Cancelled by client before activation.',
});
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>
2026-05-25 15:09:35 +02:00
await tx.insert(berthTenancies).values(reservationValues);
return {
berths: berthRows.length,
clients: clientRows.length,
companies: companyRows.length,
yachts: yachtRows.length,
interests: interestPlan.length,
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>
2026-05-25 15:09:35 +02:00
tenancies: reservationValues.length,
};
});
}