MeshiTrack/packages/api/tests/modules/medicine-prices/medicine-prices.service.test.ts

182 lines
5.7 KiB
TypeScript
Raw Normal View History

import { describe, it, expect, vi, beforeEach } from 'vitest';
2026-05-19 11:06:03 +09:00
import { MedicinePricesService } from '../../../src/modules/medicine-prices/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',
});
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',
}),
);
});
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',
});
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',
);
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',
});
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' });
});
});
});