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

309 lines
10 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,
type CreateRefillListInput,
type UpdateRefillListInput,
type UpdateRefillListItemInput,
type RefillListQueryInput,
} from '@meshitrack/shared';
import { RefillsRepository } from './refills.repository.js';
import { RefillsService } from './refills.service.js';
import type { RefillListDocument } from '../../schemas/refill-list.schema.js';
interface SerializedRefillListItemResponse {
_id: string;
medicineId: string;
medicineName: string;
quantity: number;
unit: string;
estimatedPrice?: number;
actualPrice?: number;
checked: boolean;
checkedAt?: string;
addedToCabinet: boolean;
storeId?: string;
notes?: string;
}
interface SerializedRefillListResponse {
_id: string;
householdId: string;
name: string;
items: SerializedRefillListItemResponse[];
status: 'active' | 'completed' | 'cancelled';
preferredStoreId?: string;
totalEstimatedCost?: number;
createdBy: string;
createdAt: string;
updatedAt: string;
}
function toListResponse(doc: RefillListDocument): SerializedRefillListResponse {
const docAny = doc as unknown as {
preferredStoreId?: string | null;
totalEstimatedCost?: number | null;
createdAt?: { toISOString?: () => string } | string;
updatedAt?: { toISOString?: () => string } | string;
};
const getIsoStr = (d: { toISOString?: () => string } | string | undefined | null): string => {
if (!d) return '';
if (typeof d === 'string') return d;
if (typeof d.toISOString === 'function') return d.toISOString();
return String(d);
};
const response: SerializedRefillListResponse = {
_id: doc._id.toString(),
householdId: doc.householdId,
name: doc.name,
items: doc.items.map((item) => {
const itemAny = item as unknown as {
_id: { toString: () => string };
estimatedPrice?: number | null;
actualPrice?: number | null;
checkedAt?: { toISOString?: () => string } | string | Date;
storeId?: string | null;
notes?: string | null;
};
const itemResponse: SerializedRefillListItemResponse = {
_id: itemAny._id.toString(),
medicineId: item.medicineId,
medicineName: item.medicineName,
quantity: item.quantity,
unit: item.unit,
checked: item.checked,
addedToCabinet: item.addedToCabinet,
};
if (itemAny.estimatedPrice != null) itemResponse.estimatedPrice = itemAny.estimatedPrice;
if (itemAny.actualPrice != null) itemResponse.actualPrice = itemAny.actualPrice;
if (itemAny.checkedAt) {
if (typeof itemAny.checkedAt === 'string') {
itemResponse.checkedAt = itemAny.checkedAt;
} else if (itemAny.checkedAt instanceof Date) {
itemResponse.checkedAt = itemAny.checkedAt.toISOString();
} else if (typeof itemAny.checkedAt.toISOString === 'function') {
itemResponse.checkedAt = itemAny.checkedAt.toISOString();
}
}
if (itemAny.storeId) itemResponse.storeId = itemAny.storeId;
if (itemAny.notes) itemResponse.notes = itemAny.notes;
return itemResponse;
}),
status: doc.status as 'active' | 'completed' | 'cancelled',
createdBy: doc.createdBy,
createdAt: getIsoStr(docAny.createdAt),
updatedAt: getIsoStr(docAny.updatedAt),
};
if (docAny.preferredStoreId) response.preferredStoreId = docAny.preferredStoreId;
if (docAny.totalEstimatedCost != null) response.totalEstimatedCost = docAny.totalEstimatedCost;
return response;
}
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 params = request.params as { householdId: string };
const query = request.query as { userId?: string; thresholdDays?: number };
const alerts = await service.getAlerts(
params.householdId,
query.userId ?? request.user.keycloakId,
query.thresholdDays,
);
return reply.send({
data: alerts.map((a) => ({
...a,
lastKnownPrice: a.lastKnownPrice
? { ...a.lastKnownPrice, date: a.lastKnownPrice.date.toISOString() }
: undefined,
cheapestOption: a.cheapestOption
? { ...a.cheapestOption, date: a.cheapestOption.date.toISOString() }
: 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 params = request.params as { householdId: string };
const body = request.body as CreateRefillListInput;
const list = await service.createList(body, 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 params = request.params as { householdId: string };
const query = request.query as RefillListQueryInput;
const result = await service.list(params.householdId, 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 params = request.params as { householdId: string; id: string };
const list = await service.getById(params.id, 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 params = request.params as { householdId: string; id: string };
const body = request.body as UpdateRefillListInput;
const list = await service.updateList(params.id, params.householdId, 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 params = request.params as { householdId: string; id: string; itemId: string };
const body = request.body as UpdateRefillListItemInput;
const list = await service.updateItem(params.id, params.householdId, params.itemId, 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 params = request.params as { householdId: string; id: string };
const result = await service.addToCabinet(
params.id,
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 params = request.params as { householdId: string; id: string };
const comparisons = await service.getStoreComparison(params.id, params.householdId);
return reply.send({
data: comparisons.map((c) => ({
medicineId: c.medicineId,
storeOptions: c.storeOptions.map((opt) => ({
...opt,
date: opt.date.toISOString(),
})),
})),
});
},
});
},
{
name: 'refills-routes',
dependencies: ['auth-plugin'],
},
);