MeshiTrack/packages/api/src/modules/refills/refills.routes.ts

280 lines
8.7 KiB
TypeScript
Raw Normal View History

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<ZodTypeProvider>();
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'],
},
);