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, } from '@meshitrack/shared'; import { RefillsRepository } from './refills.repository.js'; import { RefillsService } from './refills.service.js'; function toIso(v: Date | string | { toISOString: () => string }): string { if (typeof v === 'string') return v; return v.toISOString(); } type AnyItem = { _id: string | { toString: () => string }; medicineId: string; medicineName: string; quantity: number; unit: string; estimatedPrice?: number; actualPrice?: number; checked: boolean; checkedAt?: Date | string; addedToCabinet: boolean; storeId?: string; notes?: string; }; type AnyRefillList = { _id: string | { toString: () => string }; householdId: string; name: string; items: AnyItem[]; status: string; preferredStoreId?: string; totalEstimatedCost?: number; createdBy: string; createdAt: Date | string | { toISOString: () => string }; updatedAt: Date | string | { toISOString: () => string }; }; function toItemResponse(rawItem: unknown) { const item = rawItem as AnyItem; return { _id: typeof item._id === 'string' ? item._id : item._id.toString(), medicineId: item.medicineId, medicineName: item.medicineName, quantity: item.quantity, unit: item.unit, ...(item.estimatedPrice != null ? { estimatedPrice: item.estimatedPrice } : {}), ...(item.actualPrice != null ? { actualPrice: item.actualPrice } : {}), checked: item.checked, ...(item.checkedAt != null ? { checkedAt: toIso(item.checkedAt) } : {}), addedToCabinet: item.addedToCabinet, ...(item.storeId != null ? { storeId: item.storeId } : {}), ...(item.notes != null ? { notes: item.notes } : {}), }; } function toListResponse(rawDoc: unknown) { const doc = rawDoc as AnyRefillList; return { _id: typeof doc._id === 'string' ? doc._id : doc._id.toString(), householdId: doc.householdId, name: doc.name, items: doc.items.map(toItemResponse), status: doc.status as never, ...(doc.preferredStoreId != null ? { preferredStoreId: doc.preferredStoreId } : {}), ...(doc.totalEstimatedCost != null ? { totalEstimatedCost: doc.totalEstimatedCost } : {}), createdBy: doc.createdBy, createdAt: toIso(doc.createdAt), updatedAt: toIso(doc.updatedAt), }; } 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 alerts = await service.getAlerts( request.params.householdId, request.query.userId ?? request.user.keycloakId, request.query.thresholdDays, ); return reply.send({ data: alerts.map((a) => ({ ...a, lastKnownPrice: a.lastKnownPrice ? { ...a.lastKnownPrice, date: toIso(a.lastKnownPrice.date) } : undefined, cheapestOption: a.cheapestOption ? { ...a.cheapestOption, date: toIso(a.cheapestOption.date) } : 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 list = await service.createList( request.body, request.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 result = await service.list(request.params.householdId, request.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 list = await service.getById(request.params.id, request.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 list = await service.updateList( request.params.id, request.params.householdId, request.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 list = await service.updateItem( request.params.id, request.params.householdId, request.params.itemId, request.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 result = await service.addToCabinet( request.params.id, request.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 comparisons = await service.getStoreComparison( request.params.id, request.params.householdId, ); return reply.send({ data: comparisons.map((c) => ({ medicineId: c.medicineId, storeOptions: c.storeOptions.map((opt) => ({ ...opt, date: toIso(opt.date), })), })), }); }, }); }, { name: 'refills-routes', dependencies: ['auth-plugin'], }, );