257 lines
8 KiB
TypeScript
257 lines
8 KiB
TypeScript
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('../../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 './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('../../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('../../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('../../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('../../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('../../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('../../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([]);
|
|
});
|
|
});
|
|
});
|