import type { FastifyInstance } from 'fastify'; import type { FastifySchema } from 'fastify'; import type pg from 'pg'; import { z } from 'zod'; import { requireRole, type Authenticate } from '../../../shared/auth.js'; import { AppError } from '../../../shared/errors.js'; import { errorSchema } from '../../../shared/swagger.js'; import { parseJson } from '../../../shared/http-input.js'; import { CreateBrand, GetBrandBySlug, ListBrands, UpdateBrand, } from '../application/brand-use-cases.js'; import type { Brand } from '../domain/brand.js'; import { BrandSlugAlreadyExistsError } from '../domain/errors.js'; import { PgBrandRepository } from '../infrastructure/pg-brand-repository.js'; export interface BrandsRoutesDeps { pool: pg.Pool; authenticate: Authenticate; } const slugSchema = z .string() .min(1) .max(160) .regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/); const slugParamSchema = z.object({ slug: slugSchema }); const idParamSchema = z.object({ id: z.uuid() }); const newBrandSchema = z.object({ name: z.string().min(1).max(200), slug: slugSchema, seoTitle: z.string().min(1).max(200).optional().nullable(), seoDescription: z.string().min(1).max(500).optional().nullable(), }); const brandPatchSchema = newBrandSchema .partial() .refine((value) => Object.values(value).some((field) => field !== undefined), { message: 'At least one brand field is required', }); export async function registerBrandsRoutes( app: FastifyInstance, deps: BrandsRoutesDeps, ): Promise { const repository = new PgBrandRepository(deps.pool); const getBySlug = new GetBrandBySlug(repository); const listBrands = new ListBrands(repository); const createBrand = new CreateBrand(repository); const updateBrand = new UpdateBrand(repository); const listBrandsSchema: FastifySchema = { tags: ['Brands'], summary: 'List brands (público)', }; app.get('/brands', { schema: listBrandsSchema }, async (_request, reply) => { const items = await listBrands.execute(); return reply.send({ items: items.map(serializeBrand) }); }); const publicBrandSchema: FastifySchema = { tags: ['Brands'], summary: 'Get brand by slug (público)', params: { type: 'object', required: ['slug'], properties: { slug: { type: 'string' } } }, response: { 404: errorSchema }, }; app.get('/marca/:slug', { schema: publicBrandSchema }, async (request, reply) => { const { slug } = parseJson(slugParamSchema, request.params); const brand = await getBySlug.execute(slug); if (!brand) { throw new AppError(404, 'NOT_FOUND', 'Brand not found'); } return reply.send(serializeBrand(brand)); }); const createBrandSchema: FastifySchema = { tags: ['Brands'], summary: 'Create brand (admin)', body: { type: 'object' }, response: { 201: { type: 'object' }, 401: errorSchema, 403: errorSchema }, }; app.post('/brands', { schema: createBrandSchema }, async (request, reply) => { const user = await deps.authenticate(request); requireRole(user, 'admin'); const input = parseJson(newBrandSchema, request.body); try { const brand = await createBrand.execute(input); return reply.code(201).send(serializeBrand(brand)); } catch (error) { throw mapBrandError(error); } }); const updateBrandSchema: FastifySchema = { tags: ['Brands'], summary: 'Update brand (admin)', params: { type: 'object', required: ['id'], properties: { id: { type: 'string', format: 'uuid' } }, }, body: { type: 'object' }, response: { 401: errorSchema, 403: errorSchema, 404: errorSchema }, }; app.patch('/brands/:id', { schema: updateBrandSchema }, async (request, reply) => { const user = await deps.authenticate(request); requireRole(user, 'admin'); const { id } = parseJson(idParamSchema, request.params); const patch = parseJson(brandPatchSchema, request.body); try { const brand = await updateBrand.execute(id, patch); if (!brand) { throw new AppError(404, 'NOT_FOUND', 'Brand not found'); } return reply.send(serializeBrand(brand)); } catch (error) { throw mapBrandError(error); } }); // DELETE /brands/:id const deleteBrandSchema: FastifySchema = { tags: ['Brands'], summary: 'Delete brand (admin)', params: { type: 'object', required: ['id'], properties: { id: { type: 'string', format: 'uuid' } }, }, response: { 204: { type: 'null' }, 401: errorSchema, 403: errorSchema }, }; app.delete('/brands/:id', { schema: deleteBrandSchema }, async (request, reply) => { const user = await deps.authenticate(request); requireRole(user, 'admin'); const { id } = parseJson(idParamSchema, request.params); try { await repository.delete(id); } catch { throw new AppError(404, 'NOT_FOUND', 'Brand not found'); } return reply.code(204).send(); }); } function mapBrandError(error: unknown): Error { if (error instanceof BrandSlugAlreadyExistsError) { return new AppError(409, 'BRAND_SLUG_EXISTS', error.message); } return error instanceof Error ? error : new Error('Unknown brand error'); } function serializeBrand(brand: Brand) { return { id: brand.id, name: brand.name, slug: brand.slug, url: `/marca/${brand.slug}`, seoTitle: brand.seoTitle, seoDescription: brand.seoDescription, createdAt: brand.createdAt.toISOString(), updatedAt: brand.updatedAt.toISOString(), }; }