2026-04-18 12:36:29 +09:00
|
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
|
|
import { MedicinePricesService } from './medicine-prices.service.js';
|
|
|
|
|
|
|
|
|
|
describe(MedicinePricesService.name, () => {
|
|
|
|
|
const mockPricesRepo = {
|
|
|
|
|
create: vi.fn(),
|
|
|
|
|
findByMedicine: vi.fn(),
|
|
|
|
|
compareStores: vi.fn(),
|
|
|
|
|
getLatestForMedicine: vi.fn(),
|
|
|
|
|
getAnalytics: vi.fn(),
|
|
|
|
|
};
|
|
|
|
|
const mockProductsRepo = {
|
|
|
|
|
findById: vi.fn(),
|
|
|
|
|
};
|
|
|
|
|
const mockStoresRepo = {
|
|
|
|
|
findById: vi.fn(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let service: MedicinePricesService;
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
vi.clearAllMocks();
|
|
|
|
|
service = new MedicinePricesService({
|
|
|
|
|
medicinePricesRepository: mockPricesRepo as never,
|
|
|
|
|
medicineProductsRepository: mockProductsRepo as never,
|
|
|
|
|
storesRepository: mockStoresRepo as never,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('recordPrice', () => {
|
|
|
|
|
const validInput = {
|
|
|
|
|
medicineProductId: 'mp-1',
|
|
|
|
|
medicineId: 'med-1',
|
|
|
|
|
storeId: 'st-1',
|
|
|
|
|
price: 10,
|
|
|
|
|
currency: 'USD',
|
|
|
|
|
quantity: 100,
|
|
|
|
|
unit: 'tablet' as never,
|
|
|
|
|
isInsurancePrice: false,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
it('creates price record with computed pricePerUnit', async () => {
|
2026-04-26 18:44:59 +09:00
|
|
|
mockProductsRepo.findById.mockResolvedValue({
|
|
|
|
|
medicineName: 'Acetaminophen',
|
|
|
|
|
brand: 'Tylenol',
|
|
|
|
|
});
|
2026-04-18 12:36:29 +09:00
|
|
|
mockStoresRepo.findById.mockResolvedValue({ name: 'Walgreens' });
|
|
|
|
|
const record = { _id: 'pr-1', pricePerUnit: 0.1 };
|
|
|
|
|
mockPricesRepo.create.mockResolvedValue(record);
|
|
|
|
|
|
|
|
|
|
const result = await service.recordPrice(validInput, 'hh1', 'user-1');
|
|
|
|
|
|
|
|
|
|
expect(result).toEqual(record);
|
|
|
|
|
expect(mockPricesRepo.create).toHaveBeenCalledWith(
|
2026-04-26 18:44:59 +09:00
|
|
|
expect.objectContaining({
|
|
|
|
|
pricePerUnit: 0.1,
|
|
|
|
|
medicineName: 'Acetaminophen',
|
|
|
|
|
storeName: 'Walgreens',
|
|
|
|
|
}),
|
2026-04-18 12:36:29 +09:00
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('uses medicineName as brand fallback when brand is not set', async () => {
|
|
|
|
|
mockProductsRepo.findById.mockResolvedValue({ medicineName: 'Generic', brand: undefined });
|
|
|
|
|
mockStoresRepo.findById.mockResolvedValue({ name: 'CVS' });
|
|
|
|
|
mockPricesRepo.create.mockResolvedValue({ _id: 'pr-1' });
|
|
|
|
|
|
|
|
|
|
await service.recordPrice(validInput, 'hh1', 'user-1');
|
|
|
|
|
|
|
|
|
|
expect(mockPricesRepo.create).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({ medicineProductBrand: 'Generic' }),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('uses provided date when given', async () => {
|
2026-04-26 18:44:59 +09:00
|
|
|
mockProductsRepo.findById.mockResolvedValue({
|
|
|
|
|
medicineName: 'Acetaminophen',
|
|
|
|
|
brand: 'Tylenol',
|
|
|
|
|
});
|
2026-04-18 12:36:29 +09:00
|
|
|
mockStoresRepo.findById.mockResolvedValue({ name: 'Walgreens' });
|
|
|
|
|
mockPricesRepo.create.mockResolvedValue({ _id: 'pr-1' });
|
|
|
|
|
|
2026-04-26 18:44:59 +09:00
|
|
|
await service.recordPrice(
|
|
|
|
|
{ ...validInput, date: '2026-01-15T00:00:00.000Z' },
|
|
|
|
|
'hh1',
|
|
|
|
|
'user-1',
|
|
|
|
|
);
|
2026-04-18 12:36:29 +09:00
|
|
|
|
|
|
|
|
expect(mockPricesRepo.create).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({ date: new Date('2026-01-15T00:00:00.000Z') }),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('throws NotFoundError when product not found', async () => {
|
|
|
|
|
mockProductsRepo.findById.mockResolvedValue(null);
|
|
|
|
|
|
|
|
|
|
await expect(service.recordPrice(validInput, 'hh1', 'user-1')).rejects.toThrow(
|
|
|
|
|
'Medicine product not found',
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('throws NotFoundError when store not found', async () => {
|
2026-04-26 18:44:59 +09:00
|
|
|
mockProductsRepo.findById.mockResolvedValue({
|
|
|
|
|
medicineName: 'Acetaminophen',
|
|
|
|
|
brand: 'Tylenol',
|
|
|
|
|
});
|
2026-04-18 12:36:29 +09:00
|
|
|
mockStoresRepo.findById.mockResolvedValue(null);
|
|
|
|
|
|
|
|
|
|
await expect(service.recordPrice(validInput, 'hh1', 'user-1')).rejects.toThrow(
|
|
|
|
|
'Store not found',
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('getPriceHistory', () => {
|
|
|
|
|
it('delegates to repository', async () => {
|
|
|
|
|
const result = { data: [], pagination: { cursor: null, hasMore: false } };
|
|
|
|
|
mockPricesRepo.findByMedicine.mockResolvedValue(result);
|
|
|
|
|
|
|
|
|
|
const response = await service.getPriceHistory('hh1', 'med-1', { limit: 20 });
|
|
|
|
|
|
|
|
|
|
expect(response).toEqual(result);
|
|
|
|
|
expect(mockPricesRepo.findByMedicine).toHaveBeenCalledWith('hh1', 'med-1', { limit: 20 });
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('compareStores', () => {
|
|
|
|
|
it('delegates to repository', async () => {
|
|
|
|
|
const comparisons = [{ storeId: 'st-1', storeName: 'Walgreens', latestPrice: 10 }];
|
|
|
|
|
mockPricesRepo.compareStores.mockResolvedValue(comparisons);
|
|
|
|
|
|
|
|
|
|
const result = await service.compareStores('hh1', 'med-1');
|
|
|
|
|
|
|
|
|
|
expect(result).toEqual(comparisons);
|
|
|
|
|
expect(mockPricesRepo.compareStores).toHaveBeenCalledWith('hh1', 'med-1');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('estimatePrice', () => {
|
|
|
|
|
it('returns pricePerUnit of latest record', async () => {
|
|
|
|
|
mockPricesRepo.getLatestForMedicine.mockResolvedValue({ pricePerUnit: 0.15 });
|
|
|
|
|
|
|
|
|
|
const result = await service.estimatePrice('hh1', 'med-1');
|
|
|
|
|
|
|
|
|
|
expect(result).toBe(0.15);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('returns null when no records', async () => {
|
|
|
|
|
mockPricesRepo.getLatestForMedicine.mockResolvedValue(null);
|
|
|
|
|
|
|
|
|
|
const result = await service.estimatePrice('hh1', 'med-1');
|
|
|
|
|
|
|
|
|
|
expect(result).toBeNull();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('filters by storeId when provided', async () => {
|
|
|
|
|
mockPricesRepo.getLatestForMedicine.mockResolvedValue({ pricePerUnit: 0.2 });
|
|
|
|
|
|
|
|
|
|
await service.estimatePrice('hh1', 'med-1', 'st-1');
|
|
|
|
|
|
|
|
|
|
expect(mockPricesRepo.getLatestForMedicine).toHaveBeenCalledWith('hh1', 'med-1', 'st-1');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('getAnalytics', () => {
|
|
|
|
|
it('delegates to repository', async () => {
|
|
|
|
|
const analytics = {
|
|
|
|
|
spendingOverTime: [],
|
|
|
|
|
topBySpending: [],
|
|
|
|
|
spendingByStore: [],
|
|
|
|
|
priceAlerts: [],
|
|
|
|
|
};
|
|
|
|
|
mockPricesRepo.getAnalytics.mockResolvedValue(analytics);
|
|
|
|
|
|
|
|
|
|
const result = await service.getAnalytics('hh1', { period: 'month' });
|
|
|
|
|
|
|
|
|
|
expect(result).toEqual(analytics);
|
|
|
|
|
expect(mockPricesRepo.getAnalytics).toHaveBeenCalledWith('hh1', { period: 'month' });
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|