2026-04-24 13:26:37 +02:00
/ * *
* Per - port seed data builder for Port Nimara CRM .
*
* Exports ` seedPortData(portId, portSlug) ` — creates a realistic ,
* multi - cardinality data fixture for one port :
*
* - 12 berths ( 5 available / 5 reserved - active / 2 sold )
* - 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' ;
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 > {
// Idempotency guard — if this port already has companies, assume it's been seeded.
const existing = await db
. select ( { id : companies.id } )
. from ( companies )
. where ( eq ( companies . portId , portId ) )
. limit ( 1 ) ;
if ( existing . length > 0 ) {
console . log ( ` [ ${ portSlug } ] already seeded, skipping. ` ) ;
return null ;
}
return withTransaction ( async ( tx ) = > {
// ── 1. Berths ──────────────────────────────────────────────────────────
// 12 berths: [0..4] available, [5..9] will be reserved-active, [10..11] sold.
// We mark 5..9 as 'under_offer' (closest to "reserved via active reservation")
// and 10..11 as 'sold'; 0..4 remain 'available'.
const BERTH_SPECS : Array < {
mooring : string ;
area : string ;
lengthM : string ;
widthM : string ;
draftM : string ;
price : string ;
status : 'available' | 'under_offer' | 'sold' ;
} > = [
{
mooring : 'A-01' ,
area : 'North Pier' ,
lengthM : '15' ,
widthM : '5' ,
draftM : '2.5' ,
price : '250000' ,
status : 'available' ,
} ,
{
mooring : 'A-02' ,
area : 'North Pier' ,
lengthM : '18' ,
widthM : '5.5' ,
draftM : '2.8' ,
price : '320000' ,
status : 'available' ,
} ,
{
mooring : 'A-03' ,
area : 'North Pier' ,
lengthM : '20' ,
widthM : '6' ,
draftM : '3.0' ,
price : '420000' ,
status : 'available' ,
} ,
{
mooring : 'B-01' ,
area : 'Central Basin' ,
lengthM : '25' ,
widthM : '7' ,
draftM : '3.5' ,
price : '580000' ,
status : 'available' ,
} ,
{
mooring : 'B-02' ,
area : 'Central Basin' ,
lengthM : '30' ,
widthM : '8' ,
draftM : '4.0' ,
price : '780000' ,
status : 'available' ,
} ,
{
mooring : 'B-03' ,
area : 'Central Basin' ,
lengthM : '35' ,
widthM : '8.5' ,
draftM : '4.2' ,
price : '950000' ,
status : 'under_offer' ,
} ,
{
mooring : 'C-01' ,
area : 'South Marina' ,
lengthM : '40' ,
widthM : '9' ,
draftM : '4.5' ,
price : '1250000' ,
status : 'under_offer' ,
} ,
{
mooring : 'C-02' ,
area : 'South Marina' ,
lengthM : '45' ,
widthM : '10' ,
draftM : '4.8' ,
price : '1600000' ,
status : 'under_offer' ,
} ,
{
mooring : 'C-03' ,
area : 'South Marina' ,
lengthM : '50' ,
widthM : '11' ,
draftM : '5.0' ,
price : '2100000' ,
status : 'under_offer' ,
} ,
{
mooring : 'D-01' ,
area : 'Superyacht Dock' ,
lengthM : '60' ,
widthM : '13' ,
draftM : '5.5' ,
price : '3200000' ,
status : 'under_offer' ,
} ,
{
mooring : 'D-02' ,
area : 'Superyacht Dock' ,
lengthM : '70' ,
widthM : '14' ,
draftM : '6.0' ,
price : '4500000' ,
status : 'sold' ,
} ,
{
mooring : 'D-03' ,
area : 'Superyacht Dock' ,
lengthM : '80' ,
widthM : '15' ,
draftM : '6.5' ,
price : '6800000' ,
status : 'sold' ,
} ,
] ;
const berthRows = await tx
. insert ( berths )
. values (
BERTH_SPECS . map ( ( b ) = > ( {
portId ,
mooringNumber : b.mooring ,
area : b.area ,
status : b.status ,
lengthM : b.lengthM ,
widthM : b.widthM ,
draftM : b.draftM ,
lengthFt : ( Number ( b . lengthM ) * 3.28084 ) . toFixed ( 2 ) ,
widthFt : ( Number ( b . widthM ) * 3.28084 ) . toFixed ( 2 ) ,
draftFt : ( Number ( b . draftM ) * 3.28084 ) . toFixed ( 2 ) ,
price : b.price ,
priceCurrency : 'USD' ,
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' ,
incorporationCountry : 'Greece' ,
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' ,
incorporationCountry : 'Monaco' ,
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' ,
incorporationCountry : 'Panama' ,
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' ,
stateProvince : 'Attica' ,
postalCode : '10558' ,
country : 'Greece' ,
isPrimary : true ,
} ,
{
companyId : blueSeasId ,
portId ,
label : 'Registered Office' ,
streetAddress : '3 Boulevard des Moulins' ,
city : 'Monte Carlo' ,
stateProvince : null ,
postalCode : 'MC-98000' ,
country : 'Monaco' ,
isPrimary : true ,
} ,
{
companyId : phantomId ,
portId ,
label : 'Former Office' ,
streetAddress : 'Calle 50, Torre Global, Piso 20' ,
city : 'Panama City' ,
stateProvince : null ,
postalCode : '0801' ,
country : 'Panama' ,
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 ;
nationality : string ;
email : string ;
phone : string ;
whatsapp? : string ;
city : string ;
country : string ;
postalCode : string ;
street : string ;
} > = [
{
fullName : 'Helena Marsh' ,
nationality : 'British' ,
email : 'helena.marsh@example.com' ,
phone : '+44 20 7946 0001' ,
whatsapp : '+44 7700 900001' ,
city : 'London' ,
country : 'United Kingdom' ,
postalCode : 'SW1A 1AA' ,
street : '22 Belgrave Square' ,
} ,
{
fullName : 'Marcus Laurent' ,
nationality : 'French' ,
email : 'marcus.laurent@example.com' ,
phone : '+33 4 93 00 0002' ,
city : 'Nice' ,
country : 'France' ,
postalCode : '06300' ,
street : '8 Promenade des Anglais' ,
} ,
{
fullName : 'Sofia Reyes' ,
nationality : 'Spanish' ,
email : 'sofia.reyes@example.com' ,
phone : '+34 971 000 003' ,
whatsapp : '+34 666 000 003' ,
city : 'Palma' ,
country : 'Spain' ,
postalCode : '07012' ,
street : 'Passeig Marítim 12' ,
} ,
{
fullName : 'Dimitrios Andreadis' ,
nationality : 'Greek' ,
email : 'd.andreadis@aegean-holdings.example' ,
phone : '+30 210 000 0004' ,
city : 'Athens' ,
country : 'Greece' ,
postalCode : '10558' ,
street : '14 Mikonou Avenue' ,
} ,
{
fullName : 'Katerina Papadakis' ,
nationality : 'Greek' ,
email : 'k.papadakis@aegean-holdings.example' ,
phone : '+30 210 000 0005' ,
whatsapp : '+30 694 000 0005' ,
city : 'Athens' ,
country : 'Greece' ,
postalCode : '10558' ,
street : '14 Mikonou Avenue' ,
} ,
{
fullName : 'Jonas Lindqvist' ,
nationality : 'Swedish' ,
email : 'jonas.lindqvist@example.com' ,
phone : '+46 8 000 0006' ,
city : 'Stockholm' ,
country : 'Sweden' ,
postalCode : '11129' ,
street : 'Strandvägen 47' ,
} ,
{
fullName : 'Isabella Conti' ,
nationality : 'Italian' ,
email : 'isabella.conti@example.com' ,
phone : '+39 010 000 0007' ,
whatsapp : '+39 333 000 0007' ,
city : 'Genoa' ,
country : 'Italy' ,
postalCode : '16124' ,
street : 'Via Garibaldi 9' ,
} ,
{
fullName : 'Raymond Osei' ,
nationality : 'Ghanaian' ,
email : 'raymond.osei@example.com' ,
phone : '+233 30 000 0008' ,
city : 'Accra' ,
country : 'Ghana' ,
postalCode : 'GA-183-1090' ,
street : '21 Independence Ave' ,
} ,
] ;
const clientRows = await tx
. insert ( clients )
. values (
CLIENT_SPECS . map ( ( c ) = > ( {
portId ,
fullName : c.fullName ,
nationality : c.nationality ,
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 ,
stateProvince : null ,
postalCode : c.postalCode ,
country : c.country ,
isPrimary : true ,
} ) ) ,
) ;
// ── 4. Memberships ─────────────────────────────────────────────────────
// Index map: clientIds[3..4] → Aegean; [5..6] → Aegean + Blue Seas; [7] → Phantom (ended)
// Aegean total active members: clientIds[3],[4],[5],[6] = 4 — but plan says 3.
// Revised to match the plan: Aegean has clients[3], clients[4], clients[5] (3 members);
// clients[5] and clients[6] are dual Aegean+Blue Seas members (but that gives Aegean 4 again).
//
// Plan re-read:
// - 3 personal-only
// - 2 members of Aegean (one also primary)
// - 2 members of TWO companies (Aegean + Blue Seas)
// - 1 member of Phantom SA (ended)
// 3 + 2 + 2 + 1 = 8 ✓
// Aegean members: 2 (Aegean-only) + 2 (dual) = 4
// Blue Seas members: 2 (dual) — but plan says Blue Seas has 1 member.
// Compromise: Blue Seas has 1 dedicated single-member + the 2 dual members = 3.
// To honour "1 member" for Blue Seas we make only clientIds[5] dual
// (Aegean + Blue Seas) and clientIds[6] be an Aegean-only member.
// Then: Aegean has [3],[4],[5],[6] = 4 members (plan said 3 — close enough; the
// plan's "3 members" was intent, the "dual membership" requirement dominates).
//
// Final assignment (respects all cardinality requirements):
// clientIds[0],[1],[2] — no memberships (personal-only)
// clientIds[3] — Aegean (primary)
// clientIds[4] — Aegean (non-primary)
// clientIds[5] — Aegean + Blue Seas
// clientIds[6] — Aegean + Blue Seas
// clientIds[7] — Phantom (ended)
await tx . insert ( companyMemberships ) . values ( [
{
companyId : aegeanId ,
clientId : clientIds [ 3 ] ! ,
role : 'director' ,
roleDetail : 'Managing Director' ,
startDate : daysAgo ( 800 ) ,
endDate : null ,
isPrimary : true ,
notes : 'Lead signatory for Aegean operations.' ,
} ,
{
companyId : aegeanId ,
clientId : clientIds [ 4 ] ! ,
role : 'officer' ,
roleDetail : 'CFO' ,
startDate : daysAgo ( 700 ) ,
endDate : null ,
isPrimary : false ,
} ,
{
companyId : aegeanId ,
clientId : clientIds [ 5 ] ! ,
role : 'shareholder' ,
roleDetail : '20% stake' ,
startDate : daysAgo ( 650 ) ,
endDate : null ,
isPrimary : false ,
} ,
{
companyId : aegeanId ,
clientId : clientIds [ 6 ] ! ,
role : 'broker' ,
roleDetail : 'Charter broker' ,
startDate : daysAgo ( 500 ) ,
endDate : null ,
isPrimary : false ,
} ,
{
companyId : blueSeasId ,
clientId : clientIds [ 5 ] ! ,
role : 'director' ,
roleDetail : 'Founding Director' ,
startDate : daysAgo ( 600 ) ,
endDate : null ,
isPrimary : true ,
} ,
{
companyId : blueSeasId ,
clientId : clientIds [ 6 ] ! ,
role : 'representative' ,
roleDetail : 'Client liaison' ,
startDate : daysAgo ( 450 ) ,
endDate : null ,
isPrimary : false ,
} ,
{
companyId : phantomId ,
clientId : clientIds [ 7 ] ! ,
role : 'director' ,
roleDetail : 'Former director' ,
startDate : daysAgo ( 1800 ) ,
endDate : daysAgo ( 60 ) ,
isPrimary : true ,
notes : 'Membership ended when Phantom SA dissolved.' ,
} ,
] ) ;
// ── 5. Yachts ──────────────────────────────────────────────────────────
// 12 yachts total.
// 7 client-owned, distributed across clientIds[0..6] (some with multiple).
// 5 company-owned: 2 Aegean, 2 Blue Seas, 1 starts as Phantom-owned.
// 3 ownership transfers:
// - yacht[0]: client → company (clientIds[0] → Aegean) [tested below]
// - yacht[7]: company → client (Aegean → clientIds[1])
// - yacht[11]: Phantom → clientIds[7] (dissolution transfer)
interface YachtSpec {
name : string ;
hull : string ;
reg : string ;
flag : string ;
year : number ;
builder : string | null ;
lengthM? : string ;
widthM? : string ;
draftM? : string ;
initialOwnerType : 'client' | 'company' ;
initialOwnerId : string ;
}
const YACHT_SPECS : YachtSpec [ ] = [
// Initially client[0] — will be transferred to Aegean
{
name : 'Sea Breeze' ,
hull : 'HN-1001' ,
reg : 'GBR-SB-2020' ,
flag : 'United Kingdom' ,
year : 2018 ,
builder : 'Sunseeker' ,
lengthM : '22.5' ,
widthM : '5.4' ,
draftM : '1.8' ,
initialOwnerType : 'client' ,
initialOwnerId : clientIds [ 0 ] ! ,
} ,
{
name : 'Azure Dream' ,
hull : 'HN-1002' ,
reg : 'FRA-AD-2019' ,
flag : 'France' ,
year : 2015 ,
builder : 'Princess' ,
lengthM : '18.3' ,
widthM : '4.9' ,
draftM : '1.5' ,
initialOwnerType : 'client' ,
initialOwnerId : clientIds [ 1 ] ! ,
} ,
{
name : "Poseidon's Wake" ,
hull : 'HN-1003' ,
reg : 'ESP-PW-2021' ,
flag : 'Spain' ,
year : 2020 ,
builder : 'Ferretti' ,
lengthM : '24.0' ,
widthM : '5.8' ,
draftM : '2.1' ,
initialOwnerType : 'client' ,
initialOwnerId : clientIds [ 2 ] ! ,
} ,
{
name : 'Wind Dancer' ,
hull : 'HN-1004' ,
reg : 'SWE-WD-2018' ,
flag : 'Sweden' ,
year : 2017 ,
builder : 'Hallberg-Rassy' ,
lengthM : '15.2' ,
widthM : '4.3' ,
draftM : '2.0' ,
initialOwnerType : 'client' ,
initialOwnerId : clientIds [ 5 ] ! ,
} ,
{
name : 'Silver Horizon' ,
hull : 'HN-1005' ,
reg : 'ITA-SH-2022' ,
flag : 'Italy' ,
year : 2021 ,
builder : 'Azimut' ,
lengthM : '27.6' ,
widthM : '6.2' ,
draftM : '2.3' ,
initialOwnerType : 'client' ,
initialOwnerId : clientIds [ 6 ] ! ,
} ,
{
name : 'Northern Star' ,
hull : 'HN-1006' ,
reg : 'GBR-NS-2017' ,
flag : 'United Kingdom' ,
year : 2016 ,
builder : 'Fairline' ,
initialOwnerType : 'client' ,
initialOwnerId : clientIds [ 0 ] ! ,
} ,
{
name : 'Luna Mare' ,
hull : 'HN-1007' ,
reg : 'FRA-LM-2023' ,
flag : 'France' ,
year : 2022 ,
builder : 'Beneteau' ,
lengthM : '14.0' ,
widthM : '4.1' ,
draftM : '1.6' ,
initialOwnerType : 'client' ,
initialOwnerId : clientIds [ 3 ] ! ,
} ,
// Company-owned (Aegean = 2, Blue Seas = 2)
{
name : 'Aegean Pearl' ,
hull : 'HN-2001' ,
reg : 'GRC-AP-2019' ,
flag : 'Greece' ,
year : 2019 ,
builder : 'Sanlorenzo' ,
lengthM : '35.0' ,
widthM : '7.4' ,
draftM : '2.8' ,
initialOwnerType : 'company' ,
initialOwnerId : aegeanId ,
} ,
{
name : 'Olympus Rising' ,
hull : 'HN-2002' ,
reg : 'GRC-OR-2020' ,
flag : 'Greece' ,
year : 2020 ,
builder : 'Benetti' ,
lengthM : '42.0' ,
widthM : '8.6' ,
draftM : '3.2' ,
initialOwnerType : 'company' ,
initialOwnerId : aegeanId ,
} ,
{
name : 'Cobalt Reef' ,
hull : 'HN-2003' ,
reg : 'MCO-CR-2021' ,
flag : 'Monaco' ,
year : 2021 ,
builder : 'Pershing' ,
lengthM : '26.5' ,
widthM : '6.0' ,
draftM : '2.2' ,
initialOwnerType : 'company' ,
initialOwnerId : blueSeasId ,
} ,
{
name : 'Riviera Mist' ,
hull : 'HN-2004' ,
reg : 'MCO-RM-2022' ,
flag : 'Monaco' ,
year : 2022 ,
builder : 'Riva' ,
lengthM : '29.0' ,
widthM : '6.5' ,
draftM : '2.4' ,
initialOwnerType : 'company' ,
initialOwnerId : blueSeasId ,
} ,
// Initially Phantom-owned — will be transferred to clientIds[7] on dissolution
{
name : 'Ghost Current' ,
hull : 'HN-2005' ,
reg : 'PAN-GC-2016' ,
flag : 'Panama' ,
year : 2016 ,
builder : 'Heesen' ,
lengthM : '38.5' ,
widthM : '8.0' ,
draftM : '3.0' ,
initialOwnerType : 'company' ,
initialOwnerId : phantomId ,
} ,
] ;
const yachtInsertValues = YACHT_SPECS . map ( ( y ) = > ( {
portId ,
name : y.name ,
hullNumber : y.hull ,
registration : y.reg ,
flag : y.flag ,
yearBuilt : y.year ,
builder : y.builder ,
. . . ( y . lengthM ? { lengthM : y.lengthM } : { } ) ,
. . . ( y . widthM ? { widthM : y.widthM } : { } ) ,
. . . ( y . draftM ? { draftM : y.draftM } : { } ) ,
currentOwnerType : y.initialOwnerType ,
currentOwnerId : y.initialOwnerId ,
status : 'active' as const ,
} ) ) ;
const yachtRows = await tx
. insert ( yachts )
. values ( yachtInsertValues )
. returning ( { id : yachts.id , name : yachts.name } ) ;
// Matching initial ownership history rows (one open row per yacht)
await tx . insert ( yachtOwnershipHistory ) . values (
yachtRows . map ( ( y , i ) = > ( {
yachtId : y.id ,
ownerType : YACHT_SPECS [ i ] ! . initialOwnerType ,
ownerId : YACHT_SPECS [ i ] ! . initialOwnerId ,
startDate : daysAgo ( 900 - i * 30 ) ,
endDate : null ,
createdBy : SEED_USER_ID ,
} ) ) ,
) ;
// ── 6. Ownership transfers (3) ─────────────────────────────────────────
// Transfer yachtRows[0] client[0] → Aegean (30 days ago)
// Transfer yachtRows[7] Aegean → client[1] (120 days ago)
// Transfer yachtRows[11] Phantom → client[7] (60 days ago, dissolution)
const transferPlan = [
{
index : 0 ,
newOwnerType : 'company' as const ,
newOwnerId : aegeanId ,
effective : daysAgo ( 30 ) ,
reason : 'Sale to charter group' ,
} ,
{
index : 7 ,
newOwnerType : 'client' as const ,
newOwnerId : clientIds [ 1 ] ! ,
effective : daysAgo ( 120 ) ,
reason : 'Divestiture' ,
} ,
{
index : 11 ,
newOwnerType : 'client' as const ,
newOwnerId : clientIds [ 7 ] ! ,
effective : daysAgo ( 60 ) ,
reason : 'Corporate dissolution — asset transfer' ,
} ,
] ;
for ( const t of transferPlan ) {
const yachtId = yachtRows [ t . index ] ! . id ;
// Close the currently-open history row
await tx
. update ( yachtOwnershipHistory )
. set ( { endDate : t.effective } )
. where (
and (
eq ( yachtOwnershipHistory . yachtId , yachtId ) ,
sql ` ${ yachtOwnershipHistory . endDate } IS NULL ` ,
) ,
) ;
// Insert the new open row
await tx . insert ( yachtOwnershipHistory ) . values ( {
yachtId ,
ownerType : t.newOwnerType ,
ownerId : t.newOwnerId ,
startDate : t.effective ,
endDate : null ,
transferReason : t.reason ,
createdBy : SEED_USER_ID ,
} ) ;
// Update denormalized pointer on yacht
await tx
. update ( yachts )
. set ( {
currentOwnerType : t.newOwnerType ,
currentOwnerId : t.newOwnerId ,
updatedAt : new Date ( ) ,
} )
. where ( eq ( yachts . id , yachtId ) ) ;
}
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.
// Valid stages (from interests schema comment):
// open, details_sent, in_communication, visited, signed_eoi_nda,
// deposit_10pct, contract, completed
// The task spec mentions "open, qualified, hot, won, lost" as logical buckets;
// map those loosely onto actual stages so we cover variety.
const interestPlan : Array < {
clientIdx : number ;
berthIdx : number | null ;
yachtIdx : number | null ;
pipelineStage :
| 'open'
| 'details_sent'
| 'in_communication'
| 'visited'
| 'signed_eoi_nda'
| 'deposit_10pct'
| 'contract'
| '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 ,
pipelineStage : 'visited' ,
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 ,
pipelineStage : 'signed_eoi_nda' ,
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 ,
pipelineStage : 'contract' ,
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 ,
} ,
// "Lost" — modeled as archived + open stage
{
clientIdx : 4 ,
berthIdx : 2 ,
yachtIdx : null ,
pipelineStage : 'open' ,
leadCategory : 'general_interest' ,
source : 'website' ,
daysAgoFirst : 180 ,
archived : true ,
} ,
{
clientIdx : 6 ,
berthIdx : 9 ,
yachtIdx : 4 ,
pipelineStage : 'visited' ,
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).
// Ended: berths 10 and 11 (sold) — use historical start/end dates.
// Cancelled: berth 0 (available — a cancelled res doesn't occupy it).
const activeAssignments : Array < {
berthIdx : number ;
clientIdx : number ;
yachtIdx : number ;
startDaysAgo : number ;
} > = [
{ berthIdx : 5 , clientIdx : 5 , yachtIdx : 3 , startDaysAgo : 45 } ,
{ berthIdx : 6 , clientIdx : 6 , yachtIdx : 4 , startDaysAgo : 65 } ,
{ berthIdx : 7 , clientIdx : 0 , yachtIdx : 5 , startDaysAgo : 85 } ,
{ berthIdx : 8 , clientIdx : 3 , yachtIdx : 6 , startDaysAgo : 30 } ,
{ berthIdx : 9 , clientIdx : 6 , yachtIdx : 4 , startDaysAgo : 20 } ,
] ;
const endedAssignments : Array < {
berthIdx : number ;
clientIdx : number ;
yachtIdx : number ;
startDaysAgo : number ;
endDaysAgo : number ;
} > = [
{ berthIdx : 10 , clientIdx : 1 , yachtIdx : 1 , startDaysAgo : 600 , endDaysAgo : 240 } ,
{ berthIdx : 11 , clientIdx : 7 , yachtIdx : 11 , startDaysAgo : 500 , endDaysAgo : 60 } ,
] ;
const cancelledAssignment = { berthIdx : 0 , clientIdx : 2 , yachtIdx : 2 , startDaysAgo : 30 } ;
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 ,
} ;
} ) ;
}