2026-03-28 08:19:48 +09:00
|
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
|
|
import { CabinetService } from './cabinet.service.js';
|
2026-03-28 18:25:49 +09:00
|
|
|
import { CabinetEventType, CabinetEventSourceType } from '@meshitrack/shared';
|
2026-03-28 08:19:48 +09:00
|
|
|
|
|
|
|
|
describe(CabinetService.name, () => {
|
|
|
|
|
const mockCabinetRepo = {
|
|
|
|
|
findByHousehold: vi.fn(),
|
|
|
|
|
findById: vi.fn(),
|
|
|
|
|
getAggregateSummary: vi.fn(),
|
|
|
|
|
create: vi.fn(),
|
|
|
|
|
update: vi.fn(),
|
|
|
|
|
adjustQuantity: vi.fn(),
|
|
|
|
|
findExpiringSoon: vi.fn(),
|
|
|
|
|
softDelete: vi.fn(),
|
|
|
|
|
countByMedicineId: vi.fn(),
|
2026-03-28 18:25:49 +09:00
|
|
|
discard: vi.fn(),
|
|
|
|
|
findActiveByMedicineForFEFO: vi.fn(),
|
2026-03-28 08:19:48 +09:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const mockMedicinesRepo = {
|
|
|
|
|
findById: vi.fn(),
|
|
|
|
|
findByHousehold: vi.fn(),
|
|
|
|
|
findDuplicate: vi.fn(),
|
|
|
|
|
create: vi.fn(),
|
|
|
|
|
update: vi.fn(),
|
|
|
|
|
softDelete: vi.fn(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const mockProductsRepo = {
|
|
|
|
|
findById: vi.fn(),
|
|
|
|
|
findByMedicine: vi.fn(),
|
|
|
|
|
create: vi.fn(),
|
|
|
|
|
update: vi.fn(),
|
|
|
|
|
softDelete: vi.fn(),
|
|
|
|
|
countByMedicineId: vi.fn(),
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-28 18:25:49 +09:00
|
|
|
const mockCabinetEventsService = {
|
|
|
|
|
logEvent: vi.fn(),
|
|
|
|
|
logEvents: vi.fn(),
|
|
|
|
|
listEvents: vi.fn(),
|
|
|
|
|
getEventsByItem: vi.fn(),
|
|
|
|
|
getSpendingSummary: vi.fn(),
|
|
|
|
|
getAvgUnitPrices: vi.fn(),
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-28 08:19:48 +09:00
|
|
|
let service: CabinetService;
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
vi.clearAllMocks();
|
|
|
|
|
service = new CabinetService({
|
|
|
|
|
cabinetRepository: mockCabinetRepo as never,
|
|
|
|
|
medicinesRepository: mockMedicinesRepo as never,
|
|
|
|
|
medicineProductsRepository: mockProductsRepo as never,
|
2026-03-28 18:25:49 +09:00
|
|
|
cabinetEventsService: mockCabinetEventsService as never,
|
2026-03-28 08:19:48 +09:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('list', () => {
|
|
|
|
|
it('delegates to repository', async () => {
|
|
|
|
|
const result = { data: [], pagination: { cursor: null, hasMore: false } };
|
|
|
|
|
mockCabinetRepo.findByHousehold.mockResolvedValue(result);
|
|
|
|
|
|
|
|
|
|
const response = await service.list('hh1', { limit: 20 });
|
|
|
|
|
|
|
|
|
|
expect(response).toEqual(result);
|
|
|
|
|
expect(mockCabinetRepo.findByHousehold).toHaveBeenCalledWith('hh1', { limit: 20 });
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('getById', () => {
|
|
|
|
|
it('returns item when found', async () => {
|
|
|
|
|
const item = { _id: 'ci-1', quantity: 30 };
|
|
|
|
|
mockCabinetRepo.findById.mockResolvedValue(item);
|
|
|
|
|
|
|
|
|
|
const result = await service.getById('ci-1', 'hh1');
|
|
|
|
|
|
|
|
|
|
expect(result).toEqual(item);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('throws NotFoundError when not found', async () => {
|
|
|
|
|
mockCabinetRepo.findById.mockResolvedValue(null);
|
|
|
|
|
|
|
|
|
|
await expect(service.getById('ci-missing', 'hh1')).rejects.toThrow('Cabinet item not found');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('getSummary', () => {
|
|
|
|
|
it('returns aggregated summary with formatted dates', async () => {
|
|
|
|
|
const expiryDate = new Date('2026-06-01T00:00:00.000Z');
|
|
|
|
|
mockCabinetRepo.getAggregateSummary.mockResolvedValue([
|
|
|
|
|
{
|
|
|
|
|
_id: 'med-1',
|
|
|
|
|
medicineName: 'Metformin',
|
|
|
|
|
medicineStrength: 500,
|
|
|
|
|
medicineStrengthUnit: 'mg',
|
|
|
|
|
medicineForm: 'tablet',
|
|
|
|
|
totalQuantity: 60,
|
|
|
|
|
unit: 'tablet',
|
|
|
|
|
earliestExpiry: expiryDate,
|
|
|
|
|
itemCount: 2,
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
const result = await service.getSummary('hh1');
|
|
|
|
|
|
|
|
|
|
expect(result).toHaveLength(1);
|
|
|
|
|
expect(result[0].medicineId).toBe('med-1');
|
|
|
|
|
expect(result[0].earliestExpiry).toBe('2026-06-01T00:00:00.000Z');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('handles null expiry dates', async () => {
|
|
|
|
|
mockCabinetRepo.getAggregateSummary.mockResolvedValue([
|
|
|
|
|
{
|
|
|
|
|
_id: 'med-1',
|
|
|
|
|
medicineName: 'Test',
|
|
|
|
|
medicineStrength: 10,
|
|
|
|
|
medicineStrengthUnit: 'mg',
|
|
|
|
|
medicineForm: 'tablet',
|
|
|
|
|
totalQuantity: 30,
|
|
|
|
|
unit: 'tablet',
|
|
|
|
|
earliestExpiry: null,
|
|
|
|
|
itemCount: 1,
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
const result = await service.getSummary('hh1');
|
|
|
|
|
|
|
|
|
|
expect(result[0].earliestExpiry).toBeNull();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('addItem', () => {
|
|
|
|
|
const createInput = {
|
|
|
|
|
medicineId: 'med-1',
|
|
|
|
|
quantity: 30,
|
|
|
|
|
unit: 'tablet' as const,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
it('creates item with denormalized medicine fields', async () => {
|
|
|
|
|
mockMedicinesRepo.findById.mockResolvedValue({
|
|
|
|
|
_id: 'med-1',
|
|
|
|
|
name: 'Metformin',
|
|
|
|
|
strength: 500,
|
|
|
|
|
strengthUnit: 'mg',
|
|
|
|
|
form: 'tablet',
|
|
|
|
|
});
|
|
|
|
|
const created = { _id: 'ci-1', ...createInput, medicineName: 'Metformin' };
|
|
|
|
|
mockCabinetRepo.create.mockResolvedValue(created);
|
|
|
|
|
|
|
|
|
|
const result = await service.addItem(createInput, 'hh1', 'user-1');
|
|
|
|
|
|
|
|
|
|
expect(result).toEqual(created);
|
|
|
|
|
expect(mockCabinetRepo.create).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
medicineName: 'Metformin',
|
|
|
|
|
medicineStrength: 500,
|
|
|
|
|
medicineStrengthUnit: 'mg',
|
|
|
|
|
medicineForm: 'tablet',
|
|
|
|
|
}),
|
|
|
|
|
'hh1',
|
|
|
|
|
'user-1',
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-28 18:25:49 +09:00
|
|
|
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,
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-28 08:19:48 +09:00
|
|
|
it('throws NotFoundError when medicine not found', async () => {
|
|
|
|
|
mockMedicinesRepo.findById.mockResolvedValue(null);
|
|
|
|
|
|
|
|
|
|
await expect(service.addItem(createInput, 'hh1', 'user-1')).rejects.toThrow(
|
|
|
|
|
'Medicine not found',
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('denormalizes product brand when medicineProductId given', async () => {
|
|
|
|
|
mockMedicinesRepo.findById.mockResolvedValue({
|
|
|
|
|
_id: 'med-1',
|
|
|
|
|
name: 'Metformin',
|
|
|
|
|
strength: 500,
|
|
|
|
|
strengthUnit: 'mg',
|
|
|
|
|
form: 'tablet',
|
|
|
|
|
});
|
|
|
|
|
mockProductsRepo.findById.mockResolvedValue({
|
|
|
|
|
_id: 'prod-1',
|
|
|
|
|
brand: 'Glucophage',
|
|
|
|
|
});
|
|
|
|
|
mockCabinetRepo.create.mockResolvedValue({ _id: 'ci-1' });
|
|
|
|
|
|
|
|
|
|
await service.addItem({ ...createInput, medicineProductId: 'prod-1' }, 'hh1', 'user-1');
|
|
|
|
|
|
|
|
|
|
expect(mockCabinetRepo.create).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({ medicineProductBrand: 'Glucophage' }),
|
|
|
|
|
'hh1',
|
|
|
|
|
'user-1',
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('throws NotFoundError when product not found', async () => {
|
|
|
|
|
mockMedicinesRepo.findById.mockResolvedValue({
|
|
|
|
|
_id: 'med-1',
|
|
|
|
|
name: 'Metformin',
|
|
|
|
|
strength: 500,
|
|
|
|
|
strengthUnit: 'mg',
|
|
|
|
|
form: 'tablet',
|
|
|
|
|
});
|
|
|
|
|
mockProductsRepo.findById.mockResolvedValue(null);
|
|
|
|
|
|
|
|
|
|
await expect(
|
|
|
|
|
service.addItem({ ...createInput, medicineProductId: 'prod-missing' }, 'hh1', 'user-1'),
|
|
|
|
|
).rejects.toThrow('Medicine product not found');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('update', () => {
|
|
|
|
|
it('updates and returns item', async () => {
|
2026-04-26 18:44:59 +09:00
|
|
|
mockCabinetRepo.findById.mockResolvedValue({
|
|
|
|
|
_id: 'ci-1',
|
|
|
|
|
quantity: 30,
|
|
|
|
|
medicineId: 'med-1',
|
|
|
|
|
medicineName: 'Test',
|
|
|
|
|
});
|
2026-03-28 08:19:48 +09:00
|
|
|
const updated = { _id: 'ci-1', quantity: 25 };
|
|
|
|
|
mockCabinetRepo.update.mockResolvedValue(updated);
|
|
|
|
|
|
2026-03-28 18:25:49 +09:00
|
|
|
const result = await service.update('ci-1', 'hh1', { quantity: 25 }, 'user-1');
|
2026-03-28 08:19:48 +09:00
|
|
|
|
|
|
|
|
expect(result).toEqual(updated);
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-28 18:25:49 +09:00
|
|
|
it('logs ADJUSTED event when quantity changes', async () => {
|
2026-04-26 18:44:59 +09:00
|
|
|
mockCabinetRepo.findById.mockResolvedValue({
|
|
|
|
|
_id: 'ci-1',
|
|
|
|
|
quantity: 30,
|
|
|
|
|
medicineId: 'med-1',
|
|
|
|
|
medicineName: 'Test',
|
|
|
|
|
});
|
2026-03-28 18:25:49 +09:00
|
|
|
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 () => {
|
2026-04-26 18:44:59 +09:00
|
|
|
mockCabinetRepo.findById.mockResolvedValue({
|
|
|
|
|
_id: 'ci-1',
|
|
|
|
|
quantity: 30,
|
|
|
|
|
medicineId: 'med-1',
|
|
|
|
|
medicineName: 'Test',
|
|
|
|
|
});
|
2026-03-28 18:25:49 +09:00
|
|
|
mockCabinetRepo.update.mockResolvedValue({ _id: 'ci-1', quantity: 30 });
|
|
|
|
|
|
|
|
|
|
await service.update('ci-1', 'hh1', { notes: 'updated' }, 'user-1');
|
|
|
|
|
|
|
|
|
|
expect(mockCabinetEventsService.logEvent).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-28 08:19:48 +09:00
|
|
|
it('throws NotFoundError when item does not exist', async () => {
|
|
|
|
|
mockCabinetRepo.findById.mockResolvedValue(null);
|
|
|
|
|
|
2026-03-28 18:25:49 +09:00
|
|
|
await expect(service.update('ci-missing', 'hh1', { quantity: 25 }, 'user-1')).rejects.toThrow(
|
2026-03-28 08:19:48 +09:00
|
|
|
'Cabinet item not found',
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('throws NotFoundError when update returns null', async () => {
|
2026-04-26 18:44:59 +09:00
|
|
|
mockCabinetRepo.findById.mockResolvedValue({
|
|
|
|
|
_id: 'ci-1',
|
|
|
|
|
quantity: 30,
|
|
|
|
|
medicineId: 'med-1',
|
|
|
|
|
medicineName: 'Test',
|
|
|
|
|
});
|
2026-03-28 08:19:48 +09:00
|
|
|
mockCabinetRepo.update.mockResolvedValue(null);
|
|
|
|
|
|
2026-03-28 18:25:49 +09:00
|
|
|
await expect(service.update('ci-1', 'hh1', { quantity: 25 }, 'user-1')).rejects.toThrow(
|
2026-03-28 08:19:48 +09:00
|
|
|
'Cabinet item not found',
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('adjustQuantity', () => {
|
|
|
|
|
it('adjusts quantity and returns item', async () => {
|
2026-04-26 18:44:59 +09:00
|
|
|
mockCabinetRepo.findById.mockResolvedValue({
|
|
|
|
|
_id: 'ci-1',
|
|
|
|
|
quantity: 30,
|
|
|
|
|
medicineId: 'med-1',
|
|
|
|
|
medicineName: 'Test',
|
|
|
|
|
});
|
2026-03-28 08:19:48 +09:00
|
|
|
const updated = { _id: 'ci-1', quantity: 27 };
|
|
|
|
|
mockCabinetRepo.adjustQuantity.mockResolvedValue(updated);
|
|
|
|
|
|
2026-03-28 18:25:49 +09:00
|
|
|
const result = await service.adjustQuantity('ci-1', 'hh1', -3, 'user-1');
|
2026-03-28 08:19:48 +09:00
|
|
|
|
|
|
|
|
expect(result).toEqual(updated);
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-28 18:25:49 +09:00
|
|
|
it('logs ADJUSTED event', async () => {
|
2026-04-26 18:44:59 +09:00
|
|
|
mockCabinetRepo.findById.mockResolvedValue({
|
|
|
|
|
_id: 'ci-1',
|
|
|
|
|
quantity: 30,
|
|
|
|
|
medicineId: 'med-1',
|
|
|
|
|
medicineName: 'Test',
|
|
|
|
|
});
|
2026-03-28 18:25:49 +09:00
|
|
|
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',
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-28 08:19:48 +09:00
|
|
|
it('throws BadRequestError when delta is 0', async () => {
|
2026-03-28 18:25:49 +09:00
|
|
|
await expect(service.adjustQuantity('ci-1', 'hh1', 0, 'user-1')).rejects.toThrow(
|
2026-03-28 08:19:48 +09:00
|
|
|
'Delta must be non-zero',
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('throws NotFoundError when item does not exist', async () => {
|
|
|
|
|
mockCabinetRepo.findById.mockResolvedValue(null);
|
|
|
|
|
|
2026-03-28 18:25:49 +09:00
|
|
|
await expect(service.adjustQuantity('ci-missing', 'hh1', 5, 'user-1')).rejects.toThrow(
|
2026-03-28 08:19:48 +09:00
|
|
|
'Cabinet item not found',
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('throws NotFoundError when adjust returns null', async () => {
|
2026-04-26 18:44:59 +09:00
|
|
|
mockCabinetRepo.findById.mockResolvedValue({
|
|
|
|
|
_id: 'ci-1',
|
|
|
|
|
quantity: 30,
|
|
|
|
|
medicineId: 'med-1',
|
|
|
|
|
medicineName: 'Test',
|
|
|
|
|
});
|
2026-03-28 08:19:48 +09:00
|
|
|
mockCabinetRepo.adjustQuantity.mockResolvedValue(null);
|
|
|
|
|
|
2026-03-28 18:25:49 +09:00
|
|
|
await expect(service.adjustQuantity('ci-1', 'hh1', 5, 'user-1')).rejects.toThrow(
|
2026-03-28 08:19:48 +09:00
|
|
|
'Cabinet item not found',
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('getExpiringSoon', () => {
|
|
|
|
|
it('delegates to repository', async () => {
|
|
|
|
|
const items = [{ _id: 'ci-1' }];
|
|
|
|
|
mockCabinetRepo.findExpiringSoon.mockResolvedValue(items);
|
|
|
|
|
|
|
|
|
|
const result = await service.getExpiringSoon('hh1', 30);
|
|
|
|
|
|
|
|
|
|
expect(result).toEqual(items);
|
|
|
|
|
expect(mockCabinetRepo.findExpiringSoon).toHaveBeenCalledWith('hh1', 30);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('delete', () => {
|
2026-03-28 18:25:49 +09:00
|
|
|
it('soft deletes item and logs DELETED event', async () => {
|
2026-04-26 18:44:59 +09:00
|
|
|
mockCabinetRepo.findById.mockResolvedValue({
|
|
|
|
|
_id: 'ci-1',
|
|
|
|
|
quantity: 10,
|
|
|
|
|
medicineId: 'med-1',
|
|
|
|
|
medicineName: 'Test',
|
|
|
|
|
});
|
2026-03-28 08:19:48 +09:00
|
|
|
mockCabinetRepo.softDelete.mockResolvedValue({ _id: 'ci-1', isDeleted: true });
|
|
|
|
|
|
2026-03-28 18:25:49 +09:00
|
|
|
const result = await service.delete('ci-1', 'hh1', 'user-1');
|
2026-03-28 08:19:48 +09:00
|
|
|
|
|
|
|
|
expect(result.isDeleted).toBe(true);
|
2026-03-28 18:25:49 +09:00
|
|
|
expect(mockCabinetEventsService.logEvent).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
eventType: CabinetEventType.DELETED,
|
|
|
|
|
quantity: -10,
|
|
|
|
|
quantityBefore: 10,
|
|
|
|
|
quantityAfter: 0,
|
|
|
|
|
}),
|
|
|
|
|
);
|
2026-03-28 08:19:48 +09:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('throws NotFoundError when item does not exist', async () => {
|
|
|
|
|
mockCabinetRepo.findById.mockResolvedValue(null);
|
|
|
|
|
|
2026-04-26 18:44:59 +09:00
|
|
|
await expect(service.delete('ci-missing', 'hh1', 'user-1')).rejects.toThrow(
|
|
|
|
|
'Cabinet item not found',
|
|
|
|
|
);
|
2026-03-28 08:19:48 +09:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('throws NotFoundError when softDelete returns null', async () => {
|
2026-04-26 18:44:59 +09:00
|
|
|
mockCabinetRepo.findById.mockResolvedValue({
|
|
|
|
|
_id: 'ci-1',
|
|
|
|
|
quantity: 5,
|
|
|
|
|
medicineId: 'med-1',
|
|
|
|
|
medicineName: 'Test',
|
|
|
|
|
});
|
2026-03-28 08:19:48 +09:00
|
|
|
mockCabinetRepo.softDelete.mockResolvedValue(null);
|
|
|
|
|
|
2026-04-26 18:44:59 +09:00
|
|
|
await expect(service.delete('ci-1', 'hh1', 'user-1')).rejects.toThrow(
|
|
|
|
|
'Cabinet item not found',
|
|
|
|
|
);
|
2026-03-28 18:25:49 +09:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('discard', () => {
|
|
|
|
|
it('discards item and logs DISCARDED event', async () => {
|
2026-04-26 18:44:59 +09:00
|
|
|
mockCabinetRepo.findById.mockResolvedValue({
|
|
|
|
|
_id: 'ci-1',
|
|
|
|
|
quantity: 20,
|
|
|
|
|
medicineId: 'med-1',
|
|
|
|
|
medicineName: 'Test',
|
|
|
|
|
});
|
2026-03-28 18:25:49 +09:00
|
|
|
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 () => {
|
2026-04-26 18:44:59 +09:00
|
|
|
mockCabinetRepo.findById.mockResolvedValue({
|
|
|
|
|
_id: 'ci-1',
|
|
|
|
|
quantity: 0,
|
|
|
|
|
medicineId: 'med-1',
|
|
|
|
|
medicineName: 'Test',
|
|
|
|
|
});
|
2026-03-28 18:25:49 +09:00
|
|
|
|
|
|
|
|
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 () => {
|
2026-04-26 18:44:59 +09:00
|
|
|
mockCabinetRepo.findById.mockResolvedValue({
|
|
|
|
|
_id: 'ci-1',
|
|
|
|
|
quantity: 10,
|
|
|
|
|
medicineId: 'med-1',
|
|
|
|
|
medicineName: 'Test',
|
|
|
|
|
});
|
2026-03-28 18:25:49 +09:00
|
|
|
mockCabinetRepo.discard.mockResolvedValue(null);
|
|
|
|
|
|
|
|
|
|
await expect(service.discard('ci-1', 'hh1', 'user-1', 'expired')).rejects.toThrow(
|
|
|
|
|
'Cabinet item not found',
|
|
|
|
|
);
|
2026-03-28 08:19:48 +09:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|