import { describe, it, expect, vi, beforeEach } from 'vitest'; import { RefillsService } from '../../../src/modules/refills/refills.service.js'; describe(RefillsService.name, () => { const mockRepo = { create: vi.fn(), findByHousehold: vi.fn(), findById: vi.fn(), update: vi.fn(), updateItem: vi.fn(), markItemsAddedToCabinet: vi.fn(), }; const mockRegimensService = { calculateBurnRates: vi.fn(), }; const mockCabinetRepo = { getAggregateSummary: vi.fn(), }; const mockCabinetService = { addItem: vi.fn(), }; const mockPricesRepo = { getLatestForMedicine: vi.fn(), compareStores: vi.fn(), }; const mockPurchasesRepo = { getPendingMedicineStock: vi.fn(), }; let service: RefillsService; beforeEach(() => { vi.clearAllMocks(); mockPurchasesRepo.getPendingMedicineStock.mockResolvedValue([]); service = new RefillsService({ refillsRepository: mockRepo as never, regimensService: mockRegimensService as never, cabinetRepository: mockCabinetRepo as never, cabinetService: mockCabinetService as never, medicinePricesRepository: mockPricesRepo as never, purchasesRepository: mockPurchasesRepo as never, }); }); describe('getAlerts', () => { it('returns empty array when no medicines are running low', async () => { mockRegimensService.calculateBurnRates.mockResolvedValue([ { medicineId: 'med-1', medicineName: 'Aspirin', dailyConsumption: 1, totalInCabinet: 100, daysUntilEmpty: 100, }, ]); const result = await service.getAlerts('hh1', 'user-1', 7); expect(result).toEqual([]); }); it('returns alerts for medicines below threshold', async () => { mockRegimensService.calculateBurnRates.mockResolvedValue([ { medicineId: 'med-1', medicineName: 'Aspirin', dailyConsumption: 2, totalInCabinet: 10, daysUntilEmpty: 5, }, ]); mockCabinetRepo.getAggregateSummary.mockResolvedValue([ { _id: 'med-1', medicineStrength: 500, medicineStrengthUnit: 'mg' }, ]); mockPricesRepo.getLatestForMedicine.mockResolvedValue(null); mockPricesRepo.compareStores.mockResolvedValue([]); const result = await service.getAlerts('hh1', 'user-1', 7); expect(result).toHaveLength(1); expect(result[0].medicineId).toBe('med-1'); expect(result[0].daysUntilEmpty).toBe(5); expect(result[0].suggestedQuantity).toBe(60); // ceil(2 * 30) }); it('attaches lastKnownPrice when available', async () => { mockRegimensService.calculateBurnRates.mockResolvedValue([ { medicineId: 'med-1', medicineName: 'Aspirin', dailyConsumption: 2, totalInCabinet: 5, daysUntilEmpty: 2, }, ]); mockCabinetRepo.getAggregateSummary.mockResolvedValue([]); mockPricesRepo.getLatestForMedicine.mockResolvedValue({ price: 10, pricePerUnit: 0.1, storeName: 'Walgreens', storeId: 'st-1', date: new Date('2026-01-01T00:00:00.000Z'), }); mockPricesRepo.compareStores.mockResolvedValue([]); const result = await service.getAlerts('hh1', 'user-1', 7); expect(result[0].lastKnownPrice).toBeDefined(); expect(result[0].lastKnownPrice?.storeName).toBe('Walgreens'); }); it('attaches cheapestOption from compareStores', async () => { mockRegimensService.calculateBurnRates.mockResolvedValue([ { medicineId: 'med-1', medicineName: 'Aspirin', dailyConsumption: 2, totalInCabinet: 5, daysUntilEmpty: 2, }, ]); mockCabinetRepo.getAggregateSummary.mockResolvedValue([]); mockPricesRepo.getLatestForMedicine.mockResolvedValue(null); mockPricesRepo.compareStores.mockResolvedValue([ { storeId: 'st-1', storeName: 'CVS', latestPrice: 8, latestPricePerUnit: 0.08, currency: 'USD', date: new Date(), }, ]); const result = await service.getAlerts('hh1', 'user-1', 7); expect(result[0].cheapestOption).toBeDefined(); expect(result[0].cheapestOption?.storeName).toBe('CVS'); }); it('includes pendingOrderStock and daysUntilEmptyWithOrders from ordered purchases', async () => { mockRegimensService.calculateBurnRates.mockResolvedValue([ { medicineId: 'med-1', medicineName: 'Aspirin', dailyConsumption: 2, totalInCabinet: 4, daysUntilEmpty: 2, }, ]); mockCabinetRepo.getAggregateSummary.mockResolvedValue([]); mockPricesRepo.getLatestForMedicine.mockResolvedValue(null); mockPricesRepo.compareStores.mockResolvedValue([]); mockPurchasesRepo.getPendingMedicineStock.mockResolvedValue([ { medicineId: 'med-1', totalUnits: 60 }, ]); const result = await service.getAlerts('hh1', 'user-1', 7); expect(result[0].pendingOrderStock).toBe(60); expect(result[0].daysUntilEmptyWithOrders).toBe(32); // (4 + 60) / 2 }); it('excludes medicines with null daysUntilEmpty', async () => { mockRegimensService.calculateBurnRates.mockResolvedValue([ { medicineId: 'med-1', medicineName: 'Aspirin', dailyConsumption: 2, totalInCabinet: 0, daysUntilEmpty: null, }, ]); const result = await service.getAlerts('hh1', 'user-1', 7); expect(result).toHaveLength(0); }); }); describe('createList', () => { it('creates list with provided items', async () => { const list = { _id: 'rl-1', name: 'My List', items: [], status: 'active' }; mockRepo.create.mockResolvedValue(list); const result = await service.createList( { name: 'My List', fromAlerts: false, thresholdDays: 7, items: [ { medicineId: 'med-1', medicineName: 'Aspirin', quantity: 30, unit: 'tablet' as never }, ], }, 'hh1', 'user-1', ); expect(result).toEqual(list); expect(mockRepo.create).toHaveBeenCalledWith( expect.objectContaining({ name: 'My List', householdId: 'hh1' }), ); }); it('creates list with no items when neither fromAlerts nor items provided', async () => { mockRepo.create.mockResolvedValue({ _id: 'rl-1', items: [] }); await service.createList( { name: 'Empty List', fromAlerts: false, thresholdDays: 7 }, 'hh1', 'user-1', ); expect(mockRepo.create).toHaveBeenCalledWith( expect.objectContaining({ items: [], totalEstimatedCost: undefined }), ); }); it('computes totalEstimatedCost from items with estimatedPrice', async () => { mockRepo.create.mockResolvedValue({ _id: 'rl-1', items: [] }); await service.createList( { name: 'Priced List', fromAlerts: false, thresholdDays: 7, items: [ { medicineId: 'med-1', medicineName: 'Aspirin', quantity: 30, unit: 'tablet' as never, estimatedPrice: 10, }, { medicineId: 'med-2', medicineName: 'Ibuprofen', quantity: 20, unit: 'tablet' as never, estimatedPrice: 8, }, ], }, 'hh1', 'user-1', ); expect(mockRepo.create).toHaveBeenCalledWith( expect.objectContaining({ totalEstimatedCost: 18 }), ); }); it('creates list from alerts when fromAlerts is true', async () => { mockRegimensService.calculateBurnRates.mockResolvedValue([ { medicineId: 'med-1', medicineName: 'Aspirin', dailyConsumption: 2, totalInCabinet: 5, daysUntilEmpty: 2, }, ]); mockCabinetRepo.getAggregateSummary.mockResolvedValue([]); mockPricesRepo.getLatestForMedicine.mockResolvedValue(null); mockPricesRepo.compareStores.mockResolvedValue([]); mockRepo.create.mockResolvedValue({ _id: 'rl-1', items: [] }); await service.createList( { name: 'Auto List', fromAlerts: true, thresholdDays: 7 }, 'hh1', 'user-1', ); expect(mockRepo.create).toHaveBeenCalledWith( expect.objectContaining({ items: expect.arrayContaining([expect.objectContaining({ medicineId: 'med-1' })]), }), ); }); }); describe('list', () => { it('delegates to repository', async () => { const result = { data: [], pagination: { cursor: null, hasMore: false } }; mockRepo.findByHousehold.mockResolvedValue(result); const response = await service.list('hh1', { limit: 20 }); expect(response).toEqual(result); expect(mockRepo.findByHousehold).toHaveBeenCalledWith('hh1', { limit: 20 }); }); }); describe('getById', () => { it('returns list when found', async () => { const list = { _id: 'rl-1', name: 'My List' }; mockRepo.findById.mockResolvedValue(list); expect(await service.getById('rl-1', 'hh1')).toEqual(list); }); it('throws NotFoundError when not found', async () => { mockRepo.findById.mockResolvedValue(null); await expect(service.getById('missing', 'hh1')).rejects.toThrow('Refill list not found'); }); }); describe('updateList', () => { it('updates and returns list', async () => { mockRepo.findById.mockResolvedValue({ _id: 'rl-1' }); const updated = { _id: 'rl-1', name: 'Updated' }; mockRepo.update.mockResolvedValue(updated); const result = await service.updateList('rl-1', 'hh1', { name: 'Updated' }); expect(result).toEqual(updated); }); it('throws NotFoundError when list not found', async () => { mockRepo.findById.mockResolvedValue(null); await expect(service.updateList('missing', 'hh1', {})).rejects.toThrow( 'Refill list not found', ); }); it('throws NotFoundError when update returns null', async () => { mockRepo.findById.mockResolvedValue({ _id: 'rl-1' }); mockRepo.update.mockResolvedValue(null); await expect(service.updateList('rl-1', 'hh1', {})).rejects.toThrow('Refill list not found'); }); }); describe('updateItem', () => { it('updates item and returns list', async () => { mockRepo.findById.mockResolvedValue({ _id: 'rl-1' }); const updated = { _id: 'rl-1', items: [{ _id: 'item-1', checked: true }] }; mockRepo.updateItem.mockResolvedValue(updated); const result = await service.updateItem('rl-1', 'hh1', 'item-1', { checked: true }); expect(result).toEqual(updated); }); it('sets checkedAt when checked is true', async () => { mockRepo.findById.mockResolvedValue({ _id: 'rl-1' }); mockRepo.updateItem.mockResolvedValue({ _id: 'rl-1', items: [] }); await service.updateItem('rl-1', 'hh1', 'item-1', { checked: true }); expect(mockRepo.updateItem).toHaveBeenCalledWith( 'rl-1', 'hh1', 'item-1', expect.objectContaining({ checkedAt: expect.any(Date) }), ); }); it('throws NotFoundError when list not found', async () => { mockRepo.findById.mockResolvedValue(null); await expect(service.updateItem('missing', 'hh1', 'item-1', {})).rejects.toThrow( 'Refill list not found', ); }); it('throws NotFoundError when item not found', async () => { mockRepo.findById.mockResolvedValue({ _id: 'rl-1' }); mockRepo.updateItem.mockResolvedValue(null); await expect(service.updateItem('rl-1', 'hh1', 'bad-item', {})).rejects.toThrow( 'Refill list or item not found', ); }); }); describe('addToCabinet', () => { it('adds checked items to cabinet', async () => { mockRepo.findById.mockResolvedValue({ _id: 'rl-1', items: [ { _id: { toString: () => 'item-1' }, medicineId: 'med-1', medicineName: 'Aspirin', quantity: 30, unit: 'tablet', checked: true, addedToCabinet: false, }, ], }); mockCabinetService.addItem.mockResolvedValue({ _id: 'ci-1' }); mockRepo.markItemsAddedToCabinet.mockResolvedValue({ _id: 'rl-1' }); const result = await service.addToCabinet('rl-1', 'hh1', 'user-1'); expect(result.addedCount).toBe(1); expect(mockCabinetService.addItem).toHaveBeenCalledTimes(1); }); it('computes unitPrice when actualPrice is set', async () => { mockRepo.findById.mockResolvedValue({ _id: 'rl-1', items: [ { _id: { toString: () => 'item-1' }, medicineId: 'med-1', medicineName: 'Aspirin', quantity: 30, unit: 'tablet', actualPrice: 9, checked: true, addedToCabinet: false, }, ], }); mockCabinetService.addItem.mockResolvedValue({ _id: 'ci-1' }); mockRepo.markItemsAddedToCabinet.mockResolvedValue({ _id: 'rl-1' }); await service.addToCabinet('rl-1', 'hh1', 'user-1'); expect(mockCabinetService.addItem).toHaveBeenCalledWith( expect.objectContaining({ unitPrice: 0.3, totalPrice: 9 }), 'hh1', 'user-1', ); }); it('returns zero count when no checked items', async () => { mockRepo.findById.mockResolvedValue({ _id: 'rl-1', items: [ { _id: 'item-1', medicineId: 'med-1', medicineName: 'Aspirin', quantity: 30, unit: 'tablet', checked: false, addedToCabinet: false, }, ], }); const result = await service.addToCabinet('rl-1', 'hh1', 'user-1'); expect(result.addedCount).toBe(0); expect(mockCabinetService.addItem).not.toHaveBeenCalled(); }); it('skips already-added items', async () => { mockRepo.findById.mockResolvedValue({ _id: 'rl-1', items: [ { _id: 'item-1', medicineId: 'med-1', medicineName: 'Aspirin', quantity: 30, unit: 'tablet', checked: true, addedToCabinet: true, }, ], }); const result = await service.addToCabinet('rl-1', 'hh1', 'user-1'); expect(result.addedCount).toBe(0); expect(mockCabinetService.addItem).not.toHaveBeenCalled(); }); }); describe('getStoreComparison', () => { it('returns store comparisons for list items', async () => { mockRepo.findById.mockResolvedValue({ _id: 'rl-1', items: [{ medicineId: 'med-1' }, { medicineId: 'med-2' }], }); mockPricesRepo.compareStores .mockResolvedValueOnce([{ storeId: 'st-1', storeName: 'CVS' }]) .mockResolvedValueOnce([]); const result = await service.getStoreComparison('rl-1', 'hh1'); expect(result).toHaveLength(1); expect(result[0].medicineId).toBe('med-1'); }); it('deduplicates medicine ids', async () => { mockRepo.findById.mockResolvedValue({ _id: 'rl-1', items: [{ medicineId: 'med-1' }, { medicineId: 'med-1' }], }); mockPricesRepo.compareStores.mockResolvedValue([{ storeId: 'st-1', storeName: 'CVS' }]); const result = await service.getStoreComparison('rl-1', 'hh1'); expect(mockPricesRepo.compareStores).toHaveBeenCalledTimes(1); expect(result).toHaveLength(1); }); }); });