import fp from 'fastify-plugin'; import { asClass, Lifetime } from 'awilix'; import type { ZodTypeProvider } from 'fastify-type-provider-zod'; import { z } from 'zod/v4'; import { CreateRefillListSchema, UpdateRefillListSchema, UpdateRefillListItemSchema, RefillListQuerySchema, RefillAlertQuerySchema, RefillAlertResponseSchema, RefillListResponseSchema, RefillListListResponseSchema, AddToCabinetResponseSchema, StoreComparisonItemSchema, type CreateRefillListInput, type UpdateRefillListInput, type UpdateRefillListItemInput, type RefillListQueryInput, } from '@meshitrack/shared'; import { RefillsRepository } from './refills.repository.js'; import { RefillsService } from './refills.service.js'; import type { RefillListDocument } from '../../schemas/refill-list.schema.js'; interface SerializedRefillListItemResponse { _id: string; medicineId: string; medicineName: string; quantity: number; unit: string; estimatedPrice?: number; actualPrice?: number; checked: boolean; checkedAt?: string; addedToCabinet: boolean; storeId?: string; notes?: string; } interface SerializedRefillListResponse { _id: string; householdId: string; name: string; items: SerializedRefillListItemResponse[]; status: 'active' | 'completed' | 'cancelled'; preferredStoreId?: string; totalEstimatedCost?: number; createdBy: string; createdAt: string; updatedAt: string; } function toListResponse(doc: RefillListDocument): SerializedRefillListResponse { const docAny = doc as unknown as { preferredStoreId?: string | null; totalEstimatedCost?: number | null; createdAt?: { toISOString?: () => string } | string; updatedAt?: { toISOString?: () => string } | string; }; const getIsoStr = (d: { toISOString?: () => string } | string | undefined | null): string => { if (!d) return ''; if (typeof d === 'string') return d; if (typeof d.toISOString === 'function') return d.toISOString(); return String(d); }; const response: SerializedRefillListResponse = { _id: doc._id.toString(), householdId: doc.householdId, name: doc.name, items: doc.items.map((item) => { const itemAny = item as unknown as { _id: { toString: () => string }; estimatedPrice?: number | null; actualPrice?: number | null; checkedAt?: { toISOString?: () => string } | string | Date; storeId?: string | null; notes?: string | null; }; const itemResponse: SerializedRefillListItemResponse = { _id: itemAny._id.toString(), medicineId: item.medicineId, medicineName: item.medicineName, quantity: item.quantity, unit: item.unit, checked: item.checked, addedToCabinet: item.addedToCabinet, }; if (itemAny.estimatedPrice != null) itemResponse.estimatedPrice = itemAny.estimatedPrice; if (itemAny.actualPrice != null) itemResponse.actualPrice = itemAny.actualPrice; if (itemAny.checkedAt) { if (typeof itemAny.checkedAt === 'string') { itemResponse.checkedAt = itemAny.checkedAt; } else if (itemAny.checkedAt instanceof Date) { itemResponse.checkedAt = itemAny.checkedAt.toISOString(); } else if (typeof itemAny.checkedAt.toISOString === 'function') { itemResponse.checkedAt = itemAny.checkedAt.toISOString(); } } if (itemAny.storeId) itemResponse.storeId = itemAny.storeId; if (itemAny.notes) itemResponse.notes = itemAny.notes; return itemResponse; }), status: doc.status as 'active' | 'completed' | 'cancelled', createdBy: doc.createdBy, createdAt: getIsoStr(docAny.createdAt), updatedAt: getIsoStr(docAny.updatedAt), }; if (docAny.preferredStoreId) response.preferredStoreId = docAny.preferredStoreId; if (docAny.totalEstimatedCost != null) response.totalEstimatedCost = docAny.totalEstimatedCost; return response; } declare module '@fastify/awilix' { interface Cradle { refillsRepository: RefillsRepository; refillsService: RefillsService; } } export default fp( async (fastify) => { fastify.diContainer.register({ refillsRepository: asClass(RefillsRepository, { lifetime: Lifetime.SINGLETON }), refillsService: asClass(RefillsService, { lifetime: Lifetime.SINGLETON }), }); const app = fastify.withTypeProvider(); const householdParams = z.object({ householdId: z.string() }); app.route({ method: 'GET', url: '/api/v1/households/:householdId/refills/alerts', schema: { params: householdParams, querystring: RefillAlertQuerySchema, response: { 200: z.object({ data: z.array(RefillAlertResponseSchema) }) }, }, handler: async (request, reply) => { const service = fastify.diContainer.resolve('refillsService'); const params = request.params as { householdId: string }; const query = request.query as { userId?: string; thresholdDays?: number }; const alerts = await service.getAlerts( params.householdId, query.userId ?? request.user.keycloakId, query.thresholdDays, ); return reply.send({ data: alerts.map((a) => ({ ...a, lastKnownPrice: a.lastKnownPrice ? { ...a.lastKnownPrice, date: a.lastKnownPrice.date.toISOString() } : undefined, cheapestOption: a.cheapestOption ? { ...a.cheapestOption, date: a.cheapestOption.date.toISOString() } : undefined, })), }); }, }); app.route({ method: 'POST', url: '/api/v1/households/:householdId/refills/lists', schema: { params: householdParams, body: CreateRefillListSchema, response: { 201: RefillListResponseSchema }, }, handler: async (request, reply) => { const service = fastify.diContainer.resolve('refillsService'); const params = request.params as { householdId: string }; const body = request.body as CreateRefillListInput; const list = await service.createList(body, params.householdId, request.user.keycloakId); return reply.status(201).send(toListResponse(list)); }, }); app.route({ method: 'GET', url: '/api/v1/households/:householdId/refills/lists', schema: { params: householdParams, querystring: RefillListQuerySchema, response: { 200: RefillListListResponseSchema }, }, handler: async (request, reply) => { const service = fastify.diContainer.resolve('refillsService'); const params = request.params as { householdId: string }; const query = request.query as RefillListQueryInput; const result = await service.list(params.householdId, query); return reply.send({ data: result.data.map(toListResponse), pagination: result.pagination, }); }, }); app.route({ method: 'GET', url: '/api/v1/households/:householdId/refills/lists/:id', schema: { params: householdParams.extend({ id: z.string() }), response: { 200: RefillListResponseSchema }, }, handler: async (request, reply) => { const service = fastify.diContainer.resolve('refillsService'); const params = request.params as { householdId: string; id: string }; const list = await service.getById(params.id, params.householdId); return reply.send(toListResponse(list)); }, }); app.route({ method: 'PATCH', url: '/api/v1/households/:householdId/refills/lists/:id', schema: { params: householdParams.extend({ id: z.string() }), body: UpdateRefillListSchema, response: { 200: RefillListResponseSchema }, }, handler: async (request, reply) => { const service = fastify.diContainer.resolve('refillsService'); const params = request.params as { householdId: string; id: string }; const body = request.body as UpdateRefillListInput; const list = await service.updateList(params.id, params.householdId, body); return reply.send(toListResponse(list)); }, }); app.route({ method: 'PATCH', url: '/api/v1/households/:householdId/refills/lists/:id/items/:itemId', schema: { params: householdParams.extend({ id: z.string(), itemId: z.string() }), body: UpdateRefillListItemSchema, response: { 200: RefillListResponseSchema }, }, handler: async (request, reply) => { const service = fastify.diContainer.resolve('refillsService'); const params = request.params as { householdId: string; id: string; itemId: string }; const body = request.body as UpdateRefillListItemInput; const list = await service.updateItem(params.id, params.householdId, params.itemId, body); return reply.send(toListResponse(list)); }, }); app.route({ method: 'POST', url: '/api/v1/households/:householdId/refills/lists/:id/add-to-cabinet', schema: { params: householdParams.extend({ id: z.string() }), response: { 200: AddToCabinetResponseSchema }, }, handler: async (request, reply) => { const service = fastify.diContainer.resolve('refillsService'); const params = request.params as { householdId: string; id: string }; const result = await service.addToCabinet( params.id, params.householdId, request.user.keycloakId, ); return reply.send(result); }, }); app.route({ method: 'GET', url: '/api/v1/households/:householdId/refills/lists/:id/store-comparison', schema: { params: householdParams.extend({ id: z.string() }), response: { 200: z.object({ data: z.array( z.object({ medicineId: z.string(), storeOptions: z.array(StoreComparisonItemSchema), }), ), }), }, }, handler: async (request, reply) => { const service = fastify.diContainer.resolve('refillsService'); const params = request.params as { householdId: string; id: string }; const comparisons = await service.getStoreComparison(params.id, params.householdId); return reply.send({ data: comparisons.map((c) => ({ medicineId: c.medicineId, storeOptions: c.storeOptions.map((opt) => ({ ...opt, date: opt.date.toISOString(), })), })), }); }, }); }, { name: 'refills-routes', dependencies: ['auth-plugin'], }, );