import { describe, it, expect, vi, beforeEach } from 'vitest'; import { PurchasesService } from './purchases.service.js'; describe(PurchasesService.name, () => { const mockPurchasesRepo = { create: vi.fn(), findByHousehold: vi.fn(), findById: vi.fn(), update: vi.fn(), receiveAll: vi.fn(), softDelete: vi.fn(), getPendingMedicineStock: vi.fn(), }; const mockCabinetService = { addItem: vi.fn(), }; const mockStoresRepo = { findById: vi.fn(), }; const mockProductsRepo = { findById: vi.fn(), }; const mockPricesRepo = { create: vi.fn(), }; let service: PurchasesService; beforeEach(() => { vi.clearAllMocks(); service = new PurchasesService({ purchasesRepository: mockPurchasesRepo as never, cabinetService: mockCabinetService as never, storesRepository: mockStoresRepo as never, medicineProductsRepository: mockProductsRepo as never, medicinePricesRepository: mockPricesRepo as never, }); }); const fakeStore = { _id: 'st-1', name: 'CVS' }; const fakeProduct = { _id: 'mp-1', medicineId: 'med-1', medicineName: 'Ibuprofen', brand: 'Advil' }; describe('create', () => { const validInput = { storeId: 'st-1', status: 'in_cabinet' as const, items: [{ name: 'Advil', quantity: 30, unit: 'tablet', medicineProductId: 'mp-1' }], }; it('throws NotFoundError when store not found', async () => { mockStoresRepo.findById.mockResolvedValue(null); await expect(service.create(validInput, 'hh1', 'user-1')).rejects.toThrow('Store not found'); }); it('throws NotFoundError when medicine product not found', async () => { mockStoresRepo.findById.mockResolvedValue(fakeStore); mockProductsRepo.findById.mockResolvedValue(null); await expect(service.create(validInput, 'hh1', 'user-1')).rejects.toThrow( 'Medicine product not found: mp-1', ); }); it('creates purchase with in_cabinet status and adds items to cabinet', async () => { mockStoresRepo.findById.mockResolvedValue(fakeStore); mockProductsRepo.findById.mockResolvedValue(fakeProduct); mockCabinetService.addItem.mockResolvedValue({}); const purchase = { _id: 'p-1', status: 'in_cabinet' }; mockPurchasesRepo.create.mockResolvedValue(purchase); const result = await service.create(validInput, 'hh1', 'user-1'); expect(mockCabinetService.addItem).toHaveBeenCalledOnce(); expect(mockPurchasesRepo.create).toHaveBeenCalledWith( expect.objectContaining({ status: 'in_cabinet', storeName: 'CVS' }), ); expect(result).toEqual(purchase); }); it('records price when actualPrice is set and status is in_cabinet', async () => { const inputWithPrice = { ...validInput, items: [{ ...validInput.items[0], actualPrice: 9.99, currency: 'USD' }], }; mockStoresRepo.findById.mockResolvedValue(fakeStore); mockProductsRepo.findById.mockResolvedValue(fakeProduct); mockCabinetService.addItem.mockResolvedValue({}); mockPricesRepo.create.mockResolvedValue({}); mockPurchasesRepo.create.mockResolvedValue({ _id: 'p-1' }); await service.create(inputWithPrice, 'hh1', 'user-1'); expect(mockPricesRepo.create).toHaveBeenCalledWith( expect.objectContaining({ price: 9.99, medicineName: 'Ibuprofen', storeName: 'CVS', pricePerUnit: expect.closeTo(0.333, 2), }), ); }); it('does not add to cabinet when status is ordered', async () => { const orderedInput = { ...validInput, status: 'ordered' as const }; mockStoresRepo.findById.mockResolvedValue(fakeStore); mockProductsRepo.findById.mockResolvedValue(fakeProduct); mockPurchasesRepo.create.mockResolvedValue({ _id: 'p-1', status: 'ordered' }); await service.create(orderedInput, 'hh1', 'user-1'); expect(mockCabinetService.addItem).not.toHaveBeenCalled(); expect(mockPricesRepo.create).not.toHaveBeenCalled(); }); it('handles item without medicineProductId for in_cabinet', async () => { const noProductInput = { storeId: 'st-1', status: 'in_cabinet' as const, items: [{ name: 'Generic OTC', quantity: 1, unit: 'tablet' }], }; mockStoresRepo.findById.mockResolvedValue(fakeStore); mockPurchasesRepo.create.mockResolvedValue({ _id: 'p-1' }); await service.create(noProductInput, 'hh1', 'user-1'); expect(mockCabinetService.addItem).not.toHaveBeenCalled(); expect(mockPurchasesRepo.create).toHaveBeenCalled(); }); it('uses purchasedAt from input when provided', async () => { const inputWithDate = { ...validInput, purchasedAt: '2026-01-15T00:00:00.000Z' }; mockStoresRepo.findById.mockResolvedValue(fakeStore); mockProductsRepo.findById.mockResolvedValue(fakeProduct); mockCabinetService.addItem.mockResolvedValue({}); mockPurchasesRepo.create.mockResolvedValue({ _id: 'p-1' }); await service.create(inputWithDate, 'hh1', 'user-1'); expect(mockPurchasesRepo.create).toHaveBeenCalledWith( expect.objectContaining({ purchasedAt: new Date('2026-01-15T00:00:00.000Z') }), ); }); it('uses medicineName as brand fallback when brand is undefined', async () => { mockStoresRepo.findById.mockResolvedValue(fakeStore); mockProductsRepo.findById.mockResolvedValue({ ...fakeProduct, brand: undefined }); mockCabinetService.addItem.mockResolvedValue({}); mockPricesRepo.create.mockResolvedValue({}); const inputWithPrice = { ...validInput, items: [{ ...validInput.items[0], actualPrice: 5 }], }; mockPurchasesRepo.create.mockResolvedValue({ _id: 'p-1' }); await service.create(inputWithPrice, 'hh1', 'user-1'); expect(mockPricesRepo.create).toHaveBeenCalledWith( expect.objectContaining({ medicineProductBrand: 'Ibuprofen' }), ); }); }); describe('receive', () => { it('throws NotFoundError when purchase not found', async () => { mockPurchasesRepo.findById.mockResolvedValue(null); await expect(service.receive('missing', 'hh1', 'user-1')).rejects.toThrow('Purchase not found'); }); it('throws BadRequestError when status is not ordered', async () => { mockPurchasesRepo.findById.mockResolvedValue({ _id: 'p-1', status: 'in_cabinet', items: [] }); await expect(service.receive('p-1', 'hh1', 'user-1')).rejects.toThrow( 'Purchase is not in ordered status', ); }); it('adds medicine items to cabinet and calls receiveAll', async () => { const purchase = { _id: 'p-1', status: 'ordered', storeId: 'st-1', storeName: 'CVS', purchasedAt: new Date('2026-01-01T00:00:00.000Z'), items: [ { medicineProductId: 'mp-1', medicineId: 'med-1', name: 'Advil', quantity: 30, unit: 'tablet', addedToCabinet: false, }, ], }; mockPurchasesRepo.findById.mockResolvedValue(purchase); mockCabinetService.addItem.mockResolvedValue({}); mockPurchasesRepo.receiveAll.mockResolvedValue({ _id: 'p-1', status: 'in_cabinet' }); const result = await service.receive('p-1', 'hh1', 'user-1'); expect(mockCabinetService.addItem).toHaveBeenCalledOnce(); expect(mockPurchasesRepo.receiveAll).toHaveBeenCalledWith('p-1', 'hh1'); expect(result.addedCount).toBe(1); expect(result.priceRecordsCreated).toBe(0); }); it('creates price record when actualPrice is set on item', async () => { const purchase = { _id: 'p-1', status: 'ordered', storeId: 'st-1', storeName: 'CVS', purchasedAt: new Date('2026-01-01T00:00:00.000Z'), items: [ { medicineProductId: 'mp-1', medicineId: 'med-1', name: 'Advil', quantity: 30, unit: 'tablet', actualPrice: 9.99, currency: 'USD', addedToCabinet: false, }, ], }; mockPurchasesRepo.findById.mockResolvedValue(purchase); mockCabinetService.addItem.mockResolvedValue({}); mockProductsRepo.findById.mockResolvedValue(fakeProduct); mockPricesRepo.create.mockResolvedValue({}); mockPurchasesRepo.receiveAll.mockResolvedValue({}); const result = await service.receive('p-1', 'hh1', 'user-1'); expect(mockPricesRepo.create).toHaveBeenCalledOnce(); expect(result.priceRecordsCreated).toBe(1); }); it('uses medicineName as brand fallback in price record when brand is undefined', async () => { const purchase = { _id: 'p-1', status: 'ordered', storeId: 'st-1', storeName: 'CVS', purchasedAt: new Date('2026-01-01T00:00:00.000Z'), items: [ { medicineProductId: 'mp-1', medicineId: 'med-1', name: 'Ibuprofen', quantity: 30, unit: 'tablet', actualPrice: 9.99, currency: 'USD', addedToCabinet: false, }, ], }; mockPurchasesRepo.findById.mockResolvedValue(purchase); mockCabinetService.addItem.mockResolvedValue({}); mockProductsRepo.findById.mockResolvedValue({ ...fakeProduct, brand: undefined }); mockPricesRepo.create.mockResolvedValue({}); mockPurchasesRepo.receiveAll.mockResolvedValue({}); await service.receive('p-1', 'hh1', 'user-1'); expect(mockPricesRepo.create).toHaveBeenCalledWith( expect.objectContaining({ medicineProductBrand: 'Ibuprofen' }), ); }); it('skips price record creation when product not found in receive', async () => { const purchase = { _id: 'p-1', status: 'ordered', storeId: 'st-1', storeName: 'CVS', purchasedAt: new Date(), items: [ { medicineProductId: 'mp-1', medicineId: 'med-1', name: 'Advil', quantity: 30, unit: 'tablet', actualPrice: 9.99, addedToCabinet: false, }, ], }; mockPurchasesRepo.findById.mockResolvedValue(purchase); mockCabinetService.addItem.mockResolvedValue({}); mockProductsRepo.findById.mockResolvedValue(null); mockPurchasesRepo.receiveAll.mockResolvedValue({}); const result = await service.receive('p-1', 'hh1', 'user-1'); expect(mockPricesRepo.create).not.toHaveBeenCalled(); expect(result.priceRecordsCreated).toBe(0); }); it('skips items already added to cabinet', async () => { const purchase = { _id: 'p-1', status: 'ordered', storeId: 'st-1', storeName: 'CVS', purchasedAt: new Date(), items: [ { medicineProductId: 'mp-1', medicineId: 'med-1', name: 'X', quantity: 10, unit: 'tablet', addedToCabinet: true }, ], }; mockPurchasesRepo.findById.mockResolvedValue(purchase); mockPurchasesRepo.receiveAll.mockResolvedValue({}); const result = await service.receive('p-1', 'hh1', 'user-1'); expect(mockCabinetService.addItem).not.toHaveBeenCalled(); expect(result.addedCount).toBe(0); }); }); describe('list', () => { it('delegates to repository', async () => { const result = { data: [], pagination: { cursor: null, hasMore: false } }; mockPurchasesRepo.findByHousehold.mockResolvedValue(result); const response = await service.list('hh1', { limit: 20 }); expect(response).toEqual(result); expect(mockPurchasesRepo.findByHousehold).toHaveBeenCalledWith('hh1', { limit: 20 }); }); }); describe('getById', () => { it('returns purchase when found', async () => { const purchase = { _id: 'p-1', status: 'in_cabinet' }; mockPurchasesRepo.findById.mockResolvedValue(purchase); expect(await service.getById('p-1', 'hh1')).toEqual(purchase); }); it('throws NotFoundError when not found', async () => { mockPurchasesRepo.findById.mockResolvedValue(null); await expect(service.getById('missing', 'hh1')).rejects.toThrow('Purchase not found'); }); }); describe('update', () => { it('updates and returns purchase', async () => { mockPurchasesRepo.findById.mockResolvedValue({ _id: 'p-1' }); const updated = { _id: 'p-1', notes: 'updated' }; mockPurchasesRepo.update.mockResolvedValue(updated); const result = await service.update('p-1', 'hh1', { notes: 'updated' }); expect(result).toEqual(updated); }); it('throws NotFoundError when purchase does not exist', async () => { mockPurchasesRepo.findById.mockResolvedValue(null); await expect(service.update('missing', 'hh1', {})).rejects.toThrow('Purchase not found'); }); it('throws NotFoundError when update returns null', async () => { mockPurchasesRepo.findById.mockResolvedValue({ _id: 'p-1' }); mockPurchasesRepo.update.mockResolvedValue(null); await expect(service.update('p-1', 'hh1', {})).rejects.toThrow('Purchase not found'); }); }); describe('delete', () => { it('soft-deletes and returns purchase', async () => { const deleted = { _id: 'p-1', isDeleted: true }; mockPurchasesRepo.softDelete.mockResolvedValue(deleted); const result = await service.delete('p-1', 'hh1'); expect(result).toEqual(deleted); expect(mockPurchasesRepo.softDelete).toHaveBeenCalledWith('p-1', 'hh1'); }); it('throws NotFoundError when purchase not found', async () => { mockPurchasesRepo.softDelete.mockResolvedValue(null); await expect(service.delete('missing', 'hh1')).rejects.toThrow( 'Purchase not found or cannot be deleted', ); }); }); describe('getPendingStockByMedicine', () => { it('returns map of medicineId to totalUnits', async () => { mockPurchasesRepo.getPendingMedicineStock.mockResolvedValue([ { medicineId: 'med-1', totalUnits: 60 }, { medicineId: 'med-2', totalUnits: 30 }, ]); const result = await service.getPendingStockByMedicine('hh1'); expect(result.get('med-1')).toBe(60); expect(result.get('med-2')).toBe(30); }); it('returns empty map when no pending stock', async () => { mockPurchasesRepo.getPendingMedicineStock.mockResolvedValue([]); const result = await service.getPendingStockByMedicine('hh1'); expect(result.size).toBe(0); }); }); });