import { describe, it, expect, vi, beforeEach } from 'vitest'; const { mockFind, mockFindOne, mockFindOneAndUpdate, mockAggregate, mockSave } = vi.hoisted(() => ({ mockFind: vi.fn(), mockFindOne: vi.fn(), mockFindOneAndUpdate: vi.fn(), mockAggregate: vi.fn(), mockSave: vi.fn(), })); vi.mock('../../../src/schemas/purchase.schema.js', () => { const findChain = () => ({ sort: vi.fn().mockReturnThis(), limit: vi.fn().mockReturnThis(), lean: vi.fn().mockReturnThis(), exec: mockFind, }); const findOneChain = () => ({ lean: vi.fn().mockReturnThis(), exec: mockFindOne }); const updateChain = () => ({ exec: mockFindOneAndUpdate }); const aggregateChain = () => ({ exec: mockAggregate }); class FakeModel { data: unknown; constructor(data: unknown) { this.data = data; } save = mockSave; toObject() { return this.data; } static find = vi.fn(() => findChain()); static findOne = vi.fn(() => findOneChain()); static findOneAndUpdate = vi.fn(() => updateChain()); static aggregate = vi.fn(() => aggregateChain()); } return { PurchaseModel: FakeModel }; }); import { PurchasesRepository } from '../../../src/modules/purchases/purchases.repository.js'; const makeItem = (overrides = {}) => ({ medicineProductId: 'mp-1', medicineId: 'med-1', name: 'Tylenol', quantity: 30, unit: 'tablet', addedToCabinet: false, ...overrides, }); describe(PurchasesRepository.name, () => { let repo: PurchasesRepository; beforeEach(() => { vi.clearAllMocks(); repo = new PurchasesRepository(); }); describe('create', () => { it('saves and returns plain object', async () => { const data = { householdId: 'hh1', storeId: 'st-1', storeName: 'CVS', status: 'in_cabinet', items: [makeItem()], purchasedAt: new Date(), createdBy: 'u-1', }; mockSave.mockResolvedValue({ toObject: () => data }); const result = await repo.create(data); expect(mockSave).toHaveBeenCalled(); expect(result).toEqual(data); }); }); describe('findByHousehold', () => { it('returns paginated items without hasMore', async () => { const items = [{ _id: { toString: () => 'p-1' }, householdId: 'hh1' }]; mockFind.mockResolvedValue(items); const result = await repo.findByHousehold('hh1', { limit: 20 }); expect(result.data).toEqual(items); expect(result.pagination.hasMore).toBe(false); expect(result.pagination.cursor).toBeNull(); }); it('returns hasMore and cursor when results exceed limit', async () => { const items = Array.from({ length: 21 }, (_, i) => ({ _id: { toString: () => `p-${i}` } })); mockFind.mockResolvedValue(items); const result = await repo.findByHousehold('hh1', { limit: 20 }); expect(result.data).toHaveLength(20); expect(result.pagination.hasMore).toBe(true); expect(result.pagination.cursor).not.toBeNull(); }); it('filters by status when provided', async () => { mockFind.mockResolvedValue([]); const { PurchaseModel } = await import('../../../src/schemas/purchase.schema.js'); await repo.findByHousehold('hh1', { limit: 20, status: 'ordered' }); expect(PurchaseModel.find).toHaveBeenCalledWith( expect.objectContaining({ status: 'ordered' }), ); }); it('filters by storeId when provided', async () => { mockFind.mockResolvedValue([]); const { PurchaseModel } = await import('../../../src/schemas/purchase.schema.js'); await repo.findByHousehold('hh1', { limit: 20, storeId: 'st-1' }); expect(PurchaseModel.find).toHaveBeenCalledWith(expect.objectContaining({ storeId: 'st-1' })); }); it('applies cursor filter when provided', async () => { mockFind.mockResolvedValue([]); const cursor = Buffer.from('p-1').toString('base64'); const { PurchaseModel } = await import('../../../src/schemas/purchase.schema.js'); await repo.findByHousehold('hh1', { limit: 20, cursor }); expect(PurchaseModel.find).toHaveBeenCalledWith( expect.objectContaining({ _id: { $lt: 'p-1' } }), ); }); }); describe('findById', () => { it('returns purchase when found', async () => { const purchase = { _id: 'p-1', householdId: 'hh1' }; mockFindOne.mockResolvedValue(purchase); const result = await repo.findById('p-1', 'hh1'); expect(result).toEqual(purchase); }); it('returns null when not found', async () => { mockFindOne.mockResolvedValue(null); const result = await repo.findById('missing', 'hh1'); expect(result).toBeNull(); }); }); describe('update', () => { it('updates notes and returns updated doc', async () => { const updated = { _id: 'p-1', notes: 'new note' }; mockFindOneAndUpdate.mockResolvedValue(updated); const result = await repo.update('p-1', 'hh1', { notes: 'new note' }); expect(result).toEqual(updated); }); it('returns null when purchase not found', async () => { mockFindOneAndUpdate.mockResolvedValue(null); const result = await repo.update('missing', 'hh1', {}); expect(result).toBeNull(); }); it('includes items in update set when provided', async () => { const updated = { _id: 'p-1' }; mockFindOneAndUpdate.mockResolvedValue(updated); const { PurchaseModel } = await import('../../../src/schemas/purchase.schema.js'); const items = [{ name: 'X', quantity: 1, unit: 'tablet', addedToCabinet: false }]; await repo.update('p-1', 'hh1', { items } as never); expect(PurchaseModel.findOneAndUpdate).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ $set: expect.objectContaining({ items }) }), expect.anything(), ); }); }); describe('receiveAll', () => { it('sets status to in_cabinet and all items addedToCabinet', async () => { const updated = { _id: 'p-1', status: 'in_cabinet' }; mockFindOneAndUpdate.mockResolvedValue(updated); const { PurchaseModel } = await import('../../../src/schemas/purchase.schema.js'); const result = await repo.receiveAll('p-1', 'hh1'); expect(PurchaseModel.findOneAndUpdate).toHaveBeenCalledWith( { _id: 'p-1', householdId: 'hh1', isDeleted: false }, expect.objectContaining({ $set: expect.objectContaining({ status: 'in_cabinet' }), }), expect.anything(), ); expect(result).toEqual(updated); }); }); describe('markItemsAddedToCabinet', () => { it('builds per-index update set and calls findOneAndUpdate', async () => { const updated = { _id: 'p-1', items: [] }; mockFindOneAndUpdate.mockResolvedValue(updated); const { PurchaseModel } = await import('../../../src/schemas/purchase.schema.js'); const result = await repo.markItemsAddedToCabinet('p-1', 'hh1', [0, 2]); expect(PurchaseModel.findOneAndUpdate).toHaveBeenCalledWith( { _id: 'p-1', householdId: 'hh1', isDeleted: false }, expect.objectContaining({ $set: expect.objectContaining({ 'items.0.addedToCabinet': true, 'items.2.addedToCabinet': true, }), }), expect.anything(), ); expect(result).toEqual(updated); }); }); describe('softDelete', () => { it('sets isDeleted to true', async () => { mockFindOneAndUpdate.mockResolvedValue({ _id: 'p-1', isDeleted: true }); const result = await repo.softDelete('p-1', 'hh1'); expect(result).toBeTruthy(); }); }); describe('getPendingMedicineStock', () => { it('returns aggregated stock by medicineId', async () => { const rows = [{ medicineId: 'med-1', totalUnits: 60 }]; mockAggregate.mockResolvedValue(rows); const result = await repo.getPendingMedicineStock('hh1'); expect(result).toEqual(rows); }); it('returns empty array when no pending purchases', async () => { mockAggregate.mockResolvedValue([]); const result = await repo.getPendingMedicineStock('hh1'); expect(result).toEqual([]); }); }); });