Files
pn-new-crm/src/lib/db/seed-synthetic-data.ts
Matt 221ae5784e chore(autonomous-session): consolidate uncommitted work from prior session
Bundles the prior autonomous-session output that was sitting unstaged:

- Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances)
- country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that
  never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk
  after the per-subpath dynamic-import approach silently failed in webpack)
- Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index,
  redirects (ocr to ai, reports to dashboard, invitations to users),
  docs/admin-ia-proposal.md
- Per-template email tester (registry + endpoint + UI on Email admin page)
- Cancel-document mode picker (delete-from-Documenso vs keep-for-audit)
- Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers
- Customize-widgets per-region sortables at xl+ (charts/rails/feed); single
  flat sortable below xl when the layout stacks; per-viewport saved orders
- Audit doc updates capturing each shipped item
- Lint fixes: react-compiler immutability in DonutChart (reduce instead of
  let-reassign), set-state-in-effect disables in CountryFlag and
  UploadForSigning preview-bytes effect, unused 'confirm' destructures in
  interest contract + reservation tabs, unescaped apostrophe in test-template
  card copy
2026-05-23 00:52:59 +02:00

829 lines
27 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Per-port synthetic seed builder for "every pipeline stage" coverage.
*
* The realistic seed in `seed-data.ts` mirrors the legacy NocoDB shape;
* this one is purpose-built for thoroughly exercising the CRM. Every
* pipeline stage gets at least one client, plus a handful of edge-case
* fixtures (multi-interest, signed-EOI, archived with metadata, hard-
* delete-eligible, company member, yacht owner).
*
* Berths come from the same NocoDB snapshot so the public berth API
* still has data; the synthetic clients link to specific moorings so
* the under_offer / sold derivations are deterministic.
*
* Idempotent: skips if the port already has clients seeded.
*
* Run via `pnpm db:seed:synthetic`.
*/
import { eq } from 'drizzle-orm';
import { db } from './index';
import { withTransaction } from './utils';
import {
clients,
clientContacts,
clientAddresses,
companies,
companyMemberships,
companyAddresses,
yachts,
yachtOwnershipHistory,
berths,
berthReservations,
interests,
interestBerths,
} from './schema';
import { residentialClients, residentialInterests } from './schema';
import { SUPER_ADMIN_USER_ID } from './seed-bootstrap';
import berthSnapshot from './seed-data/berths.json';
import type { PipelineStage } from '@/lib/constants';
import type { ArchiveMetadata } from '@/lib/services/client-archive.service';
type SeedBerth = {
legacyId: number;
mooringNumber: string;
area: string | null;
status: 'available' | 'under_offer' | 'sold';
lengthFt: number | null;
widthFt: number | null;
draftFt: number | null;
lengthM: number | null;
widthM: number | null;
draftM: number | null;
widthIsMinimum: boolean;
nominalBoatSize: number | null;
nominalBoatSizeM: number | null;
waterDepth: number | null;
waterDepthM: number | null;
waterDepthIsMinimum: boolean;
sidePontoon: string | null;
powerCapacity: number | null;
voltage: number | null;
mooringType: string | null;
cleatType: string | null;
cleatCapacity: string | null;
bollardType: string | null;
bollardCapacity: string | null;
access: string | null;
price: number | null;
bowFacing: string | null;
berthApproved: boolean;
statusOverrideMode: string | null;
};
const BERTH_SNAPSHOT = berthSnapshot as SeedBerth[];
function daysAgo(n: number): Date {
return new Date(Date.now() - n * 86_400_000);
}
export interface SyntheticSeedSummary {
berths: number;
clients: number;
interests: number;
companies: number;
yachts: number;
residentialClients: number;
}
interface SyntheticClientSpec {
/** Stable identifier used by Playwright selectors and intra-seed wiring
* (memberships, yachts, etc.). Decoupled from the display name so the
* rendered list looks like real client data, not test fixtures. */
tag: string;
fullName: string;
email: string;
phone: string;
countryIso: string;
city: string;
street: string;
postalCode: string;
/** Pipeline stage of the (single) interest. Omit for archived-only clients. */
stage?: PipelineStage;
/** Sub-status badges for the doc-signing stages (eoi / reservation / contract).
* Only meaningful when stage matches; otherwise null/undefined. */
eoiDocStatus?: 'sent' | 'signed';
reservationDocStatus?: 'sent' | 'signed';
contractDocStatus?: 'sent' | 'signed';
/** Index into BERTH_SNAPSHOT for the primary linked berth. */
berthIdx?: number;
/** Mark interest as won/lost when stage = contract+signed. */
outcome?: 'won' | 'lost_unqualified' | 'lost_no_response';
/** Archive the CLIENT after creation. When 'rich', fabricate
* archive_metadata so the smart-restore wizard surfaces reversals. */
archive?: 'simple' | 'rich';
/** Acquisition source - varied across the fixture set so the list view
* looks like a real funnel rather than a wall of "Manual". */
source?: 'website' | 'manual' | 'referral' | 'broker';
/** How long ago (in days) this client record was created. Spreads the
* "Created" column across realistic timestamps so list pages look like
* a real CRM with months of history rather than 12 rows all stamped
* with today's date. */
createdDaysAgo?: number;
}
/**
* Each spec produces exactly one client + one interest at the given
* stage. Clients are tagged so a Playwright test can locate them by
* either name (full name) or tag (substring after the dash).
*
* Berth indices map deterministically into the NocoDB snapshot which is
* pre-sorted: idx 0..4 available, 5..9 under_offer, 10..11 sold.
*/
/**
* Believable demo dataset - names, emails, phone numbers, addresses, and
* acquisition sources read like a real marina's prospect list rather
* than fixtures keyed on enum names. The `tag` field still carries the
* stage/role identity for selectors and intra-seed wiring; nothing in
* the rendered UI references it.
*
* Spread across acquisition sources, ages (3280 days), and countries
* so list / dashboard / kanban surfaces look populated and natural.
*/
const PIPELINE_CLIENTS: SyntheticClientSpec[] = [
{
tag: 'open',
fullName: 'Olivia Sinclair',
email: 'olivia.sinclair@gmail.com',
phone: '+44 7700 900142',
countryIso: 'GB',
city: 'London',
street: '14 Cheyne Walk',
postalCode: 'SW3 5RA',
stage: 'enquiry',
source: 'website',
createdDaysAgo: 4,
// Open stage: no berth link yet
},
{
tag: 'details',
fullName: 'Daniel Whitaker',
email: 'daniel.whitaker@outlook.com',
phone: '+1 305 555 0182',
countryIso: 'US',
city: 'Miami',
street: '880 Brickell Bay Drive',
postalCode: '33131',
stage: 'enquiry',
berthIdx: 0,
source: 'broker',
createdDaysAgo: 12,
},
{
tag: 'comms',
fullName: 'Carla Mendoza',
email: 'carla.mendoza@gmail.com',
phone: '+34 971 555 028',
countryIso: 'ES',
city: 'Palma de Mallorca',
street: 'Carrer de Sant Magí 23',
postalCode: '07013',
stage: 'qualified',
berthIdx: 5,
source: 'referral',
createdDaysAgo: 28,
},
{
tag: 'eoi-sent',
fullName: 'Marco Bianchi',
email: 'marco.bianchi@libero.it',
phone: '+39 010 8740 215',
countryIso: 'IT',
city: 'Genoa',
street: 'Via XX Settembre 47',
postalCode: '16121',
stage: 'eoi',
eoiDocStatus: 'sent',
berthIdx: 6,
source: 'broker',
createdDaysAgo: 45,
},
{
tag: 'eoi-signed',
fullName: 'Sara Laurent',
email: 'sara.laurent@orange.fr',
phone: '+33 4 93 92 18 47',
countryIso: 'FR',
city: 'Nice',
street: '8 Promenade des Anglais',
postalCode: '06000',
stage: 'eoi',
eoiDocStatus: 'signed',
berthIdx: 7,
source: 'website',
createdDaysAgo: 72,
},
{
tag: 'deposit',
fullName: 'Nikolas Papadakis',
email: 'n.papadakis@gmail.com',
phone: '+30 210 8945 612',
countryIso: 'GR',
city: 'Athens',
street: 'Vouliagmenis Avenue 142',
postalCode: '16674',
stage: 'deposit_paid',
berthIdx: 8,
source: 'referral',
createdDaysAgo: 95,
},
{
tag: 'contract-sent',
fullName: 'Connor Murphy',
email: 'connor.murphy@me.com',
phone: '+353 1 555 0184',
countryIso: 'IE',
city: 'Dublin',
street: '12 Merrion Square North',
postalCode: 'D02 E2X3',
stage: 'contract',
contractDocStatus: 'sent',
berthIdx: 9,
source: 'manual',
createdDaysAgo: 118,
},
{
tag: 'contract-signed',
fullName: 'Carmen Costa',
email: 'carmen.costa@sapo.pt',
phone: '+351 21 386 4520',
countryIso: 'PT',
city: 'Lisbon',
street: 'Rua Garrett 88',
postalCode: '1200-205',
stage: 'contract',
contractDocStatus: 'signed',
berthIdx: 4,
source: 'broker',
createdDaysAgo: 156,
},
{
tag: 'completed-won',
fullName: 'Carlos Vega',
email: 'carlos.vega@gmail.com',
phone: '+507 6612 4485',
countryIso: 'PA',
city: 'Panama City',
street: 'Calle 50, Torre Banistmo Piso 18',
postalCode: '0816',
stage: 'contract',
contractDocStatus: 'signed',
berthIdx: 10,
outcome: 'won',
source: 'referral',
createdDaysAgo: 245,
},
{
tag: 'completed-lost',
fullName: 'Hannah Schmidt',
email: 'hannah.schmidt@gmx.de',
phone: '+49 40 4286 9152',
countryIso: 'DE',
city: 'Hamburg',
street: 'Alsterufer 28',
postalCode: '20354',
stage: 'enquiry',
berthIdx: 1,
outcome: 'lost_unqualified',
source: 'website',
createdDaysAgo: 84,
},
{
tag: 'archived-simple',
fullName: 'Anna de Jong',
email: 'anna.dejong@kpn.nl',
phone: '+31 20 624 7185',
countryIso: 'NL',
city: 'Amsterdam',
street: 'Herengracht 412',
postalCode: '1017 BX',
archive: 'simple',
source: 'website',
createdDaysAgo: 201,
},
{
tag: 'archived-rich',
fullName: 'Rita Vermeulen',
email: 'rita.vermeulen@telenet.be',
phone: '+32 3 226 8420',
countryIso: 'BE',
city: 'Antwerp',
street: 'Meir 102',
postalCode: '2000',
archive: 'rich',
source: 'broker',
createdDaysAgo: 280,
},
];
export async function seedSyntheticPortData(
portId: string,
portSlug: string,
): Promise<SyntheticSeedSummary | null> {
const existing = await db
.select({ id: clients.id })
.from(clients)
.where(eq(clients.portId, portId))
.limit(1);
if (existing.length > 0) {
console.log(` [${portSlug}] already seeded (clients exist), skipping.`);
return null;
}
return withTransaction(async (tx) => {
// ── 1. Berths ───────────────────────────────────────────────────────────
// Same NocoDB snapshot as the realistic seed so the public map keeps
// working. We override status for the moorings we link to so the
// dossier UI shows the expected stake levels (under_offer / sold).
const berthRows = await tx
.insert(berths)
.values(
BERTH_SNAPSHOT.map((b) => ({
portId,
mooringNumber: b.mooringNumber,
area: b.area,
status: b.status,
lengthFt: b.lengthFt != null ? String(b.lengthFt) : null,
widthFt: b.widthFt != null ? String(b.widthFt) : null,
draftFt: b.draftFt != null ? String(b.draftFt) : null,
lengthM: b.lengthM != null ? String(b.lengthM) : null,
widthM: b.widthM != null ? String(b.widthM) : null,
draftM: b.draftM != null ? String(b.draftM) : null,
widthIsMinimum: b.widthIsMinimum,
nominalBoatSize: b.nominalBoatSize != null ? String(b.nominalBoatSize) : null,
nominalBoatSizeM: b.nominalBoatSizeM != null ? String(b.nominalBoatSizeM) : null,
waterDepth: b.waterDepth != null ? String(b.waterDepth) : null,
waterDepthM: b.waterDepthM != null ? String(b.waterDepthM) : null,
waterDepthIsMinimum: b.waterDepthIsMinimum,
sidePontoon: b.sidePontoon,
powerCapacity: b.powerCapacity != null ? String(b.powerCapacity) : null,
voltage: b.voltage != null ? String(b.voltage) : null,
mooringType: b.mooringType,
cleatType: b.cleatType,
cleatCapacity: b.cleatCapacity,
bollardType: b.bollardType,
bollardCapacity: b.bollardCapacity,
access: b.access,
price: b.price != null ? String(b.price) : null,
priceCurrency: 'USD',
bowFacing: b.bowFacing,
berthApproved: b.berthApproved,
statusOverrideMode: b.statusOverrideMode,
tenureType: 'permanent' as const,
})),
)
.returning({ id: berths.id, status: berths.status, mooringNumber: berths.mooringNumber });
// ── 2. Companies (one active, one with multiple memberships) ────────────
const companyRows = await tx
.insert(companies)
.values([
{
portId,
name: 'Test Charter Co.',
legalName: 'Test Charter Company Ltd.',
taxId: `TC-${portSlug}-001`,
registrationNumber: 'TC-2024-0001',
incorporationCountryIso: 'GB',
incorporationDate: new Date('2024-01-01'),
status: 'active',
billingEmail: 'billing@testcharter.test.local',
notes: 'Synthetic test company - has multiple member clients.',
},
])
.returning({ id: companies.id, name: companies.name });
const charterCoId = companyRows[0]!.id;
await tx.insert(companyAddresses).values([
{
companyId: charterCoId,
portId,
label: 'Head Office',
streetAddress: '1 Test Street',
city: 'London',
subdivisionIso: null,
postalCode: 'W1A 1AA',
countryIso: 'GB',
isPrimary: true,
},
]);
// ── 3. Clients ──────────────────────────────────────────────────────────
const clientRows = await tx
.insert(clients)
.values(
PIPELINE_CLIENTS.map((spec) => {
const created =
spec.createdDaysAgo !== undefined ? daysAgo(spec.createdDaysAgo) : new Date();
return {
portId,
fullName: spec.fullName,
nationalityIso: spec.countryIso,
preferredContactMethod: 'email' as const,
preferredLanguage: 'en',
source: spec.source ?? ('manual' as const),
// Override the schema default so the list page shows a
// realistic range of "Created" timestamps rather than 12
// rows all stamped with today's date. updated_at gets the
// same value so sorted-by-recency lists put the freshest
// records first.
createdAt: created,
updatedAt: created,
};
}),
)
.returning({ id: clients.id, fullName: clients.fullName });
const idByTag = new Map<string, string>();
PIPELINE_CLIENTS.forEach((spec, i) => idByTag.set(spec.tag, clientRows[i]!.id));
// Contacts
const contactValues: Array<typeof clientContacts.$inferInsert> = [];
PIPELINE_CLIENTS.forEach((spec, i) => {
const cid = clientRows[i]!.id;
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,
});
});
await tx.insert(clientContacts).values(contactValues);
// Addresses
await tx.insert(clientAddresses).values(
PIPELINE_CLIENTS.map((spec, i) => ({
clientId: clientRows[i]!.id,
portId,
label: 'Primary',
streetAddress: spec.street,
city: spec.city,
subdivisionIso: null,
postalCode: spec.postalCode,
countryIso: spec.countryIso,
isPrimary: true,
})),
);
// ── 4. Yachts (the completed-won client gets one) ───────────────────────
const completedWonId = idByTag.get('completed-won')!;
const charterYachtRow = await tx
.insert(yachts)
.values([
{
portId,
name: 'Test Wanderer',
hullNumber: 'TW-001',
flag: 'PA',
yearBuilt: 2018,
builder: 'Synthetic Yard',
model: 'Cruiser 50',
lengthFt: '50',
widthFt: '15',
draftFt: '6',
currentOwnerType: 'client' as const,
currentOwnerId: completedWonId,
status: 'active' as const,
notes: 'Owned by the completed-won test client.',
},
{
portId,
name: 'Charter Co. Flagship',
hullNumber: 'CC-FLAG-001',
flag: 'GB',
yearBuilt: 2022,
builder: 'Synthetic Yard',
model: 'Sailing Yacht 55',
lengthFt: '55',
widthFt: '17',
draftFt: '7',
currentOwnerType: 'company' as const,
currentOwnerId: charterCoId,
status: 'active' as const,
notes: 'Owned by Test Charter Co.',
},
])
.returning({ id: yachts.id, name: yachts.name });
await tx.insert(yachtOwnershipHistory).values([
{
yachtId: charterYachtRow[0]!.id,
ownerType: 'client',
ownerId: completedWonId,
startDate: daysAgo(180),
endDate: null,
transferReason: null,
transferNotes: null,
createdBy: SUPER_ADMIN_USER_ID,
},
{
yachtId: charterYachtRow[1]!.id,
ownerType: 'company',
ownerId: charterCoId,
startDate: daysAgo(365),
endDate: null,
transferReason: null,
transferNotes: null,
createdBy: SUPER_ADMIN_USER_ID,
},
]);
// ── 5. Memberships (link a couple of clients to Test Charter Co.) ──────
const dirClientId = idByTag.get('contract-sent')!;
const officerClientId = idByTag.get('eoi-signed')!;
await tx.insert(companyMemberships).values([
{
companyId: charterCoId,
clientId: dirClientId,
role: 'director',
roleDetail: 'Test director',
startDate: daysAgo(120),
endDate: null,
isPrimary: true,
},
{
companyId: charterCoId,
clientId: officerClientId,
role: 'officer',
roleDetail: 'Test officer',
startDate: daysAgo(90),
endDate: null,
isPrimary: false,
},
]);
// ── 6. Berth status overrides for linked moorings ───────────────────────
// Match the dossier classification to the berth's pipeline stage.
// Sold = contract+signed+won. Under offer = active berth-bearing stages
// (eoi / reservation / deposit_paid / contract-not-yet-won).
const stageToBerthStatus = (
spec: SyntheticClientSpec,
): 'available' | 'under_offer' | 'sold' | null => {
const stage = spec.stage;
if (!stage) return null;
if (stage === 'contract' && spec.contractDocStatus === 'signed' && spec.outcome === 'won') {
return 'sold';
}
if (
stage === 'eoi' ||
stage === 'reservation' ||
stage === 'deposit_paid' ||
stage === 'contract'
) {
return 'under_offer';
}
return null;
};
for (const spec of PIPELINE_CLIENTS) {
if (spec.berthIdx === undefined) continue;
const newStatus = stageToBerthStatus(spec);
if (!newStatus) continue;
const berthId = berthRows[spec.berthIdx]!.id;
await tx.update(berths).set({ status: newStatus }).where(eq(berths.id, berthId));
}
// ── 7. Interests + interest_berths ──────────────────────────────────────
let interestCount = 0;
for (const spec of PIPELINE_CLIENTS) {
if (!spec.stage) continue;
const clientId = idByTag.get(spec.tag)!;
// Derive deal age from the (stage, doc-sub-status) pair so a
// contract+signed+won record looks older than a brand-new enquiry.
const stageDaysAgoMap: Record<PipelineStage, number> = {
enquiry: 5,
qualified: 10,
nurturing: 30,
eoi: spec.eoiDocStatus === 'signed' ? 35 : 20,
reservation: 50,
deposit_paid: 60,
contract: spec.outcome === 'won' ? 200 : spec.contractDocStatus === 'signed' ? 110 : 80,
};
const ageDays = stageDaysAgoMap[spec.stage];
const yachtId = spec.tag === 'completed-won' ? charterYachtRow[0]!.id : null;
// Stage-progression flags so the date_* timestamps cascade correctly.
// Anything past "eoi+sent" implies the EOI was at least sent.
const eoiReached =
spec.stage === 'eoi' ||
spec.stage === 'reservation' ||
spec.stage === 'deposit_paid' ||
spec.stage === 'contract';
const eoiSigned =
(spec.stage === 'eoi' && spec.eoiDocStatus === 'signed') ||
spec.stage === 'reservation' ||
spec.stage === 'deposit_paid' ||
spec.stage === 'contract';
const [intRow] = await tx
.insert(interests)
.values({
portId,
clientId,
yachtId,
pipelineStage: spec.stage,
eoiDocStatus: spec.eoiDocStatus ?? (eoiSigned ? 'signed' : null),
reservationDocStatus: spec.reservationDocStatus ?? null,
contractDocStatus: spec.contractDocStatus ?? null,
leadCategory:
spec.stage === 'enquiry'
? 'general_interest'
: spec.stage === 'qualified' || spec.stage === 'nurturing'
? 'specific_qualified'
: 'hot_lead',
source: 'manual' as const,
dateFirstContact: daysAgo(ageDays),
dateLastContact: daysAgo(Math.max(0, ageDays - 2)),
dateEoiSent: eoiReached ? daysAgo(Math.max(0, ageDays - 5)) : null,
dateEoiSigned: eoiSigned ? daysAgo(Math.max(0, ageDays - 10)) : null,
eoiStatus:
spec.stage === 'eoi' && spec.eoiDocStatus === 'sent'
? 'waiting_for_signatures'
: eoiSigned
? 'signed'
: null,
outcome: spec.outcome ?? null,
outcomeAt: spec.outcome ? daysAgo(7) : null,
outcomeReason:
spec.outcome === 'lost_unqualified' ? 'Synthetic test: not qualified.' : null,
})
.returning({ id: interests.id });
interestCount += 1;
if (spec.berthIdx !== undefined) {
const berthId = berthRows[spec.berthIdx]!.id;
await tx.insert(interestBerths).values({
interestId: intRow!.id,
berthId,
isPrimary: true,
isSpecificInterest: true,
isInEoiBundle: spec.stage !== 'enquiry' && spec.stage !== 'qualified',
addedBy: SUPER_ADMIN_USER_ID,
});
}
}
// ── 8. Multi-interest client ────────────────────────────────────────────
// Adds a second interest to "carla.comms" so the dossier shows
// multiple deals on the same client.
const carlaId = idByTag.get('comms')!;
const [secondInt] = await tx
.insert(interests)
.values({
portId,
clientId: carlaId,
yachtId: null,
pipelineStage: 'enquiry',
leadCategory: 'general_interest',
source: 'website' as const,
dateFirstContact: daysAgo(2),
dateLastContact: daysAgo(1),
})
.returning({ id: interests.id });
await tx.insert(interestBerths).values({
interestId: secondInt!.id,
berthId: berthRows[2]!.id,
isPrimary: true,
isSpecificInterest: true,
isInEoiBundle: false,
addedBy: SUPER_ADMIN_USER_ID,
});
// ── 9. Reservations ─────────────────────────────────────────────────────
// One active reservation on the under_offer berth held by Carla,
// one cancelled on an available berth.
// berthReservations requires a yacht - wire both to the charter co.
// flagship since Carla / Olivia don't own yachts yet.
const sharedYachtId = charterYachtRow[1]!.id;
await tx.insert(berthReservations).values([
{
portId,
berthId: berthRows[5]!.id,
clientId: carlaId,
yachtId: sharedYachtId,
startDate: daysAgo(10),
endDate: null,
status: 'active',
notes: 'Synthetic active reservation.',
createdBy: SUPER_ADMIN_USER_ID,
},
{
portId,
berthId: berthRows[3]!.id,
clientId: idByTag.get('open')!,
yachtId: sharedYachtId,
startDate: daysAgo(30),
endDate: daysAgo(20),
status: 'cancelled',
notes: 'Synthetic cancelled reservation.',
createdBy: SUPER_ADMIN_USER_ID,
},
]);
// ── 10. Apply archive metadata for Anna + Rita ──────────────────────────
const annaId = idByTag.get('archived-simple')!;
await tx
.update(clients)
.set({
archivedAt: daysAgo(30),
// archived_by FK references the better-auth user table; the
// synthetic super-admin is just a profile placeholder so we
// leave this null. Field is set to the actual operator id by
// the smart-archive service in production code paths.
archivedBy: null,
archiveReason: '',
archiveMetadata: null,
})
.where(eq(clients.id, annaId));
// Rich-archive: fabricate a metadata payload that the smart-restore
// wizard will surface as auto-reversible (berth still available) +
// opt-in-to-undo (yacht transferred).
const ritaId = idByTag.get('archived-rich')!;
const richMetadata: ArchiveMetadata = {
decisions: [
{
kind: 'berth_released',
refId: berthRows[2]!.id,
detail: { mooringNumber: berthRows[2]!.mooringNumber },
},
{
kind: 'yacht_transferred',
refId: charterYachtRow[1]!.id,
detail: { newOwnerType: 'company', newOwnerId: charterCoId },
},
],
decidedAt: daysAgo(20).toISOString(),
decidedBy: SUPER_ADMIN_USER_ID,
reason: 'Synthetic rich-archive for restore wizard testing.',
};
await tx
.update(clients)
.set({
archivedAt: daysAgo(20),
archivedBy: null,
archiveReason: richMetadata.reason,
archiveMetadata: richMetadata,
})
.where(eq(clients.id, ritaId));
// ── 11. Residential pipeline (one per stage cluster) ────────────────────
const residentialRows = await tx
.insert(residentialClients)
.values([
{
portId,
fullName: 'Robert Resident',
email: 'robert.resident@test.local',
phone: '+1 555 020 0001',
source: 'website' as const,
notes: 'Synthetic residential lead.',
},
{
portId,
fullName: 'Rina Resident',
email: 'rina.resident@test.local',
phone: '+1 555 020 0002',
source: 'referral' as const,
notes: 'Synthetic residential lead - qualified.',
},
])
.returning({ id: residentialClients.id });
await tx.insert(residentialInterests).values([
{
portId,
residentialClientId: residentialRows[0]!.id,
pipelineStage: 'new',
notes: 'Synthetic residential interest at "new" stage.',
dateFirstContact: daysAgo(2),
},
{
portId,
residentialClientId: residentialRows[1]!.id,
pipelineStage: 'contacted',
notes: 'Synthetic residential interest at "contacted" stage.',
dateFirstContact: daysAgo(7),
dateLastContact: daysAgo(2),
},
]);
return {
berths: berthRows.length,
clients: clientRows.length,
interests: interestCount + 1, // +1 for Carla's second interest
companies: 1,
yachts: charterYachtRow.length,
residentialClients: residentialRows.length,
} satisfies SyntheticSeedSummary;
});
}