diff --git a/tests/helpers/factories.ts b/tests/helpers/factories.ts index 656fec6..5afc23c 100644 --- a/tests/helpers/factories.ts +++ b/tests/helpers/factories.ts @@ -10,12 +10,19 @@ * — 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, type NewCompany, type Company } from '@/lib/db/schema/companies'; +import { + companies, + companyMemberships, + type NewCompany, + type Company, +} from '@/lib/db/schema/companies'; +import { berthReservations, type BerthReservation } from '@/lib/db/schema/reservations'; // ─── Port ──────────────────────────────────────────────────────────────────── @@ -119,6 +126,125 @@ export async function makeCompany(args: { 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 { diff --git a/tests/integration/factories-smoke.test.ts b/tests/integration/factories-smoke.test.ts new file mode 100644 index 0000000..c0a7e12 --- /dev/null +++ b/tests/integration/factories-smoke.test.ts @@ -0,0 +1,70 @@ +import { describe, it, expect } from 'vitest'; +import { + makePort, + makeClient, + makeCompany, + makeBerth, + makeYacht, + makeMembership, + makeReservation, + makeOwnershipTransfer, +} from '../helpers/factories'; +import { db } from '@/lib/db'; +import { yachtOwnershipHistory } from '@/lib/db/schema'; +import { eq, isNull, and } from 'drizzle-orm'; + +describe('factory helpers smoke', () => { + it('makeMembership inserts a row', async () => { + const port = await makePort(); + const client = await makeClient({ portId: port.id }); + const company = await makeCompany({ portId: port.id }); + const m = await makeMembership({ companyId: company.id, clientId: client.id }); + expect(m.role).toBe('director'); + expect(m.endDate).toBeNull(); + }); + + it('makeReservation inserts a row in any status', async () => { + const port = await makePort(); + const berth = await makeBerth({ portId: port.id }); + const client = await makeClient({ portId: port.id }); + const yacht = await makeYacht({ + portId: port.id, + ownerType: 'client', + ownerId: client.id, + }); + const r = await makeReservation({ + berthId: berth.id, + portId: port.id, + clientId: client.id, + yachtId: yacht.id, + status: 'pending', + }); + expect(r.status).toBe('pending'); + }); + + it('makeOwnershipTransfer closes prior history and opens new', async () => { + const port = await makePort(); + const clientA = await makeClient({ portId: port.id }); + const clientB = await makeClient({ portId: port.id }); + const yacht = await makeYacht({ + portId: port.id, + ownerType: 'client', + ownerId: clientA.id, + }); + + const { yacht: updated } = await makeOwnershipTransfer({ + yachtId: yacht.id, + newOwner: { type: 'client', id: clientB.id }, + }); + expect(updated.currentOwnerId).toBe(clientB.id); + + const open = await db + .select() + .from(yachtOwnershipHistory) + .where( + and(eq(yachtOwnershipHistory.yachtId, yacht.id), isNull(yachtOwnershipHistory.endDate)), + ); + expect(open).toHaveLength(1); + expect(open[0]!.ownerId).toBe(clientB.id); + }); +});