Implement stores and refills, improve testing
This commit is contained in:
parent
9f416903ef
commit
5536acd67d
137 changed files with 21218 additions and 221 deletions
285
packages/api/src/modules/refills/refills.service.ts
Normal file
285
packages/api/src/modules/refills/refills.service.ts
Normal file
|
|
@ -0,0 +1,285 @@
|
|||
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<string, { medicineStrength: number; medicineStrengthUnit: string }>();
|
||||
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<string, number>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue