import fp from 'fastify-plugin'; import { asClass, Lifetime } from 'awilix'; import type { ZodTypeProvider } from 'fastify-type-provider-zod'; import { z } from 'zod/v4'; import { CreateCabinetItemSchema, UpdateCabinetItemSchema, AdjustQuantitySchema, CabinetQuerySchema, CabinetItemResponseSchema, CabinetItemListResponseSchema, CabinetSummaryResponseSchema, } from '@meshitrack/shared'; import { CabinetRepository } from './cabinet.repository.js'; import { CabinetService } from './cabinet.service.js'; type AnyCabinetDoc = { _id: string | { toString: () => string }; householdId: string; medicineId: string; medicineName: string; medicineStrength: number; medicineStrengthUnit: string; medicineForm: string; medicineProductId?: string | null; medicineProductBrand?: string | null; concentration?: number | null; concentrationUnit?: string | null; quantity: number; unit: string; expirationDate?: Date | string | null; status: string; notes?: string | null; createdBy: string; createdAt: string | { toISOString: () => string }; updatedAt: string | { toISOString: () => string }; }; function toStr(v: string | { toString: () => string }): string { return typeof v === 'string' ? v : v.toString(); } function toIso(v: string | Date | { toISOString: () => string }): string { if (typeof v === 'string') return v; if (v instanceof Date) return v.toISOString(); return v.toISOString(); } function toOptIso(v: Date | string | null | undefined): string | undefined { /* v8 ignore next */ if (!v) return undefined; if (typeof v === 'string') return v; return v.toISOString(); } function toCabinetItemResponse(doc: AnyCabinetDoc): z.infer { return { _id: toStr(doc._id), householdId: doc.householdId, medicineId: doc.medicineId, medicineName: doc.medicineName, medicineStrength: doc.medicineStrength, medicineStrengthUnit: doc.medicineStrengthUnit, medicineForm: doc.medicineForm, ...(doc.medicineProductId ? { medicineProductId: doc.medicineProductId } : {}), ...(doc.medicineProductBrand ? { medicineProductBrand: doc.medicineProductBrand } : {}), ...(doc.concentration != null ? { concentration: doc.concentration } : {}), ...(doc.concentrationUnit ? { concentrationUnit: doc.concentrationUnit } : {}), quantity: doc.quantity, unit: doc.unit, ...(doc.expirationDate ? { expirationDate: toOptIso(doc.expirationDate) } : {}), status: doc.status, ...(doc.notes ? { notes: doc.notes } : {}), createdBy: doc.createdBy, createdAt: toIso(doc.createdAt), updatedAt: toIso(doc.updatedAt), }; } declare module '@fastify/awilix' { interface Cradle { cabinetRepository: CabinetRepository; cabinetService: CabinetService; } } export default fp( async (fastify) => { fastify.diContainer.register({ cabinetRepository: asClass(CabinetRepository, { lifetime: Lifetime.SINGLETON }), cabinetService: asClass(CabinetService, { lifetime: Lifetime.SINGLETON }), }); const app = fastify.withTypeProvider(); const householdParams = z.object({ householdId: z.string() }); // GET /api/v1/households/:householdId/cabinet — list items app.route({ method: 'GET', url: '/api/v1/households/:householdId/cabinet', schema: { params: householdParams, querystring: CabinetQuerySchema, response: { 200: CabinetItemListResponseSchema }, }, handler: async (request, reply) => { const service = fastify.diContainer.resolve('cabinetService'); const result = await service.list(request.params.householdId, request.query); return reply.send({ data: result.data.map(toCabinetItemResponse), pagination: result.pagination, }); }, }); // GET /api/v1/households/:householdId/cabinet/summary — aggregate per medicine app.route({ method: 'GET', url: '/api/v1/households/:householdId/cabinet/summary', schema: { params: householdParams, response: { 200: CabinetSummaryResponseSchema }, }, handler: async (request, reply) => { const service = fastify.diContainer.resolve('cabinetService'); const data = await service.getSummary(request.params.householdId); return reply.send({ data }); }, }); // GET /api/v1/households/:householdId/cabinet/expiring-soon — items expiring within N days app.route({ method: 'GET', url: '/api/v1/households/:householdId/cabinet/expiring-soon', schema: { params: householdParams, querystring: z.object({ days: z.coerce.number().int().min(1).max(365).default(30), }), response: { 200: z.object({ data: z.array(CabinetItemResponseSchema) }), }, }, handler: async (request, reply) => { const service = fastify.diContainer.resolve('cabinetService'); const items = await service.getExpiringSoon(request.params.householdId, request.query.days); return reply.send({ data: items.map(toCabinetItemResponse) }); }, }); // GET /api/v1/households/:householdId/cabinet/:id — get single item app.route({ method: 'GET', url: '/api/v1/households/:householdId/cabinet/:id', schema: { params: householdParams.extend({ id: z.string() }), response: { 200: CabinetItemResponseSchema }, }, handler: async (request, reply) => { const service = fastify.diContainer.resolve('cabinetService'); const item = await service.getById(request.params.id, request.params.householdId); return reply.send(toCabinetItemResponse(item)); }, }); // POST /api/v1/households/:householdId/cabinet — add item app.route({ method: 'POST', url: '/api/v1/households/:householdId/cabinet', schema: { params: householdParams, body: CreateCabinetItemSchema, response: { 201: CabinetItemResponseSchema }, }, handler: async (request, reply) => { const service = fastify.diContainer.resolve('cabinetService'); const item = await service.addItem( request.body, request.params.householdId, request.user.keycloakId, ); return reply.status(201).send(toCabinetItemResponse(item)); }, }); // PATCH /api/v1/households/:householdId/cabinet/:id — update item app.route({ method: 'PATCH', url: '/api/v1/households/:householdId/cabinet/:id', schema: { params: householdParams.extend({ id: z.string() }), body: UpdateCabinetItemSchema, response: { 200: CabinetItemResponseSchema }, }, handler: async (request, reply) => { const service = fastify.diContainer.resolve('cabinetService'); const item = await service.update( request.params.id, request.params.householdId, request.body, ); return reply.send(toCabinetItemResponse(item)); }, }); // POST /api/v1/households/:householdId/cabinet/:id/adjust — adjust quantity app.route({ method: 'POST', url: '/api/v1/households/:householdId/cabinet/:id/adjust', schema: { params: householdParams.extend({ id: z.string() }), body: AdjustQuantitySchema, response: { 200: CabinetItemResponseSchema }, }, handler: async (request, reply) => { const service = fastify.diContainer.resolve('cabinetService'); const item = await service.adjustQuantity( request.params.id, request.params.householdId, request.body.delta, ); return reply.send(toCabinetItemResponse(item)); }, }); // DELETE /api/v1/households/:householdId/cabinet/:id — soft delete app.route({ method: 'DELETE', url: '/api/v1/households/:householdId/cabinet/:id', schema: { params: householdParams.extend({ id: z.string() }), response: { 204: z.undefined() }, }, handler: async (request, reply) => { const service = fastify.diContainer.resolve('cabinetService'); await service.delete(request.params.id, request.params.householdId); return reply.status(204).send(); }, }); }, { name: 'cabinet-routes', dependencies: ['auth-plugin'], }, );