import { describe, it, expect, vi, beforeEach } from 'vitest'; import { MedicinesService } from '../../../src/modules/medicines/medicines.service.js'; import { NotFoundError, ConflictError } from '../../../src/common/errors.js'; import { MedicineForm, StrengthUnit, MedicineCategory } from '@meshitrack/shared'; describe(MedicinesService.name, () => { const mockRepo = { findByHousehold: vi.fn(), findById: vi.fn(), findDuplicate: vi.fn(), create: vi.fn(), update: vi.fn(), softDelete: vi.fn(), }; const mockProductsRepo = { countByMedicineId: vi.fn(), }; let service: MedicinesService; beforeEach(() => { vi.clearAllMocks(); service = new MedicinesService({ medicinesRepository: mockRepo as never, medicineProductsRepository: mockProductsRepo as never, }); }); describe('list', () => { it('delegates to repository', async () => { const expected = { data: [], pagination: { cursor: null, hasMore: false } }; mockRepo.findByHousehold.mockResolvedValue(expected); const result = await service.list('hh1', { limit: 20 }); expect(mockRepo.findByHousehold).toHaveBeenCalledWith('hh1', { limit: 20 }); expect(result).toEqual(expected); }); }); describe('getById', () => { it('returns medicine when found', async () => { const medicine = { _id: 'med-1', name: 'Aspirin' }; mockRepo.findById.mockResolvedValue(medicine); const result = await service.getById('med-1', 'hh1'); expect(result).toEqual(medicine); }); it('throws NotFoundError when not found', async () => { mockRepo.findById.mockResolvedValue(null); await expect(service.getById('missing', 'hh1')).rejects.toThrow(NotFoundError); }); }); describe('create', () => { const data = { name: 'Aspirin', form: MedicineForm.TABLET, strength: 500, strengthUnit: StrengthUnit.MG, category: MedicineCategory.OTC, tags: [], }; it('creates when no duplicate exists', async () => { mockRepo.findDuplicate.mockResolvedValue(null); mockRepo.create.mockResolvedValue({ _id: 'med-1', ...data }); const result = await service.create(data, 'hh1', 'kc-1'); expect(mockRepo.findDuplicate).toHaveBeenCalledWith( 'hh1', 'Aspirin', 500, StrengthUnit.MG, MedicineForm.TABLET, ); expect(result._id).toBe('med-1'); }); it('throws ConflictError when duplicate exists', async () => { mockRepo.findDuplicate.mockResolvedValue({ _id: 'existing' }); await expect(service.create(data, 'hh1', 'kc-1')).rejects.toThrow(ConflictError); expect(mockRepo.create).not.toHaveBeenCalled(); }); }); describe('update', () => { it('updates a medicine', async () => { mockRepo.findById.mockResolvedValue({ _id: 'med-1', name: 'Aspirin', strength: 500, strengthUnit: StrengthUnit.MG, form: MedicineForm.TABLET, }); mockRepo.findDuplicate.mockResolvedValue(null); mockRepo.update.mockResolvedValue({ _id: 'med-1', name: 'Updated' }); const result = await service.update('med-1', 'hh1', { name: 'Updated' }); expect(result.name).toBe('Updated'); }); it('throws NotFoundError when medicine does not exist', async () => { mockRepo.findById.mockResolvedValue(null); await expect(service.update('missing', 'hh1', { name: 'X' })).rejects.toThrow(NotFoundError); }); it('throws ConflictError when update would create duplicate', async () => { mockRepo.findById.mockResolvedValue({ _id: 'med-1', name: 'Aspirin', strength: 500, strengthUnit: StrengthUnit.MG, form: MedicineForm.TABLET, }); mockRepo.findDuplicate.mockResolvedValue({ _id: 'med-2' }); await expect(service.update('med-1', 'hh1', { name: 'Ibuprofen' })).rejects.toThrow( ConflictError, ); }); it('throws NotFoundError when repo update returns null', async () => { mockRepo.findById.mockResolvedValue({ _id: 'med-1', name: 'Aspirin' }); mockRepo.update.mockResolvedValue(null); await expect(service.update('med-1', 'hh1', { notes: 'Updated' })).rejects.toThrow( NotFoundError, ); }); it('uses current name when name not provided in update', async () => { mockRepo.findById.mockResolvedValue({ _id: 'med-1', name: 'Aspirin', strength: 500, strengthUnit: StrengthUnit.MG, form: MedicineForm.TABLET, }); mockRepo.findDuplicate.mockResolvedValue(null); mockRepo.update.mockResolvedValue({ _id: 'med-1', name: 'Aspirin', strength: 250 }); const result = await service.update('med-1', 'hh1', { strength: 250 }); expect(result).toBeTruthy(); expect(mockRepo.findDuplicate).toHaveBeenCalledWith( 'hh1', 'Aspirin', 250, StrengthUnit.MG, MedicineForm.TABLET, 'med-1', ); }); it('skips dedup check when no identity fields change', async () => { mockRepo.findById.mockResolvedValue({ _id: 'med-1', name: 'Aspirin' }); mockRepo.update.mockResolvedValue({ _id: 'med-1', notes: 'Updated notes' }); await service.update('med-1', 'hh1', { notes: 'Updated notes' }); expect(mockRepo.findDuplicate).not.toHaveBeenCalled(); }); }); describe('delete', () => { it('soft deletes a medicine', async () => { mockRepo.findById.mockResolvedValue({ _id: 'med-1' }); mockProductsRepo.countByMedicineId.mockResolvedValue(0); mockRepo.softDelete.mockResolvedValue({ _id: 'med-1', isDeleted: true }); await service.delete('med-1', 'hh1'); expect(mockProductsRepo.countByMedicineId).toHaveBeenCalledWith('med-1'); expect(mockRepo.softDelete).toHaveBeenCalledWith('med-1', 'hh1'); }); it('throws ConflictError when linked products exist', async () => { mockRepo.findById.mockResolvedValue({ _id: 'med-1' }); mockProductsRepo.countByMedicineId.mockResolvedValue(3); await expect(service.delete('med-1', 'hh1')).rejects.toThrow(ConflictError); expect(mockRepo.softDelete).not.toHaveBeenCalled(); }); it('throws NotFoundError when not found', async () => { mockRepo.findById.mockResolvedValue(null); await expect(service.delete('missing', 'hh1')).rejects.toThrow(NotFoundError); }); it('throws NotFoundError when softDelete returns null', async () => { mockRepo.findById.mockResolvedValue({ _id: 'med-1' }); mockProductsRepo.countByMedicineId.mockResolvedValue(0); mockRepo.softDelete.mockResolvedValue(null); await expect(service.delete('med-1', 'hh1')).rejects.toThrow(NotFoundError); }); }); });