2026-04-18 12:36:29 +09:00
|
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
|
|
|
|
|
|
|
const { mockFind, mockFindOne, mockAggregate, mockSave } = vi.hoisted(() => ({
|
|
|
|
|
mockFind: vi.fn(),
|
|
|
|
|
mockFindOne: vi.fn(),
|
|
|
|
|
mockAggregate: vi.fn(),
|
|
|
|
|
mockSave: vi.fn(),
|
|
|
|
|
}));
|
|
|
|
|
|
2026-05-19 11:06:03 +09:00
|
|
|
vi.mock('../../../src/schemas/medicine-price.schema.js', () => {
|
2026-04-18 12:36:29 +09:00
|
|
|
const chain = () => ({
|
|
|
|
|
sort: vi.fn().mockReturnThis(),
|
|
|
|
|
limit: vi.fn().mockReturnThis(),
|
|
|
|
|
lean: vi.fn().mockReturnThis(),
|
|
|
|
|
exec: mockFind,
|
|
|
|
|
});
|
|
|
|
|
const findOneChain = () => ({
|
|
|
|
|
sort: vi.fn().mockReturnThis(),
|
|
|
|
|
lean: vi.fn().mockReturnThis(),
|
|
|
|
|
exec: mockFindOne,
|
|
|
|
|
});
|
|
|
|
|
const aggregateChain = () => ({ exec: mockAggregate });
|
|
|
|
|
|
|
|
|
|
class FakeModel {
|
|
|
|
|
data: unknown;
|
2026-04-26 18:44:59 +09:00
|
|
|
constructor(data: unknown) {
|
|
|
|
|
this.data = data;
|
|
|
|
|
}
|
2026-04-18 12:36:29 +09:00
|
|
|
save = mockSave;
|
2026-04-26 18:44:59 +09:00
|
|
|
toObject() {
|
|
|
|
|
return this.data;
|
|
|
|
|
}
|
2026-04-18 12:36:29 +09:00
|
|
|
static find = vi.fn(() => chain());
|
|
|
|
|
static findOne = vi.fn(() => findOneChain());
|
|
|
|
|
static aggregate = vi.fn(() => aggregateChain());
|
|
|
|
|
}
|
|
|
|
|
return { MedicinePriceModel: FakeModel };
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-19 11:06:03 +09:00
|
|
|
import { MedicinePricesRepository } from '../../../src/modules/medicine-prices/medicine-prices.repository.js';
|
2026-04-18 12:36:29 +09:00
|
|
|
|
|
|
|
|
describe(MedicinePricesRepository.name, () => {
|
|
|
|
|
let repo: MedicinePricesRepository;
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
vi.clearAllMocks();
|
|
|
|
|
repo = new MedicinePricesRepository();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('create', () => {
|
|
|
|
|
it('saves and returns price record', async () => {
|
|
|
|
|
const data = {
|
|
|
|
|
householdId: 'hh1',
|
|
|
|
|
medicineProductId: 'mp-1',
|
|
|
|
|
medicineProductBrand: 'Tylenol',
|
|
|
|
|
medicineId: 'med-1',
|
|
|
|
|
medicineName: 'Acetaminophen',
|
|
|
|
|
storeId: 'st-1',
|
|
|
|
|
storeName: 'Walgreens',
|
|
|
|
|
price: 10,
|
|
|
|
|
currency: 'USD',
|
|
|
|
|
quantity: 100,
|
|
|
|
|
unit: 'tablet',
|
|
|
|
|
pricePerUnit: 0.1,
|
|
|
|
|
date: new Date(),
|
|
|
|
|
isInsurancePrice: false,
|
|
|
|
|
createdBy: 'user-1',
|
|
|
|
|
};
|
|
|
|
|
mockSave.mockImplementation(function (this: { toObject: () => unknown }) {
|
|
|
|
|
return Promise.resolve(this);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const result = await repo.create(data as never);
|
|
|
|
|
|
|
|
|
|
expect(mockSave).toHaveBeenCalled();
|
|
|
|
|
expect(result).toBeTruthy();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('findByMedicine', () => {
|
|
|
|
|
it('returns paginated items', async () => {
|
|
|
|
|
const items = [{ _id: 'pr-1', medicineId: 'med-1' }];
|
|
|
|
|
mockFind.mockResolvedValue(items);
|
|
|
|
|
|
|
|
|
|
const result = await repo.findByMedicine('hh1', 'med-1', { limit: 20 });
|
|
|
|
|
|
|
|
|
|
expect(result.data).toEqual(items);
|
|
|
|
|
expect(result.pagination.hasMore).toBe(false);
|
|
|
|
|
expect(result.pagination.cursor).toBeNull();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('sets hasMore when more items exist', async () => {
|
|
|
|
|
const items = [{ _id: 'pr-1' }, { _id: 'pr-2' }, { _id: 'pr-3' }];
|
|
|
|
|
mockFind.mockResolvedValue(items);
|
|
|
|
|
|
|
|
|
|
const result = await repo.findByMedicine('hh1', 'med-1', { limit: 2 });
|
|
|
|
|
|
|
|
|
|
expect(result.data).toHaveLength(2);
|
|
|
|
|
expect(result.pagination.hasMore).toBe(true);
|
|
|
|
|
expect(result.pagination.cursor).toBeTruthy();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('handles cursor', async () => {
|
|
|
|
|
mockFind.mockResolvedValue([]);
|
|
|
|
|
|
|
|
|
|
const cursor = Buffer.from('pr-1').toString('base64');
|
|
|
|
|
const result = await repo.findByMedicine('hh1', 'med-1', { cursor, limit: 20 });
|
|
|
|
|
|
|
|
|
|
expect(result.pagination.hasMore).toBe(false);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('returns null cursor when no data', async () => {
|
|
|
|
|
mockFind.mockResolvedValue([]);
|
|
|
|
|
|
|
|
|
|
const result = await repo.findByMedicine('hh1', 'med-1', { limit: 20 });
|
|
|
|
|
|
|
|
|
|
expect(result.pagination.cursor).toBeNull();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('applies storeId filter', async () => {
|
|
|
|
|
mockFind.mockResolvedValue([]);
|
|
|
|
|
|
|
|
|
|
await repo.findByMedicine('hh1', 'med-1', { storeId: 'st-1', limit: 20 });
|
|
|
|
|
|
|
|
|
|
expect(mockFind).toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('applies startDate-only filter', async () => {
|
|
|
|
|
mockFind.mockResolvedValue([]);
|
|
|
|
|
|
|
|
|
|
await repo.findByMedicine('hh1', 'med-1', {
|
|
|
|
|
startDate: '2026-01-01T00:00:00.000Z',
|
|
|
|
|
limit: 20,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(mockFind).toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('applies endDate-only filter', async () => {
|
|
|
|
|
mockFind.mockResolvedValue([]);
|
|
|
|
|
|
|
|
|
|
await repo.findByMedicine('hh1', 'med-1', {
|
|
|
|
|
endDate: '2026-12-31T00:00:00.000Z',
|
|
|
|
|
limit: 20,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(mockFind).toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('compareStores', () => {
|
|
|
|
|
it('returns store comparison results', async () => {
|
|
|
|
|
const rows = [
|
|
|
|
|
{
|
|
|
|
|
_id: 'st-1',
|
|
|
|
|
storeName: 'Walgreens',
|
|
|
|
|
latestPrice: 10,
|
|
|
|
|
latestPricePerUnit: 0.1,
|
|
|
|
|
currency: 'USD',
|
|
|
|
|
date: new Date(),
|
|
|
|
|
isInsurancePrice: false,
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
mockAggregate.mockResolvedValue(rows);
|
|
|
|
|
|
|
|
|
|
const result = await repo.compareStores('hh1', 'med-1');
|
|
|
|
|
|
|
|
|
|
expect(result).toHaveLength(1);
|
|
|
|
|
expect(result[0].storeId).toBe('st-1');
|
|
|
|
|
expect(result[0].storeName).toBe('Walgreens');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('returns empty array when no records', async () => {
|
|
|
|
|
mockAggregate.mockResolvedValue([]);
|
|
|
|
|
|
|
|
|
|
const result = await repo.compareStores('hh1', 'med-1');
|
|
|
|
|
|
|
|
|
|
expect(result).toEqual([]);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('getLatestForMedicine', () => {
|
|
|
|
|
it('returns latest record', async () => {
|
|
|
|
|
const record = { _id: 'pr-1', pricePerUnit: 0.1 };
|
|
|
|
|
mockFindOne.mockResolvedValue(record);
|
|
|
|
|
|
|
|
|
|
const result = await repo.getLatestForMedicine('hh1', 'med-1');
|
|
|
|
|
|
|
|
|
|
expect(result).toEqual(record);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('filters by storeId when provided', async () => {
|
|
|
|
|
mockFindOne.mockResolvedValue(null);
|
|
|
|
|
|
|
|
|
|
const result = await repo.getLatestForMedicine('hh1', 'med-1', 'st-1');
|
|
|
|
|
|
|
|
|
|
expect(result).toBeNull();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('returns null when not found', async () => {
|
|
|
|
|
mockFindOne.mockResolvedValue(null);
|
|
|
|
|
|
|
|
|
|
expect(await repo.getLatestForMedicine('hh1', 'med-1')).toBeNull();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('getAnalytics', () => {
|
|
|
|
|
it('returns analytics object with all fields', async () => {
|
|
|
|
|
mockAggregate.mockResolvedValue([]);
|
|
|
|
|
|
|
|
|
|
const result = await repo.getAnalytics('hh1', { period: 'month' });
|
|
|
|
|
|
|
|
|
|
expect(result).toHaveProperty('spendingOverTime');
|
|
|
|
|
expect(result).toHaveProperty('topBySpending');
|
|
|
|
|
expect(result).toHaveProperty('spendingByStore');
|
|
|
|
|
expect(result).toHaveProperty('priceAlerts');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('uses quarter date format', async () => {
|
|
|
|
|
mockAggregate.mockResolvedValue([]);
|
|
|
|
|
|
|
|
|
|
const result = await repo.getAnalytics('hh1', { period: 'quarter' });
|
|
|
|
|
|
|
|
|
|
expect(result).toHaveProperty('spendingOverTime');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('uses year date format', async () => {
|
|
|
|
|
mockAggregate.mockResolvedValue([]);
|
|
|
|
|
|
|
|
|
|
const result = await repo.getAnalytics('hh1', { period: 'year' });
|
|
|
|
|
|
|
|
|
|
expect(result).toHaveProperty('spendingOverTime');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('handles non-empty analytics results', async () => {
|
|
|
|
|
mockAggregate
|
|
|
|
|
.mockResolvedValueOnce([{ period: '2026-01', total: 50 }])
|
2026-04-26 18:44:59 +09:00
|
|
|
.mockResolvedValueOnce([
|
|
|
|
|
{
|
|
|
|
|
medicineId: 'med-1',
|
|
|
|
|
medicineName: 'Acetaminophen',
|
|
|
|
|
totalSpent: 50,
|
|
|
|
|
avgPricePerUnit: 0.1,
|
|
|
|
|
},
|
|
|
|
|
])
|
|
|
|
|
.mockResolvedValueOnce([
|
|
|
|
|
{ storeId: 'st-1', storeName: 'Walgreens', totalSpent: 50, purchaseCount: 5 },
|
|
|
|
|
])
|
2026-04-18 12:36:29 +09:00
|
|
|
.mockResolvedValueOnce([]);
|
|
|
|
|
|
|
|
|
|
const result = await repo.getAnalytics('hh1', { period: 'month' });
|
|
|
|
|
|
|
|
|
|
expect(result.spendingOverTime).toHaveLength(1);
|
|
|
|
|
expect(result.topBySpending).toHaveLength(1);
|
|
|
|
|
expect(result.spendingByStore).toHaveLength(1);
|
|
|
|
|
expect(result.priceAlerts).toHaveLength(0);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|