280 lines
8.7 KiB
TypeScript
280 lines
8.7 KiB
TypeScript
|
|
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'],
|
||
|
|
},
|
||
|
|
);
|