import fp from 'fastify-plugin'; import { asClass, Lifetime } from 'awilix'; import type { ZodTypeProvider } from 'fastify-type-provider-zod'; import { z } from 'zod/v4'; import { CreatePriceRecordSchema, BulkPriceRecordInputSchema, PriceHistoryQuerySchema, PriceRecordResponseSchema, PriceHistoryResponseSchema, FoodStoreComparisonResponseSchema, FoodSpendingAnalyticsResponseSchema, } from '@meshitrack/shared'; import { PricesRepository } from './prices.repository.js'; import { PricesService } from './prices.service.js'; import { ProductsRepository } from '../products/products.repository.js'; import { StoresRepository } from '../stores/stores.repository.js'; type AnyPriceDoc = { _id: string | { toString: () => string }; householdId: string; productId: string; productName: string; storeId: string; storeName: string; price: number; currency: string; quantity: number; unit: string; pricePerUnit: number; date: Date | string | { toISOString: () => string }; receiptImageUrl?: string; notes?: string; createdBy: string; createdAt: Date | string | { toISOString: () => string }; }; function toIso(v: Date | string | { toISOString: () => string }): string { if (typeof v === 'string') return v; return v.toISOString(); } function toPriceRecordResponse(rawDoc: unknown) { const doc = rawDoc as AnyPriceDoc; return { _id: typeof doc._id === 'string' ? doc._id : doc._id.toString(), householdId: doc.householdId, productId: doc.productId, productName: doc.productName, storeId: doc.storeId, storeName: doc.storeName, price: doc.price, currency: doc.currency, quantity: doc.quantity, unit: doc.unit, pricePerUnit: doc.pricePerUnit, date: toIso(doc.date), ...(doc.receiptImageUrl ? { receiptImageUrl: doc.receiptImageUrl } : {}), ...(doc.notes != null ? { notes: doc.notes } : {}), createdBy: doc.createdBy, createdAt: toIso(doc.createdAt), }; } declare module '@fastify/awilix' { interface Cradle { pricesRepository: PricesRepository; pricesService: PricesService; } } export default fp( async (fastify) => { // Register DI containers fastify.diContainer.register({ pricesRepository: asClass(PricesRepository, { lifetime: Lifetime.SINGLETON }), pricesService: asClass(PricesService, { lifetime: Lifetime.SINGLETON }), productsRepository: asClass(ProductsRepository, { lifetime: Lifetime.SINGLETON }), storesRepository: asClass(StoresRepository, { lifetime: Lifetime.SINGLETON }), }); const app = fastify.withTypeProvider(); const householdParams = z.object({ householdId: z.string() }); app.route({ method: 'POST', url: '/api/v1/households/:householdId/prices', schema: { params: householdParams, body: CreatePriceRecordSchema, response: { 201: PriceRecordResponseSchema }, }, handler: async (request, reply) => { const service = fastify.diContainer.resolve('pricesService'); const record = await service.recordPrice( request.body, request.params.householdId, request.user.keycloakId ); return reply.status(201).send(toPriceRecordResponse(record)); }, }); app.route({ method: 'POST', url: '/api/v1/households/:householdId/prices/bulk', schema: { params: householdParams, body: BulkPriceRecordInputSchema, response: { 201: z.array(PriceRecordResponseSchema) }, }, handler: async (request, reply) => { const service = fastify.diContainer.resolve('pricesService'); const records = await service.recordBulkPrices( request.body, request.params.householdId, request.user.keycloakId ); return reply.status(201).send(records.map(toPriceRecordResponse)); }, }); app.route({ method: 'GET', url: '/api/v1/households/:householdId/prices/history/:productId', schema: { params: householdParams.extend({ productId: z.string() }), querystring: PriceHistoryQuerySchema, response: { 200: PriceHistoryResponseSchema }, }, handler: async (request, reply) => { const service = fastify.diContainer.resolve('pricesService'); const result = await service.getPriceHistory( request.params.productId, request.params.householdId, request.query ); return reply.send({ data: result.data.map(toPriceRecordResponse), pagination: result.pagination, }); }, }); app.route({ method: 'GET', url: '/api/v1/households/:householdId/prices/compare/:productId', schema: { params: householdParams.extend({ productId: z.string() }), response: { 200: FoodStoreComparisonResponseSchema }, }, handler: async (request, reply) => { const service = fastify.diContainer.resolve('pricesService'); const results = await service.compareStores( request.params.productId, request.params.householdId ); return reply.send({ data: results.map((r) => ({ ...r, date: toIso(r.date), })), }); }, }); app.route({ method: 'GET', url: '/api/v1/households/:householdId/prices/analytics', schema: { params: householdParams, response: { 200: FoodSpendingAnalyticsResponseSchema }, }, handler: async (request, reply) => { const service = fastify.diContainer.resolve('pricesService'); const analytics = await service.getAnalytics(request.params.householdId); return reply.send({ ...analytics, priceAlerts: analytics.priceAlerts.map((a) => ({ ...a, date: toIso(a.date) })), }); }, }); }, { name: 'prices-routes', dependencies: ['auth-plugin'], } );