import type { RefillsRepository } from './refills.repository.js'; import type { RegimensService } from '../regimens/regimens.service.js'; import type { CabinetRepository } from '../cabinet/cabinet.repository.js'; import type { CabinetService } from '../cabinet/cabinet.service.js'; import type { MedicinePricesRepository } from '../medicine-prices/medicine-prices.repository.js'; import type { PurchasesRepository } from '../purchases/purchases.repository.js'; import type { CreateRefillListInput, UpdateRefillListInput, UpdateRefillListItemInput, RefillListQueryInput, RefillAlertQueryInput, } from '@meshitrack/shared'; import { RefillListStatus } from '@meshitrack/shared'; import { NotFoundError } from '../../common/errors.js'; interface Deps { refillsRepository: RefillsRepository; regimensService: RegimensService; cabinetRepository: CabinetRepository; cabinetService: CabinetService; medicinePricesRepository: MedicinePricesRepository; purchasesRepository: PurchasesRepository; } export class RefillsService { private readonly refillsRepository: RefillsRepository; private readonly regimensService: RegimensService; private readonly cabinetRepository: CabinetRepository; private readonly cabinetService: CabinetService; private readonly medicinePricesRepository: MedicinePricesRepository; private readonly purchasesRepository: PurchasesRepository; public constructor({ refillsRepository, regimensService, cabinetRepository, cabinetService, medicinePricesRepository, purchasesRepository, }: Deps) { this.refillsRepository = refillsRepository; this.regimensService = regimensService; this.cabinetRepository = cabinetRepository; this.cabinetService = cabinetService; this.medicinePricesRepository = medicinePricesRepository; this.purchasesRepository = purchasesRepository; } public async getAlerts(householdId: string, userId: string, thresholdDays = 7) { const burnRates = await this.regimensService.calculateBurnRates(householdId, userId); const triggered = burnRates.filter( (br) => br.daysUntilEmpty !== null && br.daysUntilEmpty <= thresholdDays, ); if (triggered.length === 0) return []; // Get strength data from cabinet aggregate const summaries = await this.cabinetRepository.getAggregateSummary(householdId); const summaryMap = new Map(); for (const s of summaries) { summaryMap.set(s._id as string, { medicineStrength: s.medicineStrength as number, medicineStrengthUnit: s.medicineStrengthUnit as string, }); } // Get pending stock from ordered purchases const pendingStockRows = await this.purchasesRepository.getPendingMedicineStock(householdId); const pendingStockMap = new Map(); for (const row of pendingStockRows) { pendingStockMap.set(row.medicineId, row.totalUnits); } const alerts = await Promise.all( triggered.map(async (br) => { const summary = summaryMap.get(br.medicineId); const suggestedQuantity = Math.ceil(br.dailyConsumption * 30); const pendingOrderStock = pendingStockMap.get(br.medicineId) ?? 0; const daysUntilEmptyWithOrders = br.dailyConsumption > 0 ? (br.totalInCabinet + pendingOrderStock) / br.dailyConsumption : null; const [latestRecord, comparisons] = await Promise.all([ this.medicinePricesRepository.getLatestForMedicine(householdId, br.medicineId), this.medicinePricesRepository.compareStores(householdId, br.medicineId), ]); return { medicineId: br.medicineId, medicineName: br.medicineName, medicineStrength: summary?.medicineStrength ?? 0, medicineStrengthUnit: (summary?.medicineStrengthUnit ?? 'mg') as never, daysUntilEmpty: br.daysUntilEmpty as number, dailyConsumption: br.dailyConsumption, currentStock: br.totalInCabinet, pendingOrderStock, daysUntilEmptyWithOrders, suggestedQuantity, lastKnownPrice: latestRecord ? { price: latestRecord.price as number, pricePerUnit: latestRecord.pricePerUnit as number, storeName: latestRecord.storeName as string, storeId: latestRecord.storeId as string, date: latestRecord.date as Date, } : undefined, cheapestOption: comparisons.length > 0 ? { price: comparisons[0].latestPrice, pricePerUnit: comparisons[0].latestPricePerUnit, storeName: comparisons[0].storeName, storeId: comparisons[0].storeId, date: comparisons[0].date, } : undefined, }; }), ); return alerts; } public async createList(data: CreateRefillListInput, householdId: string, userId: string) { let items: Array<{ medicineId: string; medicineName: string; quantity: number; unit: string; estimatedPrice?: number; storeId?: string; notes?: string; }> = []; if (data.fromAlerts) { const alerts = await this.getAlerts(householdId, userId, data.thresholdDays); items = alerts.map((alert) => ({ medicineId: alert.medicineId, medicineName: alert.medicineName, quantity: alert.suggestedQuantity, unit: 'tablet' as string, estimatedPrice: alert.cheapestOption?.price ?? alert.lastKnownPrice?.price, storeId: alert.cheapestOption?.storeId ?? alert.lastKnownPrice?.storeId, })); } else if (data.items) { items = data.items.map((item) => ({ medicineId: item.medicineId, medicineName: item.medicineName, quantity: item.quantity, unit: item.unit as string, estimatedPrice: item.estimatedPrice, storeId: item.storeId, notes: item.notes, })); } const totalEstimatedCost = items.length > 0 ? items.reduce((sum, item) => sum + (item.estimatedPrice ?? 0), 0) || undefined : undefined; return this.refillsRepository.create({ householdId, name: data.name, status: RefillListStatus.ACTIVE, preferredStoreId: data.preferredStoreId, totalEstimatedCost, createdBy: userId, items, }); } public async list(householdId: string, query: RefillListQueryInput) { return this.refillsRepository.findByHousehold(householdId, query); } public async getById(id: string, householdId: string) { const list = await this.refillsRepository.findById(id, householdId); if (!list) throw new NotFoundError('Refill list not found'); return list; } public async updateList(id: string, householdId: string, data: UpdateRefillListInput) { await this.getById(id, householdId); const updated = await this.refillsRepository.update(id, householdId, data); if (!updated) throw new NotFoundError('Refill list not found'); return updated; } public async updateItem( listId: string, householdId: string, itemId: string, data: UpdateRefillListItemInput, ) { await this.getById(listId, householdId); const updateData: UpdateRefillListItemInput & { checkedAt?: Date } = { ...data }; if (data.checked === true) { updateData.checkedAt = new Date(); } const updated = await this.refillsRepository.updateItem( listId, householdId, itemId, updateData, ); if (!updated) throw new NotFoundError('Refill list or item not found'); return updated; } public async addToCabinet(listId: string, householdId: string, userId: string) { const list = await this.getById(listId, householdId); const checkedItems = (list.items as Array<{ _id: { toString: () => string }; medicineId: string; medicineName: string; quantity: number; unit: string; actualPrice?: number; storeId?: string; checked: boolean; addedToCabinet: boolean; }>).filter((item) => item.checked && !item.addedToCabinet); if (checkedItems.length === 0) { return { addedCount: 0, priceRecordsCreated: 0 }; } let addedCount = 0; const addedItemIds: string[] = []; for (const item of checkedItems) { await this.cabinetService.addItem( { medicineId: item.medicineId, quantity: item.quantity, unit: item.unit as never, unitPrice: item.actualPrice !== undefined && item.quantity > 0 ? item.actualPrice / item.quantity : undefined, totalPrice: item.actualPrice, storeId: item.storeId, purchaseDate: new Date().toISOString(), }, householdId, userId, ); addedCount++; addedItemIds.push(item._id.toString()); } await this.refillsRepository.markItemsAddedToCabinet(listId, householdId, addedItemIds); return { addedCount, priceRecordsCreated: 0 }; } public async getStoreComparison(listId: string, householdId: string) { const list = await this.getById(listId, householdId); const medicineIds = [ ...new Set( (list.items as Array<{ medicineId: string }>).map((item) => item.medicineId), ), ]; const comparisons = await Promise.all( medicineIds.map(async (medicineId) => { const storeOptions = await this.medicinePricesRepository.compareStores( householdId, medicineId, ); return { medicineId, storeOptions }; }), ); return comparisons.filter((c) => c.storeOptions.length > 0); } }