/** * Test factory helpers. * * Two flavours: * 1. Async DB-inserting factories (makePort, makeClient, makeBerth, makeYacht, * makeCompany, ...) — insert a row via the app's `db` handle and return the * inserted record. Use these from integration tests that need real FK / * unique-index enforcement. They require DATABASE_URL to be reachable. * 2. Plain-data helpers (makeAuditMeta, makeCreateClientInput, makeCreate*) * — return in-memory objects suitable for unit tests with mocked `db`. */ import { and, eq, sql } from 'drizzle-orm'; import { db } from '@/lib/db'; import { ports, type NewPort, type Port } from '@/lib/db/schema/ports'; import { clients, type NewClient, type Client } from '@/lib/db/schema/clients'; import { berths, type NewBerth, type Berth } from '@/lib/db/schema/berths'; import { yachts, yachtOwnershipHistory, type NewYacht, type Yacht } from '@/lib/db/schema/yachts'; import { companies, companyMemberships, type NewCompany, type Company, } from '@/lib/db/schema/companies'; import { berthReservations, type BerthReservation } from '@/lib/db/schema/reservations'; // ─── Port ──────────────────────────────────────────────────────────────────── export async function makePort(args?: { overrides?: Partial }): Promise { const suffix = Math.random().toString(36).slice(2, 10); const [port] = await db .insert(ports) .values({ name: args?.overrides?.name ?? `Test Port ${suffix}`, slug: args?.overrides?.slug ?? `test-port-${suffix}`, ...args?.overrides, }) .returning(); return port!; } // ─── Client ────────────────────────────────────────────────────────────────── export async function makeClient(args: { portId: string; overrides?: Partial; }): Promise { const [client] = await db .insert(clients) .values({ portId: args.portId, fullName: args.overrides?.fullName ?? `Test Client ${Math.random().toString(36).slice(2, 8)}`, ...args.overrides, }) .returning(); return client!; } // ─── Berth ──────────────────────────────────────────────────────────────────── export async function makeBerth(args: { portId: string; overrides?: Partial; }): Promise { const [berth] = await db .insert(berths) .values({ portId: args.portId, mooringNumber: args.overrides?.mooringNumber ?? `B-${Math.random().toString(36).slice(2, 8)}`, ...args.overrides, }) .returning(); return berth!; } // ─── Yacht ─────────────────────────────────────────────────────────────────── export async function makeYacht(args: { portId: string; ownerType: 'client' | 'company'; ownerId: string; name?: string; status?: 'active' | 'retired' | 'sold_away'; hullNumber?: string; registration?: string; overrides?: Partial; }): Promise { const [yacht] = await db .insert(yachts) .values({ portId: args.portId, name: args.name ?? args.overrides?.name ?? `Yacht ${Math.random().toString(36).slice(2, 8)}`, currentOwnerType: args.ownerType, currentOwnerId: args.ownerId, ...(args.status !== undefined ? { status: args.status } : {}), ...(args.hullNumber !== undefined ? { hullNumber: args.hullNumber } : {}), ...(args.registration !== undefined ? { registration: args.registration } : {}), ...args.overrides, }) .returning(); await db.insert(yachtOwnershipHistory).values({ yachtId: yacht!.id, ownerType: args.ownerType, ownerId: args.ownerId, startDate: new Date(), endDate: null, createdBy: 'test', }); return yacht!; } // ─── Company ───────────────────────────────────────────────────────────────── export async function makeCompany(args: { portId: string; overrides?: Partial; }): Promise { const [company] = await db .insert(companies) .values({ portId: args.portId, name: args.overrides?.name ?? `Company ${Math.random().toString(36).slice(2, 8)}`, ...args.overrides, }) .returning(); return company!; } // ─── Company Membership ────────────────────────────────────────────────────── export async function makeMembership(args: { companyId: string; clientId: string; role?: string; roleDetail?: string; startDate?: Date; endDate?: Date | null; isPrimary?: boolean; notes?: string; }) { const [row] = await db .insert(companyMemberships) .values({ companyId: args.companyId, clientId: args.clientId, role: args.role ?? 'director', roleDetail: args.roleDetail ?? null, startDate: args.startDate ?? new Date(), endDate: args.endDate ?? null, isPrimary: args.isPrimary ?? false, notes: args.notes ?? null, }) .returning(); if (!row) throw new Error('Failed to create membership'); return row; } // ─── Berth Reservation ─────────────────────────────────────────────────────── export async function makeReservation(args: { berthId: string; portId: string; clientId: string; yachtId: string; status: 'pending' | 'active' | 'ended' | 'cancelled'; startDate?: Date; endDate?: Date | null; tenureType?: 'permanent' | 'fixed_term' | 'seasonal'; interestId?: string; createdBy?: string; notes?: string; }): Promise { const [row] = await db .insert(berthReservations) .values({ berthId: args.berthId, portId: args.portId, clientId: args.clientId, yachtId: args.yachtId, interestId: args.interestId ?? null, status: args.status, startDate: args.startDate ?? new Date(), endDate: args.endDate ?? null, tenureType: args.tenureType ?? 'permanent', contractFileId: null, notes: args.notes ?? null, createdBy: args.createdBy ?? 'seed-user', }) .returning(); if (!row) throw new Error('Failed to create reservation'); return row; } // ─── Yacht Ownership Transfer ──────────────────────────────────────────────── export async function makeOwnershipTransfer(args: { yachtId: string; newOwner: { type: 'client' | 'company'; id: string }; effectiveDate?: Date; transferReason?: string; transferNotes?: string; createdBy?: string; }) { const effective = args.effectiveDate ?? new Date(); const createdBy = args.createdBy ?? 'seed-user'; return await db.transaction(async (tx) => { // Close current open row await tx .update(yachtOwnershipHistory) .set({ endDate: effective }) .where( and( eq(yachtOwnershipHistory.yachtId, args.yachtId), sql`${yachtOwnershipHistory.endDate} IS NULL`, ), ); // Insert new open row const [newHistory] = await tx .insert(yachtOwnershipHistory) .values({ yachtId: args.yachtId, ownerType: args.newOwner.type, ownerId: args.newOwner.id, startDate: effective, endDate: null, transferReason: args.transferReason ?? null, transferNotes: args.transferNotes ?? null, createdBy, }) .returning(); // Update yacht's denormalized current owner const [updated] = await tx .update(yachts) .set({ currentOwnerType: args.newOwner.type, currentOwnerId: args.newOwner.id, updatedAt: new Date(), }) .where(eq(yachts.id, args.yachtId)) .returning(); return { history: newHistory!, yacht: updated! }; }); } // ─── Webhook ────────────────────────────────────────────────────────────────── export interface WebhookData { id: string; portId: string; name: string; url: string; secret: string | null; events: string[]; isActive: boolean; createdBy: string; createdAt: Date; updatedAt: Date; } export function makeWebhook(overrides?: Partial): WebhookData { return { id: crypto.randomUUID(), portId: crypto.randomUUID(), name: 'Test Webhook', url: 'https://example.com/webhook', secret: null, events: ['client.created'], isActive: true, createdBy: crypto.randomUUID(), createdAt: new Date(), updatedAt: new Date(), ...overrides, }; } // ─── Audit Log ──────────────────────────────────────────────────────────────── export interface AuditMeta { userId: string; portId: string; ipAddress: string; userAgent: string; } export function makeAuditMeta(overrides?: Partial): AuditMeta { return { userId: crypto.randomUUID(), portId: crypto.randomUUID(), ipAddress: '127.0.0.1', userAgent: 'vitest/1.0', ...overrides, }; } // ─── Auth Context ───────────────────────────────────────────────────────────── import type { RolePermissions } from '@/lib/db/schema/users'; /** Full permissions — every action allowed. */ export function makeFullPermissions(): RolePermissions { return { clients: { view: true, create: true, edit: true, delete: true, merge: true, export: true }, interests: { view: true, create: true, edit: true, delete: true, change_stage: true, generate_eoi: true, export: true, }, berths: { view: true, edit: true, import: true, manage_waiting_list: true }, documents: { view: true, create: true, send_for_signing: true, upload_signed: true, delete: true, }, expenses: { view: true, create: true, edit: true, delete: true, export: true, scan_receipt: true, }, invoices: { view: true, create: true, edit: true, delete: true, send: true, record_payment: true, export: true, }, files: { view: true, upload: true, delete: true, manage_folders: true }, email: { view: true, send: true, configure_account: true }, reminders: { view_own: true, view_all: true, create: true, edit_own: true, edit_all: true, assign_others: true, }, calendar: { connect: true, view_events: true }, reports: { view_dashboard: true, view_analytics: true, export: true }, document_templates: { view: true, generate: true, manage: true }, yachts: { view: true, create: true, edit: true, delete: true, transfer: true }, companies: { view: true, create: true, edit: true, delete: true }, memberships: { view: true, manage: true }, reservations: { view: true, create: true, activate: true, cancel: true }, admin: { manage_users: true, view_audit_log: true, manage_settings: true, manage_webhooks: true, manage_reports: true, manage_custom_fields: true, manage_forms: true, manage_tags: true, system_backup: true, }, residential_clients: { view: true, create: true, edit: true, delete: true }, residential_interests: { view: true, create: true, edit: true, delete: true, change_stage: true, }, }; } /** Read-only viewer permissions — no create/update/delete. */ export function makeViewerPermissions(): RolePermissions { return { clients: { view: true, create: false, edit: false, delete: false, merge: false, export: false }, interests: { view: true, create: false, edit: false, delete: false, change_stage: false, generate_eoi: false, export: false, }, berths: { view: true, edit: false, import: false, manage_waiting_list: false }, documents: { view: true, create: false, send_for_signing: false, upload_signed: false, delete: false, }, expenses: { view: true, create: false, edit: false, delete: false, export: false, scan_receipt: false, }, invoices: { view: true, create: false, edit: false, delete: false, send: false, record_payment: false, export: false, }, files: { view: true, upload: false, delete: false, manage_folders: false }, email: { view: true, send: false, configure_account: false }, reminders: { view_own: true, view_all: false, create: false, edit_own: false, edit_all: false, assign_others: false, }, calendar: { connect: false, view_events: true }, reports: { view_dashboard: true, view_analytics: false, export: false }, document_templates: { view: true, generate: false, manage: false }, yachts: { view: true, create: false, edit: false, delete: false, transfer: false }, companies: { view: true, create: false, edit: false, delete: false }, memberships: { view: true, manage: false }, reservations: { view: true, create: false, activate: false, cancel: false }, admin: { manage_users: false, view_audit_log: false, manage_settings: false, manage_webhooks: false, manage_reports: false, manage_custom_fields: false, manage_forms: false, manage_tags: false, system_backup: false, }, residential_clients: { view: false, create: false, edit: false, delete: false }, residential_interests: { view: false, create: false, edit: false, delete: false, change_stage: false, }, }; } /** Sales agent permissions — own clients/interests, no admin. */ export function makeSalesAgentPermissions(): RolePermissions { return { clients: { view: true, create: true, edit: true, delete: false, merge: false, export: false }, interests: { view: true, create: true, edit: true, delete: false, change_stage: true, generate_eoi: true, export: false, }, berths: { view: true, edit: false, import: false, manage_waiting_list: false }, documents: { view: true, create: true, send_for_signing: true, upload_signed: true, delete: false, }, expenses: { view: true, create: true, edit: true, delete: false, export: false, scan_receipt: true, }, invoices: { view: true, create: false, edit: false, delete: false, send: false, record_payment: false, export: false, }, files: { view: true, upload: true, delete: false, manage_folders: false }, email: { view: true, send: true, configure_account: false }, reminders: { view_own: true, view_all: false, create: true, edit_own: true, edit_all: false, assign_others: false, }, calendar: { connect: true, view_events: true }, reports: { view_dashboard: true, view_analytics: false, export: false }, document_templates: { view: true, generate: true, manage: false }, yachts: { view: true, create: true, edit: true, delete: false, transfer: false }, companies: { view: true, create: true, edit: false, delete: false }, memberships: { view: true, manage: false }, reservations: { view: true, create: true, activate: true, cancel: false }, admin: { manage_users: false, view_audit_log: false, manage_settings: false, manage_webhooks: false, manage_reports: false, manage_custom_fields: false, manage_forms: false, manage_tags: false, system_backup: false, }, residential_clients: { view: false, create: false, edit: false, delete: false }, residential_interests: { view: false, create: false, edit: false, delete: false, change_stage: false, }, }; } /** Sales manager — can do most things, limited admin. */ export function makeSalesManagerPermissions(): RolePermissions { return { clients: { view: true, create: true, edit: true, delete: true, merge: true, export: true }, interests: { view: true, create: true, edit: true, delete: true, change_stage: true, generate_eoi: true, export: true, }, berths: { view: true, edit: true, import: false, manage_waiting_list: true }, documents: { view: true, create: true, send_for_signing: true, upload_signed: true, delete: true, }, expenses: { view: true, create: true, edit: true, delete: true, export: true, scan_receipt: true, }, invoices: { view: true, create: true, edit: true, delete: false, send: true, record_payment: true, export: true, }, files: { view: true, upload: true, delete: true, manage_folders: true }, email: { view: true, send: true, configure_account: false }, reminders: { view_own: true, view_all: true, create: true, edit_own: true, edit_all: true, assign_others: true, }, calendar: { connect: true, view_events: true }, reports: { view_dashboard: true, view_analytics: true, export: true }, document_templates: { view: true, generate: true, manage: false }, yachts: { view: true, create: true, edit: true, delete: false, transfer: true }, companies: { view: true, create: true, edit: true, delete: false }, memberships: { view: true, manage: true }, reservations: { view: true, create: true, activate: true, cancel: true }, admin: { manage_users: false, view_audit_log: true, manage_settings: false, manage_webhooks: false, manage_reports: true, manage_custom_fields: false, manage_forms: false, manage_tags: true, system_backup: false, }, residential_clients: { view: true, create: true, edit: true, delete: true }, residential_interests: { view: true, create: true, edit: true, delete: true, change_stage: true, }, }; } /** Director — everything except system backup. */ export function makeDirectorPermissions(): RolePermissions { return { ...makeFullPermissions(), admin: { ...makeFullPermissions().admin, system_backup: false, }, }; } // ─── Minimal valid CreateClientInput ───────────────────────────────────────── /** Returns a minimal valid CreateClientInput object for use in service calls. */ export function makeCreateClientInput(overrides?: { fullName?: string; portId?: string }) { return { fullName: overrides?.fullName ?? 'Test Client', contacts: [{ channel: 'email' as const, value: 'test@example.com', isPrimary: true }], tagIds: [] as string[], }; } /** Returns a minimal valid CreateInterestInput object. */ export function makeCreateInterestInput(overrides?: { clientId?: string; pipelineStage?: | 'open' | 'details_sent' | 'in_communication' | 'eoi_sent' | 'eoi_signed' | 'deposit_10pct' | 'contract_sent' | 'contract_signed' | 'completed'; }) { return { clientId: overrides?.clientId ?? crypto.randomUUID(), pipelineStage: overrides?.pipelineStage ?? ('open' as const), reminderEnabled: false, tagIds: [] as string[], }; }