Phase 9
This commit is contained in:
parent
e396f5088c
commit
a1801af63b
36 changed files with 4783 additions and 31 deletions
188
packages/api/src/modules/prices/prices.routes.ts
Normal file
188
packages/api/src/modules/prices/prices.routes.ts
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
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<ZodTypeProvider>();
|
||||
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'],
|
||||
}
|
||||
);
|
||||
Loading…
Add table
Add a link
Reference in a new issue