Implement regimens
This commit is contained in:
parent
1f66fab30f
commit
9f416903ef
66 changed files with 9130 additions and 189 deletions
|
|
@ -1,5 +1,6 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { CabinetService } from './cabinet.service.js';
|
||||
import { CabinetEventType, CabinetEventSourceType } from '@meshitrack/shared';
|
||||
|
||||
describe(CabinetService.name, () => {
|
||||
const mockCabinetRepo = {
|
||||
|
|
@ -12,6 +13,8 @@ describe(CabinetService.name, () => {
|
|||
findExpiringSoon: vi.fn(),
|
||||
softDelete: vi.fn(),
|
||||
countByMedicineId: vi.fn(),
|
||||
discard: vi.fn(),
|
||||
findActiveByMedicineForFEFO: vi.fn(),
|
||||
};
|
||||
|
||||
const mockMedicinesRepo = {
|
||||
|
|
@ -32,6 +35,15 @@ describe(CabinetService.name, () => {
|
|||
countByMedicineId: vi.fn(),
|
||||
};
|
||||
|
||||
const mockCabinetEventsService = {
|
||||
logEvent: vi.fn(),
|
||||
logEvents: vi.fn(),
|
||||
listEvents: vi.fn(),
|
||||
getEventsByItem: vi.fn(),
|
||||
getSpendingSummary: vi.fn(),
|
||||
getAvgUnitPrices: vi.fn(),
|
||||
};
|
||||
|
||||
let service: CabinetService;
|
||||
|
||||
beforeEach(() => {
|
||||
|
|
@ -40,6 +52,7 @@ describe(CabinetService.name, () => {
|
|||
cabinetRepository: mockCabinetRepo as never,
|
||||
medicinesRepository: mockMedicinesRepo as never,
|
||||
medicineProductsRepository: mockProductsRepo as never,
|
||||
cabinetEventsService: mockCabinetEventsService as never,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -150,6 +163,33 @@ describe(CabinetService.name, () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('logs PURCHASED event after creation', async () => {
|
||||
mockMedicinesRepo.findById.mockResolvedValue({
|
||||
_id: 'med-1',
|
||||
name: 'Metformin',
|
||||
strength: 500,
|
||||
strengthUnit: 'mg',
|
||||
form: 'tablet',
|
||||
});
|
||||
mockCabinetRepo.create.mockResolvedValue({ _id: 'ci-1' });
|
||||
|
||||
await service.addItem(createInput, 'hh1', 'user-1');
|
||||
|
||||
expect(mockCabinetEventsService.logEvent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
householdId: 'hh1',
|
||||
userId: 'user-1',
|
||||
cabinetItemId: 'ci-1',
|
||||
medicineId: 'med-1',
|
||||
eventType: CabinetEventType.PURCHASED,
|
||||
quantity: 30,
|
||||
quantityBefore: 0,
|
||||
quantityAfter: 30,
|
||||
sourceType: CabinetEventSourceType.MANUAL,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('throws NotFoundError when medicine not found', async () => {
|
||||
mockMedicinesRepo.findById.mockResolvedValue(null);
|
||||
|
||||
|
|
@ -199,28 +239,53 @@ describe(CabinetService.name, () => {
|
|||
|
||||
describe('update', () => {
|
||||
it('updates and returns item', async () => {
|
||||
mockCabinetRepo.findById.mockResolvedValue({ _id: 'ci-1' });
|
||||
mockCabinetRepo.findById.mockResolvedValue({ _id: 'ci-1', quantity: 30, medicineId: 'med-1', medicineName: 'Test' });
|
||||
const updated = { _id: 'ci-1', quantity: 25 };
|
||||
mockCabinetRepo.update.mockResolvedValue(updated);
|
||||
|
||||
const result = await service.update('ci-1', 'hh1', { quantity: 25 });
|
||||
const result = await service.update('ci-1', 'hh1', { quantity: 25 }, 'user-1');
|
||||
|
||||
expect(result).toEqual(updated);
|
||||
});
|
||||
|
||||
it('logs ADJUSTED event when quantity changes', async () => {
|
||||
mockCabinetRepo.findById.mockResolvedValue({ _id: 'ci-1', quantity: 30, medicineId: 'med-1', medicineName: 'Test' });
|
||||
mockCabinetRepo.update.mockResolvedValue({ _id: 'ci-1', quantity: 25 });
|
||||
|
||||
await service.update('ci-1', 'hh1', { quantity: 25 }, 'user-1');
|
||||
|
||||
expect(mockCabinetEventsService.logEvent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
eventType: CabinetEventType.ADJUSTED,
|
||||
quantity: -5,
|
||||
quantityBefore: 30,
|
||||
quantityAfter: 25,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('does not log event when quantity unchanged', async () => {
|
||||
mockCabinetRepo.findById.mockResolvedValue({ _id: 'ci-1', quantity: 30, medicineId: 'med-1', medicineName: 'Test' });
|
||||
mockCabinetRepo.update.mockResolvedValue({ _id: 'ci-1', quantity: 30 });
|
||||
|
||||
await service.update('ci-1', 'hh1', { notes: 'updated' }, 'user-1');
|
||||
|
||||
expect(mockCabinetEventsService.logEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('throws NotFoundError when item does not exist', async () => {
|
||||
mockCabinetRepo.findById.mockResolvedValue(null);
|
||||
|
||||
await expect(service.update('ci-missing', 'hh1', { quantity: 25 })).rejects.toThrow(
|
||||
await expect(service.update('ci-missing', 'hh1', { quantity: 25 }, 'user-1')).rejects.toThrow(
|
||||
'Cabinet item not found',
|
||||
);
|
||||
});
|
||||
|
||||
it('throws NotFoundError when update returns null', async () => {
|
||||
mockCabinetRepo.findById.mockResolvedValue({ _id: 'ci-1' });
|
||||
mockCabinetRepo.findById.mockResolvedValue({ _id: 'ci-1', quantity: 30, medicineId: 'med-1', medicineName: 'Test' });
|
||||
mockCabinetRepo.update.mockResolvedValue(null);
|
||||
|
||||
await expect(service.update('ci-1', 'hh1', { quantity: 25 })).rejects.toThrow(
|
||||
await expect(service.update('ci-1', 'hh1', { quantity: 25 }, 'user-1')).rejects.toThrow(
|
||||
'Cabinet item not found',
|
||||
);
|
||||
});
|
||||
|
|
@ -228,17 +293,34 @@ describe(CabinetService.name, () => {
|
|||
|
||||
describe('adjustQuantity', () => {
|
||||
it('adjusts quantity and returns item', async () => {
|
||||
mockCabinetRepo.findById.mockResolvedValue({ _id: 'ci-1', quantity: 30 });
|
||||
mockCabinetRepo.findById.mockResolvedValue({ _id: 'ci-1', quantity: 30, medicineId: 'med-1', medicineName: 'Test' });
|
||||
const updated = { _id: 'ci-1', quantity: 27 };
|
||||
mockCabinetRepo.adjustQuantity.mockResolvedValue(updated);
|
||||
|
||||
const result = await service.adjustQuantity('ci-1', 'hh1', -3);
|
||||
const result = await service.adjustQuantity('ci-1', 'hh1', -3, 'user-1');
|
||||
|
||||
expect(result).toEqual(updated);
|
||||
});
|
||||
|
||||
it('logs ADJUSTED event', async () => {
|
||||
mockCabinetRepo.findById.mockResolvedValue({ _id: 'ci-1', quantity: 30, medicineId: 'med-1', medicineName: 'Test' });
|
||||
mockCabinetRepo.adjustQuantity.mockResolvedValue({ _id: 'ci-1', quantity: 27 });
|
||||
|
||||
await service.adjustQuantity('ci-1', 'hh1', -3, 'user-1', 'took some');
|
||||
|
||||
expect(mockCabinetEventsService.logEvent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
eventType: CabinetEventType.ADJUSTED,
|
||||
quantity: -3,
|
||||
quantityBefore: 30,
|
||||
quantityAfter: 27,
|
||||
reason: 'took some',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('throws BadRequestError when delta is 0', async () => {
|
||||
await expect(service.adjustQuantity('ci-1', 'hh1', 0)).rejects.toThrow(
|
||||
await expect(service.adjustQuantity('ci-1', 'hh1', 0, 'user-1')).rejects.toThrow(
|
||||
'Delta must be non-zero',
|
||||
);
|
||||
});
|
||||
|
|
@ -246,16 +328,16 @@ describe(CabinetService.name, () => {
|
|||
it('throws NotFoundError when item does not exist', async () => {
|
||||
mockCabinetRepo.findById.mockResolvedValue(null);
|
||||
|
||||
await expect(service.adjustQuantity('ci-missing', 'hh1', 5)).rejects.toThrow(
|
||||
await expect(service.adjustQuantity('ci-missing', 'hh1', 5, 'user-1')).rejects.toThrow(
|
||||
'Cabinet item not found',
|
||||
);
|
||||
});
|
||||
|
||||
it('throws NotFoundError when adjust returns null', async () => {
|
||||
mockCabinetRepo.findById.mockResolvedValue({ _id: 'ci-1' });
|
||||
mockCabinetRepo.findById.mockResolvedValue({ _id: 'ci-1', quantity: 30, medicineId: 'med-1', medicineName: 'Test' });
|
||||
mockCabinetRepo.adjustQuantity.mockResolvedValue(null);
|
||||
|
||||
await expect(service.adjustQuantity('ci-1', 'hh1', 5)).rejects.toThrow(
|
||||
await expect(service.adjustQuantity('ci-1', 'hh1', 5, 'user-1')).rejects.toThrow(
|
||||
'Cabinet item not found',
|
||||
);
|
||||
});
|
||||
|
|
@ -274,26 +356,81 @@ describe(CabinetService.name, () => {
|
|||
});
|
||||
|
||||
describe('delete', () => {
|
||||
it('soft deletes item', async () => {
|
||||
mockCabinetRepo.findById.mockResolvedValue({ _id: 'ci-1' });
|
||||
it('soft deletes item and logs DELETED event', async () => {
|
||||
mockCabinetRepo.findById.mockResolvedValue({ _id: 'ci-1', quantity: 10, medicineId: 'med-1', medicineName: 'Test' });
|
||||
mockCabinetRepo.softDelete.mockResolvedValue({ _id: 'ci-1', isDeleted: true });
|
||||
|
||||
const result = await service.delete('ci-1', 'hh1');
|
||||
const result = await service.delete('ci-1', 'hh1', 'user-1');
|
||||
|
||||
expect(result.isDeleted).toBe(true);
|
||||
expect(mockCabinetEventsService.logEvent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
eventType: CabinetEventType.DELETED,
|
||||
quantity: -10,
|
||||
quantityBefore: 10,
|
||||
quantityAfter: 0,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('throws NotFoundError when item does not exist', async () => {
|
||||
mockCabinetRepo.findById.mockResolvedValue(null);
|
||||
|
||||
await expect(service.delete('ci-missing', 'hh1')).rejects.toThrow('Cabinet item not found');
|
||||
await expect(service.delete('ci-missing', 'hh1', 'user-1')).rejects.toThrow('Cabinet item not found');
|
||||
});
|
||||
|
||||
it('throws NotFoundError when softDelete returns null', async () => {
|
||||
mockCabinetRepo.findById.mockResolvedValue({ _id: 'ci-1' });
|
||||
mockCabinetRepo.findById.mockResolvedValue({ _id: 'ci-1', quantity: 5, medicineId: 'med-1', medicineName: 'Test' });
|
||||
mockCabinetRepo.softDelete.mockResolvedValue(null);
|
||||
|
||||
await expect(service.delete('ci-1', 'hh1')).rejects.toThrow('Cabinet item not found');
|
||||
await expect(service.delete('ci-1', 'hh1', 'user-1')).rejects.toThrow('Cabinet item not found');
|
||||
});
|
||||
});
|
||||
|
||||
describe('discard', () => {
|
||||
it('discards item and logs DISCARDED event', async () => {
|
||||
mockCabinetRepo.findById.mockResolvedValue({ _id: 'ci-1', quantity: 20, medicineId: 'med-1', medicineName: 'Test' });
|
||||
mockCabinetRepo.discard.mockResolvedValue({ _id: 'ci-1', quantity: 0, isDeleted: true });
|
||||
|
||||
const result = await service.discard('ci-1', 'hh1', 'user-1', 'expired', 'smelled off');
|
||||
|
||||
expect(result.quantity).toBe(0);
|
||||
expect(result.isDeleted).toBe(true);
|
||||
expect(mockCabinetEventsService.logEvent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
eventType: CabinetEventType.DISCARDED,
|
||||
quantity: -20,
|
||||
quantityBefore: 20,
|
||||
quantityAfter: 0,
|
||||
reason: 'expired',
|
||||
notes: 'smelled off',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('throws BadRequestError when quantity is zero', async () => {
|
||||
mockCabinetRepo.findById.mockResolvedValue({ _id: 'ci-1', quantity: 0, medicineId: 'med-1', medicineName: 'Test' });
|
||||
|
||||
await expect(service.discard('ci-1', 'hh1', 'user-1', 'expired')).rejects.toThrow(
|
||||
'Cannot discard an item with zero quantity',
|
||||
);
|
||||
});
|
||||
|
||||
it('throws NotFoundError when item does not exist', async () => {
|
||||
mockCabinetRepo.findById.mockResolvedValue(null);
|
||||
|
||||
await expect(service.discard('ci-missing', 'hh1', 'user-1', 'expired')).rejects.toThrow(
|
||||
'Cabinet item not found',
|
||||
);
|
||||
});
|
||||
|
||||
it('throws NotFoundError when discard returns null', async () => {
|
||||
mockCabinetRepo.findById.mockResolvedValue({ _id: 'ci-1', quantity: 10, medicineId: 'med-1', medicineName: 'Test' });
|
||||
mockCabinetRepo.discard.mockResolvedValue(null);
|
||||
|
||||
await expect(service.discard('ci-1', 'hh1', 'user-1', 'expired')).rejects.toThrow(
|
||||
'Cabinet item not found',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue