import type { RegimensRepository } from './regimens.repository.js'; import type { MedicinesRepository } from '../medicines/medicines.repository.js'; import type { CabinetRepository } from '../cabinet/cabinet.repository.js'; import type { CabinetEventsService } from '../cabinet-events/cabinet-events.service.js'; import type { CreateRegimenInput, UpdateRegimenInput, RegimenQueryInput } from '@meshitrack/shared'; import { getFrequencyMultiplier } from '@meshitrack/shared'; import type { DosageFrequency } from '@meshitrack/shared'; import { NotFoundError } from '../../common/errors.js'; interface Deps { regimensRepository: RegimensRepository; medicinesRepository: MedicinesRepository; cabinetRepository: CabinetRepository; cabinetEventsService: CabinetEventsService; } export class RegimensService { private readonly regimensRepository: RegimensRepository; private readonly medicinesRepository: MedicinesRepository; private readonly cabinetRepository: CabinetRepository; private readonly cabinetEventsService: CabinetEventsService; public constructor({ regimensRepository, medicinesRepository, cabinetRepository, cabinetEventsService, }: Deps) { this.regimensRepository = regimensRepository; this.medicinesRepository = medicinesRepository; this.cabinetRepository = cabinetRepository; this.cabinetEventsService = cabinetEventsService; } public async list(householdId: string, userId: string, query: RegimenQueryInput) { return this.regimensRepository.findByHousehold(householdId, userId, query); } public async getById(id: string, householdId: string, userId: string) { const regimen = await this.regimensRepository.findById(id, householdId, userId); if (!regimen) throw new NotFoundError('Regimen not found'); return regimen; } public async create(data: CreateRegimenInput, householdId: string, userId: string) { const medications = await this.denormalizeMedications(data.medications, householdId); return this.regimensRepository.create( { name: data.name, isActive: data.isActive, medications }, householdId, userId, userId, ); } public async update(id: string, householdId: string, userId: string, data: UpdateRegimenInput) { await this.getById(id, householdId, userId); const updateData: Record = {}; if (data.name !== undefined) updateData['name'] = data.name; if (data.isActive !== undefined) updateData['isActive'] = data.isActive; if (data.medications !== undefined) { updateData['medications'] = await this.denormalizeMedications(data.medications, householdId); } const updated = await this.regimensRepository.update(id, householdId, userId, updateData); if (!updated) throw new NotFoundError('Regimen not found'); return updated; } public async delete(id: string, householdId: string, userId: string) { await this.getById(id, householdId, userId); const deleted = await this.regimensRepository.softDelete(id, householdId, userId); if (!deleted) throw new NotFoundError('Regimen not found'); return deleted; } public async getActiveByUser(householdId: string, userId: string) { return this.regimensRepository.findActiveByUser(householdId, userId); } public async calculateBurnRates(householdId: string, userId: string) { const regimens = await this.regimensRepository.findActiveByUser(householdId, userId); // Sum daily consumption per medicine across all active regimens const consumptionMap = new Map(); for (const regimen of regimens) { for (const med of regimen.medications) { const multiplier = getFrequencyMultiplier( med.frequency as DosageFrequency, med.customFrequencyPerDay ?? undefined, ); const dailyDose = med.dosage * multiplier; const existing = consumptionMap.get(med.medicineId); if (existing) { existing.dailyConsumption += dailyDose; } else { consumptionMap.set(med.medicineId, { medicineName: med.medicineName, dailyConsumption: dailyDose, }); } } } // Exclude medicines with zero daily consumption (e.g., AS_NEEDED frequency) for (const [id, consumption] of consumptionMap) { if (consumption.dailyConsumption === 0) consumptionMap.delete(id); } if (consumptionMap.size === 0) return []; // Get cabinet summary for all medicines in regimens const summaryResults = await this.cabinetRepository.getAggregateSummary(householdId); const stockMap = new Map(); for (const s of summaryResults) { stockMap.set(s._id as string, { totalQuantity: s.totalQuantity as number, earliestExpiry: (s.earliestExpiry as Date | null) ?? null, }); } // Get average unit prices from purchase events const medicineIds = [...consumptionMap.keys()]; const priceMap = await this.cabinetEventsService.getAvgUnitPrices(householdId, medicineIds); // Build burn rate array const burnRates = []; for (const [medicineId, consumption] of consumptionMap) { const stock = stockMap.get(medicineId); const totalInCabinet = stock?.totalQuantity ?? 0; const earliestExpiry = stock?.earliestExpiry ?? null; const dailyConsumption = consumption.dailyConsumption; /* v8 ignore next */ const daysUntilEmpty = dailyConsumption > 0 ? Math.floor(totalInCabinet / dailyConsumption) : null; const priceData = priceMap.get(medicineId); const avgUnitPrice = priceData?.avgUnitPrice ?? null; const currency = priceData?.currency ?? null; const projectedDailyCost = avgUnitPrice !== null && dailyConsumption > 0 ? avgUnitPrice * dailyConsumption : null; burnRates.push({ medicineId, medicineName: consumption.medicineName, dailyConsumption, totalInCabinet, daysUntilEmpty, earliestExpiry: earliestExpiry ? earliestExpiry.toISOString() : null, avgUnitPrice, projectedDailyCost, projectedMonthlyCost: projectedDailyCost !== null ? projectedDailyCost * 30 : null, projectedYearlyCost: projectedDailyCost !== null ? projectedDailyCost * 365 : null, currency, }); } // Sort by daysUntilEmpty ASC (most urgent first, nulls last) /* v8 ignore next 6 */ burnRates.sort((a, b) => { if (a.daysUntilEmpty === null && b.daysUntilEmpty === null) return 0; if (a.daysUntilEmpty === null) return 1; if (b.daysUntilEmpty === null) return -1; return a.daysUntilEmpty - b.daysUntilEmpty; }); return burnRates; } private async denormalizeMedications( medications: CreateRegimenInput['medications'], householdId: string, ) { const result = []; for (const med of medications) { const medicine = await this.medicinesRepository.findById(med.medicineId, householdId); if (!medicine) { throw new NotFoundError(`Medicine not found: ${med.medicineId}`); } result.push({ medicineId: med.medicineId, medicineName: medicine.name, medicineStrength: medicine.strength, medicineStrengthUnit: medicine.strengthUnit, medicineForm: medicine.form, dosage: med.dosage, dosageUnit: med.dosageUnit, frequency: med.frequency, customFrequencyPerDay: med.customFrequencyPerDay, timeOfDay: med.timeOfDay, instructions: med.instructions, }); } return result; } }