import type { FastifyInstance } from 'fastify'; import { z } from 'zod'; import { prisma } from '../../db.js'; import { getOrganizationId } from '../../lib/org.js'; const ClientUpsert = z.object({ kind: z.enum(['ul', 'ip', 'fl']), name: z.string().min(1).max(500), inn: z .string() .regex(/^\d{10}$|^\d{12}$/) .nullable(), kpp: z.string().regex(/^\d{9}$/).nullable(), address: z.string().max(1000).nullable(), email: z.string().email().nullable(), phone: z.string().max(50).nullable(), contactPerson: z.string().max(200).nullable(), }); const ListQuery = z.object({ q: z.string().optional(), limit: z.coerce.number().int().min(1).max(200).default(100), }); export async function clientsRoutes(app: FastifyInstance) { app.get('/api/clients', { preHandler: app.requireDocPermission('viewer') }, async (req, reply) => { const orgId = getOrganizationId(req); const parsed = ListQuery.safeParse(req.query); if (!parsed.success) { reply.code(400).send({ error: 'validation_error', issues: parsed.error.flatten() }); return; } const { q, limit } = parsed.data; const clients = await prisma.client.findMany({ where: { organizationId: orgId, ...(q ? { OR: [ { name: { contains: q, mode: 'insensitive' } }, { inn: { contains: q } }, { email: { contains: q, mode: 'insensitive' } }, ], } : {}), }, orderBy: { name: 'asc' }, take: limit, }); return { items: clients }; }); app.get('/api/clients/:id', { preHandler: app.requireDocPermission('viewer') }, async (req, reply) => { const orgId = getOrganizationId(req); const { id } = req.params as { id: string }; const client = await prisma.client.findFirst({ where: { id, organizationId: orgId } }); if (!client) { reply.code(404).send({ error: 'not_found' }); return; } return client; }); app.post('/api/clients', { preHandler: app.requireDocPermission('user') }, async (req, reply) => { const orgId = getOrganizationId(req); const parsed = ClientUpsert.safeParse(req.body); if (!parsed.success) { reply.code(400).send({ error: 'validation_error', issues: parsed.error.flatten() }); return; } const created = await prisma.client.create({ data: { ...parsed.data, organizationId: orgId }, }); reply.code(201).send(created); }); app.put('/api/clients/:id', { preHandler: app.requireDocPermission('user') }, async (req, reply) => { const orgId = getOrganizationId(req); const { id } = req.params as { id: string }; const parsed = ClientUpsert.safeParse(req.body); if (!parsed.success) { reply.code(400).send({ error: 'validation_error', issues: parsed.error.flatten() }); return; } const existing = await prisma.client.findFirst({ where: { id, organizationId: orgId } }); if (!existing) { reply.code(404).send({ error: 'not_found' }); return; } const updated = await prisma.client.update({ where: { id }, data: parsed.data }); return updated; }); app.delete('/api/clients/:id', { preHandler: app.requireDocPermission('admin') }, async (req, reply) => { const orgId = getOrganizationId(req); const { id } = req.params as { id: string }; const existing = await prisma.client.findFirst({ where: { id, organizationId: orgId } }); if (!existing) { reply.code(404).send({ error: 'not_found' }); return; } // Не используем onDelete: Cascade на documents.clientId — клиента с документами лучше архивировать. const docCount = await prisma.document.count({ where: { clientId: id } }); if (docCount > 0) { reply.code(409).send({ error: 'has_documents', count: docCount }); return; } await prisma.client.delete({ where: { id } }); reply.code(204).send(); }); }