From 4c171848fc69ba358222d15a70e4ba32a7547c38 Mon Sep 17 00:00:00 2001 From: Matt Ciaccio Date: Fri, 24 Apr 2026 14:31:14 +0200 Subject: [PATCH] refactor(clients): strip deprecated fields + extend getClientById with yachts/companies/reservations Co-Authored-By: Claude Opus 4.7 (1M context) --- src/lib/services/clients.service.ts | 123 ++++++++++++++++++++-------- 1 file changed, 90 insertions(+), 33 deletions(-) diff --git a/src/lib/services/clients.service.ts b/src/lib/services/clients.service.ts index ee6facd..b1db082 100644 --- a/src/lib/services/clients.service.ts +++ b/src/lib/services/clients.service.ts @@ -1,12 +1,10 @@ -import { and, eq, ilike, inArray, or } from 'drizzle-orm'; +import { and, eq, ilike, inArray, isNull, or } from 'drizzle-orm'; import { db } from '@/lib/db'; -import { - clients, - clientContacts, - clientRelationships, - clientTags, -} from '@/lib/db/schema/clients'; +import { clients, clientContacts, clientRelationships, clientTags } from '@/lib/db/schema/clients'; +import { companies, companyMemberships } from '@/lib/db/schema/companies'; +import { yachts } from '@/lib/db/schema/yachts'; +import { berthReservations } from '@/lib/db/schema/reservations'; import { tags } from '@/lib/db/schema/system'; import { createAuditLog } from '@/lib/audit'; import { NotFoundError } from '@/lib/errors'; @@ -32,7 +30,7 @@ interface AuditMeta { // ─── List ───────────────────────────────────────────────────────────────────── export async function listClients(portId: string, query: ListClientsInput) { - const { page, limit, sort, order, search, includeArchived, source, nationality, isProxy, tagIds } = query; + const { page, limit, sort, order, search, includeArchived, source, nationality, tagIds } = query; const filters = []; @@ -42,9 +40,6 @@ export async function listClients(portId: string, query: ListClientsInput) { if (nationality) { filters.push(ilike(clients.nationality, `%${nationality}%`)); } - if (isProxy !== undefined) { - filters.push(eq(clients.isProxy, isProxy)); - } if (tagIds && tagIds.length > 0) { const clientsWithTags = await db .selectDistinct({ clientId: clientTags.clientId }) @@ -105,20 +100,76 @@ export async function getClientById(id: string, portId: string) { .innerJoin(tags, eq(clientTags.tagId, tags.id)) .where(eq(clientTags.clientId, id)); + const yachtRows = await db.query.yachts.findMany({ + where: and( + eq(yachts.portId, portId), + eq(yachts.currentOwnerType, 'client'), + eq(yachts.currentOwnerId, id), + isNull(yachts.archivedAt), + ), + columns: { + id: true, + name: true, + hullNumber: true, + registration: true, + lengthFt: true, + widthFt: true, + status: true, + }, + }); + + const membershipRows = await db + .select({ + membershipId: companyMemberships.id, + role: companyMemberships.role, + isPrimary: companyMemberships.isPrimary, + startDate: companyMemberships.startDate, + company: { + id: companies.id, + name: companies.name, + legalName: companies.legalName, + status: companies.status, + }, + }) + .from(companyMemberships) + .innerJoin(companies, eq(companyMemberships.companyId, companies.id)) + .where( + and( + eq(companyMemberships.clientId, id), + eq(companies.portId, portId), + isNull(companyMemberships.endDate), + ), + ); + + const activeReservations = await db.query.berthReservations.findMany({ + where: and( + eq(berthReservations.clientId, id), + eq(berthReservations.portId, portId), + eq(berthReservations.status, 'active'), + ), + columns: { + id: true, + berthId: true, + yachtId: true, + startDate: true, + tenureType: true, + status: true, + }, + }); + return { ...client, contacts, tags: clientTagRows.map((r) => r.tag), + yachts: yachtRows, + companies: membershipRows, + activeReservations, }; } // ─── Create ─────────────────────────────────────────────────────────────────── -export async function createClient( - portId: string, - data: CreateClientInput, - meta: AuditMeta, -) { +export async function createClient(portId: string, data: CreateClientInput, meta: AuditMeta) { const result = await withTransaction(async (tx) => { const { contacts: contactsInput, tagIds, ...clientData } = data; @@ -128,15 +179,13 @@ export async function createClient( .returning(); if (contactsInput.length > 0) { - await tx.insert(clientContacts).values( - contactsInput.map((c) => ({ clientId: client!.id, ...c })), - ); + await tx + .insert(clientContacts) + .values(contactsInput.map((c) => ({ clientId: client!.id, ...c }))); } if (tagIds && tagIds.length > 0) { - await tx.insert(clientTags).values( - tagIds.map((tagId) => ({ clientId: client!.id, tagId })), - ); + await tx.insert(clientTags).values(tagIds.map((tagId) => ({ clientId: client!.id, tagId }))); } return client!; @@ -153,7 +202,11 @@ export async function createClient( userAgent: meta.userAgent, }); - emitToRoom(`port:${portId}`, 'client:created', { clientId: result.id, clientName: result.fullName ?? '', source: result.source ?? '' }); + emitToRoom(`port:${portId}`, 'client:created', { + clientId: result.id, + clientName: result.fullName ?? '', + source: result.source ?? '', + }); void import('@/lib/services/webhook-dispatch').then(({ dispatchWebhookEvent }) => dispatchWebhookEvent(portId, 'client:created', { clientId: result.id }), @@ -198,7 +251,10 @@ export async function updateClient( userAgent: meta.userAgent, }); - emitToRoom(`port:${portId}`, 'client:updated', { clientId: id, changedFields: Object.keys(diff) }); + emitToRoom(`port:${portId}`, 'client:updated', { + clientId: id, + changedFields: Object.keys(diff), + }); void import('@/lib/services/webhook-dispatch').then(({ dispatchWebhookEvent }) => dispatchWebhookEvent(portId, 'client:updated', { clientId: id }), @@ -311,7 +367,13 @@ export async function updateContact( contactId: string, clientId: string, portId: string, - data: Partial<{ channel: string; value: string; label: string; isPrimary: boolean; notes: string }>, + data: Partial<{ + channel: string; + value: string; + label: string; + isPrimary: boolean; + notes: string; + }>, _meta: AuditMeta, ) { const client = await db.query.clients.findFirst({ @@ -398,8 +460,7 @@ export async function listRelationships(clientId: string, portId: string) { if (!client || client.portId !== portId) throw new NotFoundError('Client'); return db.query.clientRelationships.findMany({ - where: (r, { or, eq }) => - or(eq(r.clientAId, clientId), eq(r.clientBId, clientId)), + where: (r, { or, eq }) => or(eq(r.clientAId, clientId), eq(r.clientBId, clientId)), }); } @@ -461,8 +522,7 @@ export async function deleteRelationship( export async function findDuplicates(portId: string, fullName: string) { return db.query.clients.findMany({ - where: (c, { and, eq }) => - and(eq(c.portId, portId), ilike(c.fullName, `%${fullName}%`)), + where: (c, { and, eq }) => and(eq(c.portId, portId), ilike(c.fullName, `%${fullName}%`)), limit: 5, }); } @@ -473,10 +533,7 @@ export async function listClientOptions(portId: string, search?: string) { const conditions = [eq(clients.portId, portId)]; if (search) { conditions.push( - or( - ilike(clients.fullName, `%${search}%`), - ilike(clients.companyName, `%${search}%`), - )!, + or(ilike(clients.fullName, `%${search}%`), ilike(clients.companyName, `%${search}%`))!, ); }