import fp from 'fastify-plugin'; import { asClass, Lifetime } from 'awilix'; import type { ZodTypeProvider } from 'fastify-type-provider-zod'; import { z } from 'zod/v4'; import { CreateRegimenSchema, UpdateRegimenSchema, RegimenQuerySchema, RegimenResponseSchema, RegimenListResponseSchema, BurnRateResponseSchema, } from '@meshitrack/shared'; import { RegimensRepository } from './regimens.repository.js'; import { RegimensService } from './regimens.service.js'; type AnyRegimenMedication = { medicineId: string; medicineName: string; medicineStrength: number; medicineStrengthUnit: string; medicineForm: string; dosage: number; dosageUnit: string; frequency: string; customFrequencyPerDay?: number | null; timeOfDay?: string | null; instructions?: string | null; }; type AnyRegimenDoc = { _id: string | { toString: () => string }; householdId: string; userId: string; name: string; isActive: boolean; medications: AnyRegimenMedication[]; 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 toRegimenResponse(doc: AnyRegimenDoc) { return { _id: toStr(doc._id), householdId: doc.householdId, userId: doc.userId, name: doc.name, isActive: doc.isActive, medications: doc.medications.map((med) => ({ medicineId: med.medicineId, medicineName: med.medicineName, medicineStrength: med.medicineStrength, medicineStrengthUnit: med.medicineStrengthUnit, medicineForm: med.medicineForm, dosage: med.dosage, dosageUnit: med.dosageUnit, frequency: med.frequency, ...(med.customFrequencyPerDay != null ? { customFrequencyPerDay: med.customFrequencyPerDay } : {}), ...(med.timeOfDay ? { timeOfDay: med.timeOfDay } : {}), ...(med.instructions ? { instructions: med.instructions } : {}), })), createdBy: doc.createdBy, createdAt: toIso(doc.createdAt), updatedAt: toIso(doc.updatedAt), }; } declare module '@fastify/awilix' { interface Cradle { regimensRepository: RegimensRepository; regimensService: RegimensService; } } export default fp( async (fastify) => { fastify.diContainer.register({ regimensRepository: asClass(RegimensRepository, { lifetime: Lifetime.SINGLETON }), regimensService: asClass(RegimensService, { lifetime: Lifetime.SINGLETON }), }); const app = fastify.withTypeProvider(); const householdParams = z.object({ householdId: z.string() }); // GET /api/v1/households/:householdId/regimens — list user's regimens app.route({ method: 'GET', url: '/api/v1/households/:householdId/regimens', schema: { params: householdParams, querystring: RegimenQuerySchema, response: { 200: RegimenListResponseSchema }, }, handler: async (request, reply) => { const service = fastify.diContainer.resolve('regimensService'); const result = await service.list( request.params.householdId, request.user.keycloakId, request.query, ); return reply.send({ data: result.data.map(toRegimenResponse), pagination: result.pagination, }); }, }); // GET /api/v1/households/:householdId/regimens/burn-rate — burn rate + spending projection app.route({ method: 'GET', url: '/api/v1/households/:householdId/regimens/burn-rate', schema: { params: householdParams, response: { 200: BurnRateResponseSchema }, }, handler: async (request, reply) => { const service = fastify.diContainer.resolve('regimensService'); const data = await service.calculateBurnRates( request.params.householdId, request.user.keycloakId, ); return reply.send({ data }); }, }); // GET /api/v1/households/:householdId/regimens/:id — get single regimen app.route({ method: 'GET', url: '/api/v1/households/:householdId/regimens/:id', schema: { params: householdParams.extend({ id: z.string() }), response: { 200: RegimenResponseSchema }, }, handler: async (request, reply) => { const service = fastify.diContainer.resolve('regimensService'); const regimen = await service.getById(request.params.id, request.params.householdId, request.user.keycloakId); return reply.send(toRegimenResponse(regimen)); }, }); // POST /api/v1/households/:householdId/regimens — create regimen app.route({ method: 'POST', url: '/api/v1/households/:householdId/regimens', schema: { params: householdParams, body: CreateRegimenSchema, response: { 201: RegimenResponseSchema }, }, handler: async (request, reply) => { const service = fastify.diContainer.resolve('regimensService'); const regimen = await service.create( request.body, request.params.householdId, request.user.keycloakId, ); return reply.status(201).send(toRegimenResponse(regimen)); }, }); // PATCH /api/v1/households/:householdId/regimens/:id — update regimen app.route({ method: 'PATCH', url: '/api/v1/households/:householdId/regimens/:id', schema: { params: householdParams.extend({ id: z.string() }), body: UpdateRegimenSchema, response: { 200: RegimenResponseSchema }, }, handler: async (request, reply) => { const service = fastify.diContainer.resolve('regimensService'); const regimen = await service.update( request.params.id, request.params.householdId, request.user.keycloakId, request.body, ); return reply.send(toRegimenResponse(regimen)); }, }); // DELETE /api/v1/households/:householdId/regimens/:id — delete regimen app.route({ method: 'DELETE', url: '/api/v1/households/:householdId/regimens/:id', schema: { params: householdParams.extend({ id: z.string() }), response: { 204: z.undefined() }, }, handler: async (request, reply) => { const service = fastify.diContainer.resolve('regimensService'); await service.delete(request.params.id, request.params.householdId, request.user.keycloakId); return reply.status(204).send(); }, }); }, { name: 'regimens-routes', dependencies: ['auth-plugin'], }, );