Implement regimens
This commit is contained in:
parent
1f66fab30f
commit
9f416903ef
66 changed files with 9130 additions and 189 deletions
202
packages/api/src/modules/regimens/regimens.service.ts
Normal file
202
packages/api/src/modules/regimens/regimens.service.ts
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
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<string, unknown> = {};
|
||||
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<
|
||||
string,
|
||||
{ medicineName: string; dailyConsumption: number }
|
||||
>();
|
||||
|
||||
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<
|
||||
string,
|
||||
{ totalQuantity: number; earliestExpiry: Date | null }
|
||||
>();
|
||||
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;
|
||||
|
||||
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)
|
||||
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;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue