import type { Prisma, PrismaClient } from "@capakraken/db"; import { CreateClientSchema, UpdateClientSchema, type ClientTree } from "@capakraken/shared"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; type ClientTreeNode = { id: string; name: string; code: string | null; parentId: string | null; isActive: boolean; sortOrder: number; tags: string[]; createdAt: Date; updatedAt: Date; }; type ClientIdentifierDb = Pick; type ClientListInput = { parentId?: string | null | undefined; isActive?: boolean | undefined; search?: string | undefined; }; type ClientDeleteRecord = { _count: { projects: number; children: number; }; }; type CreateClientInput = z.infer; type UpdateClientInput = z.infer; export function buildClientListWhere(input: ClientListInput): Prisma.ClientWhereInput { return { ...(input.parentId !== undefined ? { parentId: input.parentId } : {}), ...(input.isActive !== undefined ? { isActive: input.isActive } : {}), ...(input.search ? { OR: [ { name: { contains: input.search, mode: "insensitive" } }, { code: { contains: input.search, mode: "insensitive" } }, ], } : {}), }; } export function buildClientTree( flatItems: ClientTreeNode[], parentId: string | null = null, ): ClientTree[] { return flatItems .filter((item) => item.parentId === parentId) .sort((a, b) => a.sortOrder - b.sortOrder || a.name.localeCompare(b.name)) .map((item) => ({ ...item, children: buildClientTree(flatItems, item.id), })); } export async function findClientByIdentifier( db: ClientIdentifierDb, identifier: string, extraArgs: Record, ): Promise { const normalizedIdentifier = identifier.trim(); let client = await db.client.findUnique({ where: { id: normalizedIdentifier }, ...extraArgs, }) as TClient | null; if (!client) { client = await db.client.findUnique({ where: { code: normalizedIdentifier }, ...extraArgs, }) as TClient | null; } if (!client) { client = await db.client.findFirst({ where: { name: { equals: normalizedIdentifier, mode: "insensitive" } }, ...extraArgs, }) as TClient | null; } if (!client) { client = await db.client.findFirst({ where: { OR: [ { name: { contains: normalizedIdentifier, mode: "insensitive" } }, { code: { contains: normalizedIdentifier, mode: "insensitive" } }, ], }, ...extraArgs, }) as TClient | null; } if (!client) { throw new TRPCError({ code: "NOT_FOUND", message: `Client not found: ${normalizedIdentifier}`, }); } return client; } export function buildClientCreateData( input: CreateClientInput, ): Prisma.ClientUncheckedCreateInput { return { name: input.name, ...(input.code ? { code: input.code } : {}), ...(input.parentId ? { parentId: input.parentId } : {}), sortOrder: input.sortOrder, ...(input.tags ? { tags: input.tags } : {}), }; } export function buildClientUpdateData( input: UpdateClientInput, ): Prisma.ClientUncheckedUpdateInput { return { ...(input.name !== undefined ? { name: input.name } : {}), ...(input.code !== undefined ? { code: input.code } : {}), ...(input.sortOrder !== undefined ? { sortOrder: input.sortOrder } : {}), ...(input.isActive !== undefined ? { isActive: input.isActive } : {}), ...(input.parentId !== undefined ? { parentId: input.parentId } : {}), ...(input.tags !== undefined ? { tags: input.tags } : {}), }; } export async function assertClientCodeAvailable( db: ClientIdentifierDb, code: string, ignoreId?: string, ): Promise { const existing = await db.client.findUnique({ where: { code } }); if (existing && existing.id !== ignoreId) { throw new TRPCError({ code: "CONFLICT", message: `Client code "${code}" already exists`, }); } } export function assertClientDeletable(client: ClientDeleteRecord): void { if (client._count.projects > 0) { throw new TRPCError({ code: "PRECONDITION_FAILED", message: `Cannot delete client with ${client._count.projects} project(s). Deactivate instead.`, }); } if (client._count.children > 0) { throw new TRPCError({ code: "PRECONDITION_FAILED", message: `Cannot delete client with ${client._count.children} child client(s). Remove children first.`, }); } }