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