import { describe, it, expect, vi, beforeEach } from 'vitest'; const { mockFind, mockSave, mockInsertMany, mockAggregate } = vi.hoisted(() => ({ mockFind: vi.fn(), mockSave: vi.fn(), mockInsertMany: vi.fn(), mockAggregate: vi.fn(), })); vi.mock('../../../src/schemas/cabinet-event.schema.js', () => { const chain = () => ({ sort: vi.fn().mockReturnThis(), limit: vi.fn().mockReturnThis(), lean: vi.fn().mockReturnThis(), exec: mockFind, }); const aggChain = () => ({ exec: mockAggregate, }); class FakeModel { data: unknown; constructor(data: unknown) { this.data = data; } save = mockSave; toObject() { return this.data; } static find = vi.fn(() => chain()); static insertMany = mockInsertMany; static aggregate = vi.fn(() => aggChain()); } return { CabinetEventModel: FakeModel }; }); import { CabinetEventsRepository } from '../../../src/modules/cabinet-events/cabinet-events.repository.js'; describe(CabinetEventsRepository.name, () => { let repo: CabinetEventsRepository; const baseEventData = { householdId: 'hh1', userId: 'user-1', cabinetItemId: 'ci-1', medicineId: 'med-1', medicineName: 'Metformin', eventType: 'purchased' as const, quantity: 10, quantityBefore: 0, quantityAfter: 10, sourceType: 'manual' as const, }; beforeEach(() => { vi.clearAllMocks(); repo = new CabinetEventsRepository(); }); describe('create', () => { it('creates and returns a cabinet event', async () => { mockSave.mockImplementation(function (this: { toObject: () => unknown }) { return Promise.resolve(this); }); const result = await repo.create(baseEventData); expect(result).toEqual(baseEventData); expect(mockSave).toHaveBeenCalled(); }); }); describe('createMany', () => { it('inserts multiple events', async () => { const events = [baseEventData, { ...baseEventData, quantity: 5 }]; mockInsertMany.mockResolvedValue(events); const result = await repo.createMany(events); expect(result).toEqual(events); expect(mockInsertMany).toHaveBeenCalledWith(events); }); }); describe('findByHousehold', () => { it('returns paginated events with no filters', async () => { const items = [{ _id: 'ev-1', quantity: 10 }]; mockFind.mockResolvedValue(items); const result = await repo.findByHousehold('hh1', { limit: 20 }); expect(result.data).toEqual(items); expect(result.pagination.hasMore).toBe(false); }); it('handles cursor-based pagination', async () => { const items = [{ _id: 'ev-2', quantity: 5 }]; mockFind.mockResolvedValue(items); const cursor = Buffer.from('ev-1').toString('base64'); const result = await repo.findByHousehold('hh1', { cursor, limit: 20 }); expect(result.data).toEqual(items); expect(result.pagination.hasMore).toBe(false); }); it('sets hasMore when more items exist', async () => { const items = Array.from({ length: 3 }, (_, i) => ({ _id: `ev-${i}`, quantity: i })); mockFind.mockResolvedValue(items); const result = await repo.findByHousehold('hh1', { limit: 2 }); expect(result.data).toHaveLength(2); expect(result.pagination.hasMore).toBe(true); expect(result.pagination.cursor).toBeTruthy(); }); it('returns null cursor when no data', async () => { mockFind.mockResolvedValue([]); const result = await repo.findByHousehold('hh1', { limit: 20 }); expect(result.pagination.cursor).toBeNull(); expect(result.pagination.hasMore).toBe(false); }); it('filters by medicineId', async () => { mockFind.mockResolvedValue([]); await repo.findByHousehold('hh1', { medicineId: 'med-1', limit: 20 }); expect(mockFind).toHaveBeenCalled(); }); it('filters by eventType', async () => { mockFind.mockResolvedValue([]); await repo.findByHousehold('hh1', { eventType: 'purchased' as never, limit: 20 }); expect(mockFind).toHaveBeenCalled(); }); it('filters by startDate only', async () => { mockFind.mockResolvedValue([]); await repo.findByHousehold('hh1', { startDate: '2024-01-01T00:00:00.000Z', limit: 20, }); expect(mockFind).toHaveBeenCalled(); }); it('filters by endDate only', async () => { mockFind.mockResolvedValue([]); await repo.findByHousehold('hh1', { endDate: '2024-12-31T00:00:00.000Z', limit: 20, }); expect(mockFind).toHaveBeenCalled(); }); it('filters by both startDate and endDate', async () => { mockFind.mockResolvedValue([]); await repo.findByHousehold('hh1', { startDate: '2024-01-01T00:00:00.000Z', endDate: '2024-12-31T00:00:00.000Z', limit: 20, }); expect(mockFind).toHaveBeenCalled(); }); }); describe('findByCabinetItem', () => { it('returns paginated events for a cabinet item', async () => { const items = [{ _id: 'ev-1', cabinetItemId: 'ci-1' }]; mockFind.mockResolvedValue(items); const result = await repo.findByCabinetItem('hh1', 'ci-1', { limit: 20 }); expect(result.data).toEqual(items); expect(result.pagination.hasMore).toBe(false); }); it('handles cursor-based pagination', async () => { const items = [{ _id: 'ev-2' }]; mockFind.mockResolvedValue(items); const cursor = Buffer.from('ev-1').toString('base64'); const result = await repo.findByCabinetItem('hh1', 'ci-1', { cursor, limit: 20 }); expect(result.data).toEqual(items); expect(result.pagination.hasMore).toBe(false); }); it('sets hasMore when more items exist', async () => { const items = Array.from({ length: 3 }, (_, i) => ({ _id: `ev-${i}` })); mockFind.mockResolvedValue(items); const result = await repo.findByCabinetItem('hh1', 'ci-1', { limit: 2 }); expect(result.data).toHaveLength(2); expect(result.pagination.hasMore).toBe(true); expect(result.pagination.cursor).toBeTruthy(); }); it('returns null cursor when no data', async () => { mockFind.mockResolvedValue([]); const result = await repo.findByCabinetItem('hh1', 'ci-1', { limit: 20 }); expect(result.pagination.cursor).toBeNull(); expect(result.pagination.hasMore).toBe(false); }); }); describe('getSpendingSummary', () => { it('returns spending summary with default month period', async () => { const byMedicine = [ { _id: 'med-1', medicineName: 'Metformin', totalSpent: 100, totalQuantity: 10, avgUnitPrice: 10, purchaseCount: 2, currency: 'USD', }, ]; const byPeriod = [{ _id: '2024-01', totalSpent: 100 }]; mockAggregate.mockResolvedValueOnce(byMedicine).mockResolvedValueOnce(byPeriod); const result = await repo.getSpendingSummary('hh1', { period: 'month' }); expect(result.totalSpent).toBe(100); expect(result.currency).toBe('USD'); expect(result.byMedicine).toHaveLength(1); expect(result.byMedicine[0].medicineId).toBe('med-1'); expect(result.byMedicine[0].medicineName).toBe('Metformin'); expect(result.byMedicine[0].totalSpent).toBe(100); expect(result.byMedicine[0].totalQuantity).toBe(10); expect(result.byMedicine[0].avgUnitPrice).toBe(10); expect(result.byMedicine[0].purchaseCount).toBe(2); expect(result.byPeriod).toHaveLength(1); expect(result.byPeriod[0].period).toBe('2024-01'); expect(result.byPeriod[0].totalSpent).toBe(100); }); it('returns null currency when no medicine data', async () => { mockAggregate.mockResolvedValueOnce([]).mockResolvedValueOnce([]); const result = await repo.getSpendingSummary('hh1', { period: 'month' }); expect(result.totalSpent).toBe(0); expect(result.currency).toBeNull(); expect(result.byMedicine).toHaveLength(0); expect(result.byPeriod).toHaveLength(0); }); it('filters by medicineId', async () => { mockAggregate.mockResolvedValueOnce([]).mockResolvedValueOnce([]); await repo.getSpendingSummary('hh1', { period: 'month', medicineId: 'med-1' }); expect(mockAggregate).toHaveBeenCalledTimes(2); }); it('filters by startDate only', async () => { mockAggregate.mockResolvedValueOnce([]).mockResolvedValueOnce([]); await repo.getSpendingSummary('hh1', { period: 'month', startDate: '2024-01-01T00:00:00.000Z', }); expect(mockAggregate).toHaveBeenCalledTimes(2); }); it('filters by endDate only', async () => { mockAggregate.mockResolvedValueOnce([]).mockResolvedValueOnce([]); await repo.getSpendingSummary('hh1', { period: 'month', endDate: '2024-12-31T00:00:00.000Z', }); expect(mockAggregate).toHaveBeenCalledTimes(2); }); it('filters by both startDate and endDate', async () => { mockAggregate.mockResolvedValueOnce([]).mockResolvedValueOnce([]); await repo.getSpendingSummary('hh1', { period: 'month', startDate: '2024-01-01T00:00:00.000Z', endDate: '2024-12-31T00:00:00.000Z', }); expect(mockAggregate).toHaveBeenCalledTimes(2); }); it('uses quarter date format', async () => { mockAggregate.mockResolvedValueOnce([]).mockResolvedValueOnce([]); await repo.getSpendingSummary('hh1', { period: 'quarter' }); expect(mockAggregate).toHaveBeenCalledTimes(2); }); it('uses year date format', async () => { mockAggregate.mockResolvedValueOnce([]).mockResolvedValueOnce([]); await repo.getSpendingSummary('hh1', { period: 'year' }); expect(mockAggregate).toHaveBeenCalledTimes(2); }); it('handles null currency in first medicine entry', async () => { const byMedicine = [ { _id: 'med-1', medicineName: 'Metformin', totalSpent: 50, totalQuantity: 5, avgUnitPrice: 10, purchaseCount: 1, currency: null, }, ]; mockAggregate.mockResolvedValueOnce(byMedicine).mockResolvedValueOnce([]); const result = await repo.getSpendingSummary('hh1', { period: 'month' }); expect(result.currency).toBeNull(); }); }); describe('getAvgUnitPriceByMedicine', () => { it('returns empty map when no medicine ids provided', async () => { const result = await repo.getAvgUnitPriceByMedicine('hh1', []); expect(result).toEqual(new Map()); expect(mockAggregate).not.toHaveBeenCalled(); }); it('returns map of avg unit prices', async () => { const results = [ { _id: 'med-1', avgUnitPrice: 10.5, currency: 'USD', totalSpent: 105, totalQuantity: 10 }, { _id: 'med-2', avgUnitPrice: 5.0, currency: 'EUR', totalSpent: 50, totalQuantity: 10 }, ]; mockAggregate.mockResolvedValue(results); const map = await repo.getAvgUnitPriceByMedicine('hh1', ['med-1', 'med-2']); expect(map.size).toBe(2); expect(map.get('med-1')).toEqual({ avgUnitPrice: 10.5, currency: 'USD' }); expect(map.get('med-2')).toEqual({ avgUnitPrice: 5.0, currency: 'EUR' }); }); it('handles null currency in results', async () => { const results = [ { _id: 'med-1', avgUnitPrice: 10, currency: null, totalSpent: 100, totalQuantity: 10 }, ]; mockAggregate.mockResolvedValue(results); const map = await repo.getAvgUnitPriceByMedicine('hh1', ['med-1']); expect(map.get('med-1')).toEqual({ avgUnitPrice: 10, currency: null }); }); }); });