2026-04-24 13:26:37 +02:00
/ * *
* Per - port seed data builder for Port Nimara CRM .
*
2026-05-04 22:57:01 +02:00
* Exports ` seedPortData(portId, portSlug) ` - creates a realistic ,
2026-04-24 13:26:37 +02:00
* 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 :
2026-05-04 22:57:01 +02:00
* idx 0 . . 4 - available ( small )
* idx 5 . . 9 - under_offer ( medium )
* idx 10 . . 11 - sold ( large )
2026-04-24 13:26:37 +02:00
* - 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 ,
berthReservations ,
interests ,
2026-04-24 16:13:51 +02:00
documentTemplates ,
2026-04-24 13:26:37 +02:00
} from './schema' ;
2026-04-24 16:13:51 +02:00
import {
getStandardEoiTemplateHtml ,
STANDARD_EOI_MERGE_FIELDS ,
} from '@/lib/pdf/templates/eoi-standard-inapp' ;
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' ;
// ─── Berth snapshot ──────────────────────────────────────────────────────────
// 117 rows imported from the legacy NocoDB Berths table on 2026-05-03.
// Refresh by re-running the snapshot script (see git history of this file).
type SeedBerth = {
legacyId : number ;
mooringNumber : string ;
legacyMooringNumber : 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 [ ] ;
2026-04-24 13:26:37 +02:00
// ─── 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 ;
reservations : number ;
}
// ─── Main ────────────────────────────────────────────────────────────────────
export async function seedPortData ( portId : string , portSlug : string ) : Promise < SeedSummary | null > {
2026-05-04 22:57:01 +02:00
// Idempotency guard - if this port already has companies, assume it's been seeded.
2026-04-24 13:26:37 +02:00
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.
2026-04-24 13:26:37 +02:00
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 ) = > ( {
2026-04-24 13:26:37 +02:00
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 ,
2026-04-24 13:26:37 +02:00
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 ,
2026-04-24 13:26:37 +02:00
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 ,
2026-04-24 13:26:37 +02:00
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' ,
chore(i18n): drop legacy free-text country/nationality columns
Test-data only — no production migration needed (per earlier decision).
Schema is now ISO-only; readers convert ISO codes to localized names where
human-readable output is required (EOI documents, invoices, portal).
Migration 0016 drops:
- clients.nationality
- companies.incorporation_country
- client_addresses.{state_province, country}
- company_addresses.{state_province, country}
Code paths that previously read free-text values now read the ISO column
and pass through `getCountryName()` / `getSubdivisionName()` for rendering.
Document templates ({{client.nationality}}), portal client view, EOI/
reservation-agreement contexts, and invoice billing addresses all updated.
Public yacht-interest endpoint (/api/public/interests) drops the legacy
fields from its insert path and writes ISO codes only. The Zod validators
no longer accept the legacy fields — older website builds posting raw
'incorporationCountry' / 'country' / 'stateProvince' will get 400s.
Server-side phone normalization is unchanged.
Seed data updated to use ISO codes (GB/FR/ES/GR/SE/IT/GH/MC/PA), spread
across continents to keep test fixtures realistic.
Test assertions updated to match the new render shape (e.g.
'United States' not 'US', 'California' not 'CA').
Vitest: 741 -> 741 (unchanged count; assertions updated, no new tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 19:00:57 +02:00
incorporationCountryIso : 'GR' ,
2026-04-24 13:26:37 +02:00
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' ,
chore(i18n): drop legacy free-text country/nationality columns
Test-data only — no production migration needed (per earlier decision).
Schema is now ISO-only; readers convert ISO codes to localized names where
human-readable output is required (EOI documents, invoices, portal).
Migration 0016 drops:
- clients.nationality
- companies.incorporation_country
- client_addresses.{state_province, country}
- company_addresses.{state_province, country}
Code paths that previously read free-text values now read the ISO column
and pass through `getCountryName()` / `getSubdivisionName()` for rendering.
Document templates ({{client.nationality}}), portal client view, EOI/
reservation-agreement contexts, and invoice billing addresses all updated.
Public yacht-interest endpoint (/api/public/interests) drops the legacy
fields from its insert path and writes ISO codes only. The Zod validators
no longer accept the legacy fields — older website builds posting raw
'incorporationCountry' / 'country' / 'stateProvince' will get 400s.
Server-side phone normalization is unchanged.
Seed data updated to use ISO codes (GB/FR/ES/GR/SE/IT/GH/MC/PA), spread
across continents to keep test fixtures realistic.
Test assertions updated to match the new render shape (e.g.
'United States' not 'US', 'California' not 'CA').
Vitest: 741 -> 741 (unchanged count; assertions updated, no new tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 19:00:57 +02:00
incorporationCountryIso : 'MC' ,
2026-04-24 13:26:37 +02:00
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' ,
chore(i18n): drop legacy free-text country/nationality columns
Test-data only — no production migration needed (per earlier decision).
Schema is now ISO-only; readers convert ISO codes to localized names where
human-readable output is required (EOI documents, invoices, portal).
Migration 0016 drops:
- clients.nationality
- companies.incorporation_country
- client_addresses.{state_province, country}
- company_addresses.{state_province, country}
Code paths that previously read free-text values now read the ISO column
and pass through `getCountryName()` / `getSubdivisionName()` for rendering.
Document templates ({{client.nationality}}), portal client view, EOI/
reservation-agreement contexts, and invoice billing addresses all updated.
Public yacht-interest endpoint (/api/public/interests) drops the legacy
fields from its insert path and writes ISO codes only. The Zod validators
no longer accept the legacy fields — older website builds posting raw
'incorporationCountry' / 'country' / 'stateProvince' will get 400s.
Server-side phone normalization is unchanged.
Seed data updated to use ISO codes (GB/FR/ES/GR/SE/IT/GH/MC/PA), spread
across continents to keep test fixtures realistic.
Test assertions updated to match the new render shape (e.g.
'United States' not 'US', 'California' not 'CA').
Vitest: 741 -> 741 (unchanged count; assertions updated, no new tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 19:00:57 +02:00
incorporationCountryIso : 'PA' ,
2026-04-24 13:26:37 +02:00
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' ,
chore(i18n): drop legacy free-text country/nationality columns
Test-data only — no production migration needed (per earlier decision).
Schema is now ISO-only; readers convert ISO codes to localized names where
human-readable output is required (EOI documents, invoices, portal).
Migration 0016 drops:
- clients.nationality
- companies.incorporation_country
- client_addresses.{state_province, country}
- company_addresses.{state_province, country}
Code paths that previously read free-text values now read the ISO column
and pass through `getCountryName()` / `getSubdivisionName()` for rendering.
Document templates ({{client.nationality}}), portal client view, EOI/
reservation-agreement contexts, and invoice billing addresses all updated.
Public yacht-interest endpoint (/api/public/interests) drops the legacy
fields from its insert path and writes ISO codes only. The Zod validators
no longer accept the legacy fields — older website builds posting raw
'incorporationCountry' / 'country' / 'stateProvince' will get 400s.
Server-side phone normalization is unchanged.
Seed data updated to use ISO codes (GB/FR/ES/GR/SE/IT/GH/MC/PA), spread
across continents to keep test fixtures realistic.
Test assertions updated to match the new render shape (e.g.
'United States' not 'US', 'California' not 'CA').
Vitest: 741 -> 741 (unchanged count; assertions updated, no new tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 19:00:57 +02:00
subdivisionIso : 'GR-A' ,
2026-04-24 13:26:37 +02:00
postalCode : '10558' ,
chore(i18n): drop legacy free-text country/nationality columns
Test-data only — no production migration needed (per earlier decision).
Schema is now ISO-only; readers convert ISO codes to localized names where
human-readable output is required (EOI documents, invoices, portal).
Migration 0016 drops:
- clients.nationality
- companies.incorporation_country
- client_addresses.{state_province, country}
- company_addresses.{state_province, country}
Code paths that previously read free-text values now read the ISO column
and pass through `getCountryName()` / `getSubdivisionName()` for rendering.
Document templates ({{client.nationality}}), portal client view, EOI/
reservation-agreement contexts, and invoice billing addresses all updated.
Public yacht-interest endpoint (/api/public/interests) drops the legacy
fields from its insert path and writes ISO codes only. The Zod validators
no longer accept the legacy fields — older website builds posting raw
'incorporationCountry' / 'country' / 'stateProvince' will get 400s.
Server-side phone normalization is unchanged.
Seed data updated to use ISO codes (GB/FR/ES/GR/SE/IT/GH/MC/PA), spread
across continents to keep test fixtures realistic.
Test assertions updated to match the new render shape (e.g.
'United States' not 'US', 'California' not 'CA').
Vitest: 741 -> 741 (unchanged count; assertions updated, no new tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 19:00:57 +02:00
countryIso : 'GR' ,
2026-04-24 13:26:37 +02:00
isPrimary : true ,
} ,
{
companyId : blueSeasId ,
portId ,
label : 'Registered Office' ,
streetAddress : '3 Boulevard des Moulins' ,
city : 'Monte Carlo' ,
chore(i18n): drop legacy free-text country/nationality columns
Test-data only — no production migration needed (per earlier decision).
Schema is now ISO-only; readers convert ISO codes to localized names where
human-readable output is required (EOI documents, invoices, portal).
Migration 0016 drops:
- clients.nationality
- companies.incorporation_country
- client_addresses.{state_province, country}
- company_addresses.{state_province, country}
Code paths that previously read free-text values now read the ISO column
and pass through `getCountryName()` / `getSubdivisionName()` for rendering.
Document templates ({{client.nationality}}), portal client view, EOI/
reservation-agreement contexts, and invoice billing addresses all updated.
Public yacht-interest endpoint (/api/public/interests) drops the legacy
fields from its insert path and writes ISO codes only. The Zod validators
no longer accept the legacy fields — older website builds posting raw
'incorporationCountry' / 'country' / 'stateProvince' will get 400s.
Server-side phone normalization is unchanged.
Seed data updated to use ISO codes (GB/FR/ES/GR/SE/IT/GH/MC/PA), spread
across continents to keep test fixtures realistic.
Test assertions updated to match the new render shape (e.g.
'United States' not 'US', 'California' not 'CA').
Vitest: 741 -> 741 (unchanged count; assertions updated, no new tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 19:00:57 +02:00
subdivisionIso : null ,
2026-04-24 13:26:37 +02:00
postalCode : 'MC-98000' ,
chore(i18n): drop legacy free-text country/nationality columns
Test-data only — no production migration needed (per earlier decision).
Schema is now ISO-only; readers convert ISO codes to localized names where
human-readable output is required (EOI documents, invoices, portal).
Migration 0016 drops:
- clients.nationality
- companies.incorporation_country
- client_addresses.{state_province, country}
- company_addresses.{state_province, country}
Code paths that previously read free-text values now read the ISO column
and pass through `getCountryName()` / `getSubdivisionName()` for rendering.
Document templates ({{client.nationality}}), portal client view, EOI/
reservation-agreement contexts, and invoice billing addresses all updated.
Public yacht-interest endpoint (/api/public/interests) drops the legacy
fields from its insert path and writes ISO codes only. The Zod validators
no longer accept the legacy fields — older website builds posting raw
'incorporationCountry' / 'country' / 'stateProvince' will get 400s.
Server-side phone normalization is unchanged.
Seed data updated to use ISO codes (GB/FR/ES/GR/SE/IT/GH/MC/PA), spread
across continents to keep test fixtures realistic.
Test assertions updated to match the new render shape (e.g.
'United States' not 'US', 'California' not 'CA').
Vitest: 741 -> 741 (unchanged count; assertions updated, no new tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 19:00:57 +02:00
countryIso : 'MC' ,
2026-04-24 13:26:37 +02:00
isPrimary : true ,
} ,
{
companyId : phantomId ,
portId ,
label : 'Former Office' ,
streetAddress : 'Calle 50, Torre Global, Piso 20' ,
city : 'Panama City' ,
chore(i18n): drop legacy free-text country/nationality columns
Test-data only — no production migration needed (per earlier decision).
Schema is now ISO-only; readers convert ISO codes to localized names where
human-readable output is required (EOI documents, invoices, portal).
Migration 0016 drops:
- clients.nationality
- companies.incorporation_country
- client_addresses.{state_province, country}
- company_addresses.{state_province, country}
Code paths that previously read free-text values now read the ISO column
and pass through `getCountryName()` / `getSubdivisionName()` for rendering.
Document templates ({{client.nationality}}), portal client view, EOI/
reservation-agreement contexts, and invoice billing addresses all updated.
Public yacht-interest endpoint (/api/public/interests) drops the legacy
fields from its insert path and writes ISO codes only. The Zod validators
no longer accept the legacy fields — older website builds posting raw
'incorporationCountry' / 'country' / 'stateProvince' will get 400s.
Server-side phone normalization is unchanged.
Seed data updated to use ISO codes (GB/FR/ES/GR/SE/IT/GH/MC/PA), spread
across continents to keep test fixtures realistic.
Test assertions updated to match the new render shape (e.g.
'United States' not 'US', 'California' not 'CA').
Vitest: 741 -> 741 (unchanged count; assertions updated, no new tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 19:00:57 +02:00
subdivisionIso : null ,
2026-04-24 13:26:37 +02:00
postalCode : '0801' ,
chore(i18n): drop legacy free-text country/nationality columns
Test-data only — no production migration needed (per earlier decision).
Schema is now ISO-only; readers convert ISO codes to localized names where
human-readable output is required (EOI documents, invoices, portal).
Migration 0016 drops:
- clients.nationality
- companies.incorporation_country
- client_addresses.{state_province, country}
- company_addresses.{state_province, country}
Code paths that previously read free-text values now read the ISO column
and pass through `getCountryName()` / `getSubdivisionName()` for rendering.
Document templates ({{client.nationality}}), portal client view, EOI/
reservation-agreement contexts, and invoice billing addresses all updated.
Public yacht-interest endpoint (/api/public/interests) drops the legacy
fields from its insert path and writes ISO codes only. The Zod validators
no longer accept the legacy fields — older website builds posting raw
'incorporationCountry' / 'country' / 'stateProvince' will get 400s.
Server-side phone normalization is unchanged.
Seed data updated to use ISO codes (GB/FR/ES/GR/SE/IT/GH/MC/PA), spread
across continents to keep test fixtures realistic.
Test assertions updated to match the new render shape (e.g.
'United States' not 'US', 'California' not 'CA').
Vitest: 741 -> 741 (unchanged count; assertions updated, no new tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 19:00:57 +02:00
countryIso : 'PA' ,
2026-04-24 13:26:37 +02:00
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 ;
chore(i18n): drop legacy free-text country/nationality columns
Test-data only — no production migration needed (per earlier decision).
Schema is now ISO-only; readers convert ISO codes to localized names where
human-readable output is required (EOI documents, invoices, portal).
Migration 0016 drops:
- clients.nationality
- companies.incorporation_country
- client_addresses.{state_province, country}
- company_addresses.{state_province, country}
Code paths that previously read free-text values now read the ISO column
and pass through `getCountryName()` / `getSubdivisionName()` for rendering.
Document templates ({{client.nationality}}), portal client view, EOI/
reservation-agreement contexts, and invoice billing addresses all updated.
Public yacht-interest endpoint (/api/public/interests) drops the legacy
fields from its insert path and writes ISO codes only. The Zod validators
no longer accept the legacy fields — older website builds posting raw
'incorporationCountry' / 'country' / 'stateProvince' will get 400s.
Server-side phone normalization is unchanged.
Seed data updated to use ISO codes (GB/FR/ES/GR/SE/IT/GH/MC/PA), spread
across continents to keep test fixtures realistic.
Test assertions updated to match the new render shape (e.g.
'United States' not 'US', 'California' not 'CA').
Vitest: 741 -> 741 (unchanged count; assertions updated, no new tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 19:00:57 +02:00
nationalityIso : string ;
2026-04-24 13:26:37 +02:00
email : string ;
phone : string ;
whatsapp? : string ;
city : string ;
chore(i18n): drop legacy free-text country/nationality columns
Test-data only — no production migration needed (per earlier decision).
Schema is now ISO-only; readers convert ISO codes to localized names where
human-readable output is required (EOI documents, invoices, portal).
Migration 0016 drops:
- clients.nationality
- companies.incorporation_country
- client_addresses.{state_province, country}
- company_addresses.{state_province, country}
Code paths that previously read free-text values now read the ISO column
and pass through `getCountryName()` / `getSubdivisionName()` for rendering.
Document templates ({{client.nationality}}), portal client view, EOI/
reservation-agreement contexts, and invoice billing addresses all updated.
Public yacht-interest endpoint (/api/public/interests) drops the legacy
fields from its insert path and writes ISO codes only. The Zod validators
no longer accept the legacy fields — older website builds posting raw
'incorporationCountry' / 'country' / 'stateProvince' will get 400s.
Server-side phone normalization is unchanged.
Seed data updated to use ISO codes (GB/FR/ES/GR/SE/IT/GH/MC/PA), spread
across continents to keep test fixtures realistic.
Test assertions updated to match the new render shape (e.g.
'United States' not 'US', 'California' not 'CA').
Vitest: 741 -> 741 (unchanged count; assertions updated, no new tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 19:00:57 +02:00
countryIso : string ;
2026-04-24 13:26:37 +02:00
postalCode : string ;
street : string ;
} > = [
{
fullName : 'Helena Marsh' ,
chore(i18n): drop legacy free-text country/nationality columns
Test-data only — no production migration needed (per earlier decision).
Schema is now ISO-only; readers convert ISO codes to localized names where
human-readable output is required (EOI documents, invoices, portal).
Migration 0016 drops:
- clients.nationality
- companies.incorporation_country
- client_addresses.{state_province, country}
- company_addresses.{state_province, country}
Code paths that previously read free-text values now read the ISO column
and pass through `getCountryName()` / `getSubdivisionName()` for rendering.
Document templates ({{client.nationality}}), portal client view, EOI/
reservation-agreement contexts, and invoice billing addresses all updated.
Public yacht-interest endpoint (/api/public/interests) drops the legacy
fields from its insert path and writes ISO codes only. The Zod validators
no longer accept the legacy fields — older website builds posting raw
'incorporationCountry' / 'country' / 'stateProvince' will get 400s.
Server-side phone normalization is unchanged.
Seed data updated to use ISO codes (GB/FR/ES/GR/SE/IT/GH/MC/PA), spread
across continents to keep test fixtures realistic.
Test assertions updated to match the new render shape (e.g.
'United States' not 'US', 'California' not 'CA').
Vitest: 741 -> 741 (unchanged count; assertions updated, no new tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 19:00:57 +02:00
nationalityIso : 'GB' ,
2026-04-24 13:26:37 +02:00
email : 'helena.marsh@example.com' ,
phone : '+44 20 7946 0001' ,
whatsapp : '+44 7700 900001' ,
city : 'London' ,
chore(i18n): drop legacy free-text country/nationality columns
Test-data only — no production migration needed (per earlier decision).
Schema is now ISO-only; readers convert ISO codes to localized names where
human-readable output is required (EOI documents, invoices, portal).
Migration 0016 drops:
- clients.nationality
- companies.incorporation_country
- client_addresses.{state_province, country}
- company_addresses.{state_province, country}
Code paths that previously read free-text values now read the ISO column
and pass through `getCountryName()` / `getSubdivisionName()` for rendering.
Document templates ({{client.nationality}}), portal client view, EOI/
reservation-agreement contexts, and invoice billing addresses all updated.
Public yacht-interest endpoint (/api/public/interests) drops the legacy
fields from its insert path and writes ISO codes only. The Zod validators
no longer accept the legacy fields — older website builds posting raw
'incorporationCountry' / 'country' / 'stateProvince' will get 400s.
Server-side phone normalization is unchanged.
Seed data updated to use ISO codes (GB/FR/ES/GR/SE/IT/GH/MC/PA), spread
across continents to keep test fixtures realistic.
Test assertions updated to match the new render shape (e.g.
'United States' not 'US', 'California' not 'CA').
Vitest: 741 -> 741 (unchanged count; assertions updated, no new tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 19:00:57 +02:00
countryIso : 'GB' ,
2026-04-24 13:26:37 +02:00
postalCode : 'SW1A 1AA' ,
street : '22 Belgrave Square' ,
} ,
{
fullName : 'Marcus Laurent' ,
chore(i18n): drop legacy free-text country/nationality columns
Test-data only — no production migration needed (per earlier decision).
Schema is now ISO-only; readers convert ISO codes to localized names where
human-readable output is required (EOI documents, invoices, portal).
Migration 0016 drops:
- clients.nationality
- companies.incorporation_country
- client_addresses.{state_province, country}
- company_addresses.{state_province, country}
Code paths that previously read free-text values now read the ISO column
and pass through `getCountryName()` / `getSubdivisionName()` for rendering.
Document templates ({{client.nationality}}), portal client view, EOI/
reservation-agreement contexts, and invoice billing addresses all updated.
Public yacht-interest endpoint (/api/public/interests) drops the legacy
fields from its insert path and writes ISO codes only. The Zod validators
no longer accept the legacy fields — older website builds posting raw
'incorporationCountry' / 'country' / 'stateProvince' will get 400s.
Server-side phone normalization is unchanged.
Seed data updated to use ISO codes (GB/FR/ES/GR/SE/IT/GH/MC/PA), spread
across continents to keep test fixtures realistic.
Test assertions updated to match the new render shape (e.g.
'United States' not 'US', 'California' not 'CA').
Vitest: 741 -> 741 (unchanged count; assertions updated, no new tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 19:00:57 +02:00
nationalityIso : 'FR' ,
2026-04-24 13:26:37 +02:00
email : 'marcus.laurent@example.com' ,
phone : '+33 4 93 00 0002' ,
city : 'Nice' ,
chore(i18n): drop legacy free-text country/nationality columns
Test-data only — no production migration needed (per earlier decision).
Schema is now ISO-only; readers convert ISO codes to localized names where
human-readable output is required (EOI documents, invoices, portal).
Migration 0016 drops:
- clients.nationality
- companies.incorporation_country
- client_addresses.{state_province, country}
- company_addresses.{state_province, country}
Code paths that previously read free-text values now read the ISO column
and pass through `getCountryName()` / `getSubdivisionName()` for rendering.
Document templates ({{client.nationality}}), portal client view, EOI/
reservation-agreement contexts, and invoice billing addresses all updated.
Public yacht-interest endpoint (/api/public/interests) drops the legacy
fields from its insert path and writes ISO codes only. The Zod validators
no longer accept the legacy fields — older website builds posting raw
'incorporationCountry' / 'country' / 'stateProvince' will get 400s.
Server-side phone normalization is unchanged.
Seed data updated to use ISO codes (GB/FR/ES/GR/SE/IT/GH/MC/PA), spread
across continents to keep test fixtures realistic.
Test assertions updated to match the new render shape (e.g.
'United States' not 'US', 'California' not 'CA').
Vitest: 741 -> 741 (unchanged count; assertions updated, no new tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 19:00:57 +02:00
countryIso : 'FR' ,
2026-04-24 13:26:37 +02:00
postalCode : '06300' ,
street : '8 Promenade des Anglais' ,
} ,
{
fullName : 'Sofia Reyes' ,
chore(i18n): drop legacy free-text country/nationality columns
Test-data only — no production migration needed (per earlier decision).
Schema is now ISO-only; readers convert ISO codes to localized names where
human-readable output is required (EOI documents, invoices, portal).
Migration 0016 drops:
- clients.nationality
- companies.incorporation_country
- client_addresses.{state_province, country}
- company_addresses.{state_province, country}
Code paths that previously read free-text values now read the ISO column
and pass through `getCountryName()` / `getSubdivisionName()` for rendering.
Document templates ({{client.nationality}}), portal client view, EOI/
reservation-agreement contexts, and invoice billing addresses all updated.
Public yacht-interest endpoint (/api/public/interests) drops the legacy
fields from its insert path and writes ISO codes only. The Zod validators
no longer accept the legacy fields — older website builds posting raw
'incorporationCountry' / 'country' / 'stateProvince' will get 400s.
Server-side phone normalization is unchanged.
Seed data updated to use ISO codes (GB/FR/ES/GR/SE/IT/GH/MC/PA), spread
across continents to keep test fixtures realistic.
Test assertions updated to match the new render shape (e.g.
'United States' not 'US', 'California' not 'CA').
Vitest: 741 -> 741 (unchanged count; assertions updated, no new tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 19:00:57 +02:00
nationalityIso : 'ES' ,
2026-04-24 13:26:37 +02:00
email : 'sofia.reyes@example.com' ,
phone : '+34 971 000 003' ,
whatsapp : '+34 666 000 003' ,
city : 'Palma' ,
chore(i18n): drop legacy free-text country/nationality columns
Test-data only — no production migration needed (per earlier decision).
Schema is now ISO-only; readers convert ISO codes to localized names where
human-readable output is required (EOI documents, invoices, portal).
Migration 0016 drops:
- clients.nationality
- companies.incorporation_country
- client_addresses.{state_province, country}
- company_addresses.{state_province, country}
Code paths that previously read free-text values now read the ISO column
and pass through `getCountryName()` / `getSubdivisionName()` for rendering.
Document templates ({{client.nationality}}), portal client view, EOI/
reservation-agreement contexts, and invoice billing addresses all updated.
Public yacht-interest endpoint (/api/public/interests) drops the legacy
fields from its insert path and writes ISO codes only. The Zod validators
no longer accept the legacy fields — older website builds posting raw
'incorporationCountry' / 'country' / 'stateProvince' will get 400s.
Server-side phone normalization is unchanged.
Seed data updated to use ISO codes (GB/FR/ES/GR/SE/IT/GH/MC/PA), spread
across continents to keep test fixtures realistic.
Test assertions updated to match the new render shape (e.g.
'United States' not 'US', 'California' not 'CA').
Vitest: 741 -> 741 (unchanged count; assertions updated, no new tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 19:00:57 +02:00
countryIso : 'ES' ,
2026-04-24 13:26:37 +02:00
postalCode : '07012' ,
street : 'Passeig Marítim 12' ,
} ,
{
fullName : 'Dimitrios Andreadis' ,
chore(i18n): drop legacy free-text country/nationality columns
Test-data only — no production migration needed (per earlier decision).
Schema is now ISO-only; readers convert ISO codes to localized names where
human-readable output is required (EOI documents, invoices, portal).
Migration 0016 drops:
- clients.nationality
- companies.incorporation_country
- client_addresses.{state_province, country}
- company_addresses.{state_province, country}
Code paths that previously read free-text values now read the ISO column
and pass through `getCountryName()` / `getSubdivisionName()` for rendering.
Document templates ({{client.nationality}}), portal client view, EOI/
reservation-agreement contexts, and invoice billing addresses all updated.
Public yacht-interest endpoint (/api/public/interests) drops the legacy
fields from its insert path and writes ISO codes only. The Zod validators
no longer accept the legacy fields — older website builds posting raw
'incorporationCountry' / 'country' / 'stateProvince' will get 400s.
Server-side phone normalization is unchanged.
Seed data updated to use ISO codes (GB/FR/ES/GR/SE/IT/GH/MC/PA), spread
across continents to keep test fixtures realistic.
Test assertions updated to match the new render shape (e.g.
'United States' not 'US', 'California' not 'CA').
Vitest: 741 -> 741 (unchanged count; assertions updated, no new tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 19:00:57 +02:00
nationalityIso : 'GR' ,
2026-04-24 13:26:37 +02:00
email : 'd.andreadis@aegean-holdings.example' ,
phone : '+30 210 000 0004' ,
city : 'Athens' ,
chore(i18n): drop legacy free-text country/nationality columns
Test-data only — no production migration needed (per earlier decision).
Schema is now ISO-only; readers convert ISO codes to localized names where
human-readable output is required (EOI documents, invoices, portal).
Migration 0016 drops:
- clients.nationality
- companies.incorporation_country
- client_addresses.{state_province, country}
- company_addresses.{state_province, country}
Code paths that previously read free-text values now read the ISO column
and pass through `getCountryName()` / `getSubdivisionName()` for rendering.
Document templates ({{client.nationality}}), portal client view, EOI/
reservation-agreement contexts, and invoice billing addresses all updated.
Public yacht-interest endpoint (/api/public/interests) drops the legacy
fields from its insert path and writes ISO codes only. The Zod validators
no longer accept the legacy fields — older website builds posting raw
'incorporationCountry' / 'country' / 'stateProvince' will get 400s.
Server-side phone normalization is unchanged.
Seed data updated to use ISO codes (GB/FR/ES/GR/SE/IT/GH/MC/PA), spread
across continents to keep test fixtures realistic.
Test assertions updated to match the new render shape (e.g.
'United States' not 'US', 'California' not 'CA').
Vitest: 741 -> 741 (unchanged count; assertions updated, no new tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 19:00:57 +02:00
countryIso : 'GR' ,
2026-04-24 13:26:37 +02:00
postalCode : '10558' ,
street : '14 Mikonou Avenue' ,
} ,
{
fullName : 'Katerina Papadakis' ,
chore(i18n): drop legacy free-text country/nationality columns
Test-data only — no production migration needed (per earlier decision).
Schema is now ISO-only; readers convert ISO codes to localized names where
human-readable output is required (EOI documents, invoices, portal).
Migration 0016 drops:
- clients.nationality
- companies.incorporation_country
- client_addresses.{state_province, country}
- company_addresses.{state_province, country}
Code paths that previously read free-text values now read the ISO column
and pass through `getCountryName()` / `getSubdivisionName()` for rendering.
Document templates ({{client.nationality}}), portal client view, EOI/
reservation-agreement contexts, and invoice billing addresses all updated.
Public yacht-interest endpoint (/api/public/interests) drops the legacy
fields from its insert path and writes ISO codes only. The Zod validators
no longer accept the legacy fields — older website builds posting raw
'incorporationCountry' / 'country' / 'stateProvince' will get 400s.
Server-side phone normalization is unchanged.
Seed data updated to use ISO codes (GB/FR/ES/GR/SE/IT/GH/MC/PA), spread
across continents to keep test fixtures realistic.
Test assertions updated to match the new render shape (e.g.
'United States' not 'US', 'California' not 'CA').
Vitest: 741 -> 741 (unchanged count; assertions updated, no new tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 19:00:57 +02:00
nationalityIso : 'GR' ,
2026-04-24 13:26:37 +02:00
email : 'k.papadakis@aegean-holdings.example' ,
phone : '+30 210 000 0005' ,
whatsapp : '+30 694 000 0005' ,
city : 'Athens' ,
chore(i18n): drop legacy free-text country/nationality columns
Test-data only — no production migration needed (per earlier decision).
Schema is now ISO-only; readers convert ISO codes to localized names where
human-readable output is required (EOI documents, invoices, portal).
Migration 0016 drops:
- clients.nationality
- companies.incorporation_country
- client_addresses.{state_province, country}
- company_addresses.{state_province, country}
Code paths that previously read free-text values now read the ISO column
and pass through `getCountryName()` / `getSubdivisionName()` for rendering.
Document templates ({{client.nationality}}), portal client view, EOI/
reservation-agreement contexts, and invoice billing addresses all updated.
Public yacht-interest endpoint (/api/public/interests) drops the legacy
fields from its insert path and writes ISO codes only. The Zod validators
no longer accept the legacy fields — older website builds posting raw
'incorporationCountry' / 'country' / 'stateProvince' will get 400s.
Server-side phone normalization is unchanged.
Seed data updated to use ISO codes (GB/FR/ES/GR/SE/IT/GH/MC/PA), spread
across continents to keep test fixtures realistic.
Test assertions updated to match the new render shape (e.g.
'United States' not 'US', 'California' not 'CA').
Vitest: 741 -> 741 (unchanged count; assertions updated, no new tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 19:00:57 +02:00
countryIso : 'GR' ,
2026-04-24 13:26:37 +02:00
postalCode : '10558' ,
street : '14 Mikonou Avenue' ,
} ,
{
fullName : 'Jonas Lindqvist' ,
chore(i18n): drop legacy free-text country/nationality columns
Test-data only — no production migration needed (per earlier decision).
Schema is now ISO-only; readers convert ISO codes to localized names where
human-readable output is required (EOI documents, invoices, portal).
Migration 0016 drops:
- clients.nationality
- companies.incorporation_country
- client_addresses.{state_province, country}
- company_addresses.{state_province, country}
Code paths that previously read free-text values now read the ISO column
and pass through `getCountryName()` / `getSubdivisionName()` for rendering.
Document templates ({{client.nationality}}), portal client view, EOI/
reservation-agreement contexts, and invoice billing addresses all updated.
Public yacht-interest endpoint (/api/public/interests) drops the legacy
fields from its insert path and writes ISO codes only. The Zod validators
no longer accept the legacy fields — older website builds posting raw
'incorporationCountry' / 'country' / 'stateProvince' will get 400s.
Server-side phone normalization is unchanged.
Seed data updated to use ISO codes (GB/FR/ES/GR/SE/IT/GH/MC/PA), spread
across continents to keep test fixtures realistic.
Test assertions updated to match the new render shape (e.g.
'United States' not 'US', 'California' not 'CA').
Vitest: 741 -> 741 (unchanged count; assertions updated, no new tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 19:00:57 +02:00
nationalityIso : 'SE' ,
2026-04-24 13:26:37 +02:00
email : 'jonas.lindqvist@example.com' ,
phone : '+46 8 000 0006' ,
city : 'Stockholm' ,
chore(i18n): drop legacy free-text country/nationality columns
Test-data only — no production migration needed (per earlier decision).
Schema is now ISO-only; readers convert ISO codes to localized names where
human-readable output is required (EOI documents, invoices, portal).
Migration 0016 drops:
- clients.nationality
- companies.incorporation_country
- client_addresses.{state_province, country}
- company_addresses.{state_province, country}
Code paths that previously read free-text values now read the ISO column
and pass through `getCountryName()` / `getSubdivisionName()` for rendering.
Document templates ({{client.nationality}}), portal client view, EOI/
reservation-agreement contexts, and invoice billing addresses all updated.
Public yacht-interest endpoint (/api/public/interests) drops the legacy
fields from its insert path and writes ISO codes only. The Zod validators
no longer accept the legacy fields — older website builds posting raw
'incorporationCountry' / 'country' / 'stateProvince' will get 400s.
Server-side phone normalization is unchanged.
Seed data updated to use ISO codes (GB/FR/ES/GR/SE/IT/GH/MC/PA), spread
across continents to keep test fixtures realistic.
Test assertions updated to match the new render shape (e.g.
'United States' not 'US', 'California' not 'CA').
Vitest: 741 -> 741 (unchanged count; assertions updated, no new tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 19:00:57 +02:00
countryIso : 'SE' ,
2026-04-24 13:26:37 +02:00
postalCode : '11129' ,
street : 'Strandvägen 47' ,
} ,
{
fullName : 'Isabella Conti' ,
chore(i18n): drop legacy free-text country/nationality columns
Test-data only — no production migration needed (per earlier decision).
Schema is now ISO-only; readers convert ISO codes to localized names where
human-readable output is required (EOI documents, invoices, portal).
Migration 0016 drops:
- clients.nationality
- companies.incorporation_country
- client_addresses.{state_province, country}
- company_addresses.{state_province, country}
Code paths that previously read free-text values now read the ISO column
and pass through `getCountryName()` / `getSubdivisionName()` for rendering.
Document templates ({{client.nationality}}), portal client view, EOI/
reservation-agreement contexts, and invoice billing addresses all updated.
Public yacht-interest endpoint (/api/public/interests) drops the legacy
fields from its insert path and writes ISO codes only. The Zod validators
no longer accept the legacy fields — older website builds posting raw
'incorporationCountry' / 'country' / 'stateProvince' will get 400s.
Server-side phone normalization is unchanged.
Seed data updated to use ISO codes (GB/FR/ES/GR/SE/IT/GH/MC/PA), spread
across continents to keep test fixtures realistic.
Test assertions updated to match the new render shape (e.g.
'United States' not 'US', 'California' not 'CA').
Vitest: 741 -> 741 (unchanged count; assertions updated, no new tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 19:00:57 +02:00
nationalityIso : 'IT' ,
2026-04-24 13:26:37 +02:00
email : 'isabella.conti@example.com' ,
phone : '+39 010 000 0007' ,
whatsapp : '+39 333 000 0007' ,
city : 'Genoa' ,
chore(i18n): drop legacy free-text country/nationality columns
Test-data only — no production migration needed (per earlier decision).
Schema is now ISO-only; readers convert ISO codes to localized names where
human-readable output is required (EOI documents, invoices, portal).
Migration 0016 drops:
- clients.nationality
- companies.incorporation_country
- client_addresses.{state_province, country}
- company_addresses.{state_province, country}
Code paths that previously read free-text values now read the ISO column
and pass through `getCountryName()` / `getSubdivisionName()` for rendering.
Document templates ({{client.nationality}}), portal client view, EOI/
reservation-agreement contexts, and invoice billing addresses all updated.
Public yacht-interest endpoint (/api/public/interests) drops the legacy
fields from its insert path and writes ISO codes only. The Zod validators
no longer accept the legacy fields — older website builds posting raw
'incorporationCountry' / 'country' / 'stateProvince' will get 400s.
Server-side phone normalization is unchanged.
Seed data updated to use ISO codes (GB/FR/ES/GR/SE/IT/GH/MC/PA), spread
across continents to keep test fixtures realistic.
Test assertions updated to match the new render shape (e.g.
'United States' not 'US', 'California' not 'CA').
Vitest: 741 -> 741 (unchanged count; assertions updated, no new tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 19:00:57 +02:00
countryIso : 'IT' ,
2026-04-24 13:26:37 +02:00
postalCode : '16124' ,
street : 'Via Garibaldi 9' ,
} ,
{
fullName : 'Raymond Osei' ,
chore(i18n): drop legacy free-text country/nationality columns
Test-data only — no production migration needed (per earlier decision).
Schema is now ISO-only; readers convert ISO codes to localized names where
human-readable output is required (EOI documents, invoices, portal).
Migration 0016 drops:
- clients.nationality
- companies.incorporation_country
- client_addresses.{state_province, country}
- company_addresses.{state_province, country}
Code paths that previously read free-text values now read the ISO column
and pass through `getCountryName()` / `getSubdivisionName()` for rendering.
Document templates ({{client.nationality}}), portal client view, EOI/
reservation-agreement contexts, and invoice billing addresses all updated.
Public yacht-interest endpoint (/api/public/interests) drops the legacy
fields from its insert path and writes ISO codes only. The Zod validators
no longer accept the legacy fields — older website builds posting raw
'incorporationCountry' / 'country' / 'stateProvince' will get 400s.
Server-side phone normalization is unchanged.
Seed data updated to use ISO codes (GB/FR/ES/GR/SE/IT/GH/MC/PA), spread
across continents to keep test fixtures realistic.
Test assertions updated to match the new render shape (e.g.
'United States' not 'US', 'California' not 'CA').
Vitest: 741 -> 741 (unchanged count; assertions updated, no new tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 19:00:57 +02:00
nationalityIso : 'GH' ,
2026-04-24 13:26:37 +02:00
email : 'raymond.osei@example.com' ,
phone : '+233 30 000 0008' ,
city : 'Accra' ,
chore(i18n): drop legacy free-text country/nationality columns
Test-data only — no production migration needed (per earlier decision).
Schema is now ISO-only; readers convert ISO codes to localized names where
human-readable output is required (EOI documents, invoices, portal).
Migration 0016 drops:
- clients.nationality
- companies.incorporation_country
- client_addresses.{state_province, country}
- company_addresses.{state_province, country}
Code paths that previously read free-text values now read the ISO column
and pass through `getCountryName()` / `getSubdivisionName()` for rendering.
Document templates ({{client.nationality}}), portal client view, EOI/
reservation-agreement contexts, and invoice billing addresses all updated.
Public yacht-interest endpoint (/api/public/interests) drops the legacy
fields from its insert path and writes ISO codes only. The Zod validators
no longer accept the legacy fields — older website builds posting raw
'incorporationCountry' / 'country' / 'stateProvince' will get 400s.
Server-side phone normalization is unchanged.
Seed data updated to use ISO codes (GB/FR/ES/GR/SE/IT/GH/MC/PA), spread
across continents to keep test fixtures realistic.
Test assertions updated to match the new render shape (e.g.
'United States' not 'US', 'California' not 'CA').
Vitest: 741 -> 741 (unchanged count; assertions updated, no new tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 19:00:57 +02:00
countryIso : 'GH' ,
2026-04-24 13:26:37 +02:00
postalCode : 'GA-183-1090' ,
street : '21 Independence Ave' ,
} ,
] ;
const clientRows = await tx
. insert ( clients )
. values (
CLIENT_SPECS . map ( ( c ) = > ( {
portId ,
fullName : c.fullName ,
chore(i18n): drop legacy free-text country/nationality columns
Test-data only — no production migration needed (per earlier decision).
Schema is now ISO-only; readers convert ISO codes to localized names where
human-readable output is required (EOI documents, invoices, portal).
Migration 0016 drops:
- clients.nationality
- companies.incorporation_country
- client_addresses.{state_province, country}
- company_addresses.{state_province, country}
Code paths that previously read free-text values now read the ISO column
and pass through `getCountryName()` / `getSubdivisionName()` for rendering.
Document templates ({{client.nationality}}), portal client view, EOI/
reservation-agreement contexts, and invoice billing addresses all updated.
Public yacht-interest endpoint (/api/public/interests) drops the legacy
fields from its insert path and writes ISO codes only. The Zod validators
no longer accept the legacy fields — older website builds posting raw
'incorporationCountry' / 'country' / 'stateProvince' will get 400s.
Server-side phone normalization is unchanged.
Seed data updated to use ISO codes (GB/FR/ES/GR/SE/IT/GH/MC/PA), spread
across continents to keep test fixtures realistic.
Test assertions updated to match the new render shape (e.g.
'United States' not 'US', 'California' not 'CA').
Vitest: 741 -> 741 (unchanged count; assertions updated, no new tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 19:00:57 +02:00
nationalityIso : c.nationalityIso ,
2026-04-24 13:26:37 +02:00
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 ,
chore(i18n): drop legacy free-text country/nationality columns
Test-data only — no production migration needed (per earlier decision).
Schema is now ISO-only; readers convert ISO codes to localized names where
human-readable output is required (EOI documents, invoices, portal).
Migration 0016 drops:
- clients.nationality
- companies.incorporation_country
- client_addresses.{state_province, country}
- company_addresses.{state_province, country}
Code paths that previously read free-text values now read the ISO column
and pass through `getCountryName()` / `getSubdivisionName()` for rendering.
Document templates ({{client.nationality}}), portal client view, EOI/
reservation-agreement contexts, and invoice billing addresses all updated.
Public yacht-interest endpoint (/api/public/interests) drops the legacy
fields from its insert path and writes ISO codes only. The Zod validators
no longer accept the legacy fields — older website builds posting raw
'incorporationCountry' / 'country' / 'stateProvince' will get 400s.
Server-side phone normalization is unchanged.
Seed data updated to use ISO codes (GB/FR/ES/GR/SE/IT/GH/MC/PA), spread
across continents to keep test fixtures realistic.
Test assertions updated to match the new render shape (e.g.
'United States' not 'US', 'California' not 'CA').
Vitest: 741 -> 741 (unchanged count; assertions updated, no new tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 19:00:57 +02:00
subdivisionIso : null ,
2026-04-24 13:26:37 +02:00
postalCode : c.postalCode ,
chore(i18n): drop legacy free-text country/nationality columns
Test-data only — no production migration needed (per earlier decision).
Schema is now ISO-only; readers convert ISO codes to localized names where
human-readable output is required (EOI documents, invoices, portal).
Migration 0016 drops:
- clients.nationality
- companies.incorporation_country
- client_addresses.{state_province, country}
- company_addresses.{state_province, country}
Code paths that previously read free-text values now read the ISO column
and pass through `getCountryName()` / `getSubdivisionName()` for rendering.
Document templates ({{client.nationality}}), portal client view, EOI/
reservation-agreement contexts, and invoice billing addresses all updated.
Public yacht-interest endpoint (/api/public/interests) drops the legacy
fields from its insert path and writes ISO codes only. The Zod validators
no longer accept the legacy fields — older website builds posting raw
'incorporationCountry' / 'country' / 'stateProvince' will get 400s.
Server-side phone normalization is unchanged.
Seed data updated to use ISO codes (GB/FR/ES/GR/SE/IT/GH/MC/PA), spread
across continents to keep test fixtures realistic.
Test assertions updated to match the new render shape (e.g.
'United States' not 'US', 'California' not 'CA').
Vitest: 741 -> 741 (unchanged count; assertions updated, no new tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 19:00:57 +02:00
countryIso : c.countryIso ,
2026-04-24 13:26:37 +02:00
isPrimary : true ,
} ) ) ,
) ;
// ── 4. Memberships ─────────────────────────────────────────────────────
// Index map: clientIds[3..4] → Aegean; [5..6] → Aegean + Blue Seas; [7] → Phantom (ended)
2026-05-04 22:57:01 +02:00
// Aegean total active members: clientIds[3],[4],[5],[6] = 4 - but plan says 3.
2026-04-24 13:26:37 +02:00
// 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
2026-05-04 22:57:01 +02:00
// Blue Seas members: 2 (dual) - but plan says Blue Seas has 1 member.
2026-04-24 13:26:37 +02:00
// 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.
2026-05-04 22:57:01 +02:00
// Then: Aegean has [3],[4],[5],[6] = 4 members (plan said 3 - close enough; the
2026-04-24 13:26:37 +02:00
// plan's "3 members" was intent, the "dual membership" requirement dominates).
//
// Final assignment (respects all cardinality requirements):
2026-05-04 22:57:01 +02:00
// 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)
2026-04-24 13:26:37 +02:00
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 [ ] = [
2026-05-04 22:57:01 +02:00
// Initially client[0] - will be transferred to Aegean
2026-04-24 13:26:37 +02:00
{
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 ,
} ,
2026-05-04 22:57:01 +02:00
// Initially Phantom-owned - will be transferred to clientIds[7] on dissolution
2026-04-24 13:26:37 +02:00
{
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 ) ,
2026-05-04 22:57:01 +02:00
reason : 'Corporate dissolution - asset transfer' ,
2026-04-24 13:26:37 +02:00
} ,
] ;
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 ) ) ;
}
2026-04-24 16:13:51 +02:00
// ── 6b. Standard EOI Template (in-app PDF path) ────────────────────────
// One row per port. Used by the in-app pdfme renderer when the port opts
// for in-app PDF generation over the Documenso template flow.
await tx . insert ( documentTemplates ) . values ( {
portId ,
name : 'Standard EOI (in-app)' ,
description :
'Default Expression of Interest / Letter of Intent template, rendered in-app via pdfme. Use for ports that prefer in-app PDF generation over the Documenso template path.' ,
templateType : 'eoi' ,
bodyHtml : getStandardEoiTemplateHtml ( ) ,
mergeFields : STANDARD_EOI_MERGE_FIELDS ,
isActive : true ,
createdBy : SEED_USER_ID ,
} ) ;
2026-04-24 13:26:37 +02:00
// ── 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
2026-04-24 13:26:37 +02:00
const interestPlan : Array < {
clientIdx : number ;
berthIdx : number | null ;
yachtIdx : number | null ;
pipelineStage :
| 'open'
| 'details_sent'
| 'in_communication'
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
| 'eoi_sent'
| 'eoi_signed'
2026-04-24 13:26:37 +02:00
| 'deposit_10pct'
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
| 'contract_sent'
| 'contract_signed'
2026-04-24 13:26:37 +02:00
| 'completed' ;
leadCategory : 'general_interest' | 'specific_qualified' | 'hot_lead' ;
source : 'website' | 'manual' | 'referral' | 'broker' ;
daysAgoFirst : number ;
archived? : boolean ;
} > = [
{
clientIdx : 0 ,
berthIdx : 0 ,
yachtIdx : 0 ,
pipelineStage : 'open' ,
leadCategory : 'general_interest' ,
source : 'website' ,
daysAgoFirst : 5 ,
} ,
{
clientIdx : 1 ,
berthIdx : 1 ,
yachtIdx : 1 ,
pipelineStage : 'details_sent' ,
leadCategory : 'general_interest' ,
source : 'website' ,
daysAgoFirst : 12 ,
} ,
{
clientIdx : 2 ,
berthIdx : 2 ,
yachtIdx : 2 ,
pipelineStage : 'in_communication' ,
leadCategory : 'specific_qualified' ,
source : 'referral' ,
daysAgoFirst : 25 ,
} ,
{
clientIdx : 3 ,
berthIdx : 3 ,
yachtIdx : 6 ,
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
pipelineStage : 'eoi_sent' ,
2026-04-24 13:26:37 +02:00
leadCategory : 'specific_qualified' ,
source : 'referral' ,
daysAgoFirst : 40 ,
} ,
{
clientIdx : 4 ,
berthIdx : 4 ,
yachtIdx : null ,
pipelineStage : 'open' ,
leadCategory : 'general_interest' ,
source : 'broker' ,
daysAgoFirst : 8 ,
} ,
{
clientIdx : 5 ,
berthIdx : 5 ,
yachtIdx : 3 ,
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
pipelineStage : 'eoi_signed' ,
2026-04-24 13:26:37 +02:00
leadCategory : 'hot_lead' ,
source : 'manual' ,
daysAgoFirst : 55 ,
} ,
{
clientIdx : 6 ,
berthIdx : 6 ,
yachtIdx : 4 ,
pipelineStage : 'deposit_10pct' ,
leadCategory : 'hot_lead' ,
source : 'referral' ,
daysAgoFirst : 70 ,
} ,
{
clientIdx : 0 ,
berthIdx : 7 ,
yachtIdx : 5 ,
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
pipelineStage : 'contract_signed' ,
2026-04-24 13:26:37 +02:00
leadCategory : 'hot_lead' ,
source : 'broker' ,
daysAgoFirst : 90 ,
} ,
{
clientIdx : 1 ,
berthIdx : 10 ,
yachtIdx : 1 ,
pipelineStage : 'completed' ,
leadCategory : 'hot_lead' ,
source : 'referral' ,
daysAgoFirst : 240 ,
} ,
{
clientIdx : 7 ,
berthIdx : 11 ,
yachtIdx : 11 ,
pipelineStage : 'completed' ,
leadCategory : 'hot_lead' ,
source : 'manual' ,
daysAgoFirst : 320 ,
} ,
{
clientIdx : 2 ,
berthIdx : null ,
yachtIdx : null ,
pipelineStage : 'open' ,
leadCategory : 'general_interest' ,
source : 'website' ,
daysAgoFirst : 3 ,
} ,
{
clientIdx : 3 ,
berthIdx : 8 ,
yachtIdx : 6 ,
pipelineStage : 'in_communication' ,
leadCategory : 'specific_qualified' ,
source : 'website' ,
daysAgoFirst : 18 ,
} ,
{
clientIdx : 5 ,
berthIdx : null ,
yachtIdx : 3 ,
pipelineStage : 'details_sent' ,
leadCategory : 'general_interest' ,
source : 'referral' ,
daysAgoFirst : 10 ,
} ,
2026-05-04 22:57:01 +02:00
// "Lost" - modeled as archived + open stage
2026-04-24 13:26:37 +02:00
{
clientIdx : 4 ,
berthIdx : 2 ,
yachtIdx : null ,
pipelineStage : 'open' ,
leadCategory : 'general_interest' ,
source : 'website' ,
daysAgoFirst : 180 ,
archived : true ,
} ,
{
clientIdx : 6 ,
berthIdx : 9 ,
yachtIdx : 4 ,
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
pipelineStage : 'eoi_sent' ,
2026-04-24 13:26:37 +02:00
leadCategory : 'specific_qualified' ,
source : 'broker' ,
daysAgoFirst : 45 ,
} ,
] ;
await tx . insert ( interests ) . values (
interestPlan . map ( ( p ) = > ( {
portId ,
clientId : clientIds [ p . clientIdx ] ! ,
berthId : p.berthIdx !== null ? berthRows [ p . berthIdx ] ! . id : null ,
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 ,
} ) ) ,
) ;
// ── 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).
2026-05-04 22:57:01 +02:00
// Ended: berths 10 and 11 (sold) - use historical start/end dates.
// Cancelled: berth 0 (available - a cancelled res doesn't occupy it).
2026-04-24 13:26:37 +02:00
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 } ;
const reservationValues : Array < typeof berthReservations. $ 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.' ,
} ) ;
await tx . insert ( berthReservations ) . values ( reservationValues ) ;
return {
berths : berthRows.length ,
clients : clientRows.length ,
companies : companyRows.length ,
yachts : yachtRows.length ,
interests : interestPlan.length ,
reservations : reservationValues.length ,
} ;
} ) ;
}