Phase 5 cleanup
This commit is contained in:
parent
5536acd67d
commit
76a516a417
136 changed files with 6322 additions and 1985 deletions
|
|
@ -23,9 +23,13 @@ vi.mock('../../schemas/medicine-price.schema.js', () => {
|
|||
|
||||
class FakeModel {
|
||||
data: unknown;
|
||||
constructor(data: unknown) { this.data = data; }
|
||||
constructor(data: unknown) {
|
||||
this.data = data;
|
||||
}
|
||||
save = mockSave;
|
||||
toObject() { return this.data; }
|
||||
toObject() {
|
||||
return this.data;
|
||||
}
|
||||
static find = vi.fn(() => chain());
|
||||
static findOne = vi.fn(() => findOneChain());
|
||||
static aggregate = vi.fn(() => aggregateChain());
|
||||
|
|
@ -231,8 +235,17 @@ describe(MedicinePricesRepository.name, () => {
|
|||
it('handles non-empty analytics results', async () => {
|
||||
mockAggregate
|
||||
.mockResolvedValueOnce([{ period: '2026-01', total: 50 }])
|
||||
.mockResolvedValueOnce([{ medicineId: 'med-1', medicineName: 'Acetaminophen', totalSpent: 50, avgPricePerUnit: 0.1 }])
|
||||
.mockResolvedValueOnce([{ storeId: 'st-1', storeName: 'Walgreens', totalSpent: 50, purchaseCount: 5 }])
|
||||
.mockResolvedValueOnce([
|
||||
{
|
||||
medicineId: 'med-1',
|
||||
medicineName: 'Acetaminophen',
|
||||
totalSpent: 50,
|
||||
avgPricePerUnit: 0.1,
|
||||
},
|
||||
])
|
||||
.mockResolvedValueOnce([
|
||||
{ storeId: 'st-1', storeName: 'Walgreens', totalSpent: 50, purchaseCount: 5 },
|
||||
])
|
||||
.mockResolvedValueOnce([]);
|
||||
|
||||
const result = await repo.getAnalytics('hh1', { period: 'month' });
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import { MedicinePriceModel } from '../../schemas/medicine-price.schema.js';
|
||||
import type { MedicinePriceHistoryQueryInput, MedicinePriceAnalyticsQueryInput } from '@meshitrack/shared';
|
||||
import type {
|
||||
MedicinePriceHistoryQueryInput,
|
||||
MedicinePriceAnalyticsQueryInput,
|
||||
} from '@meshitrack/shared';
|
||||
|
||||
export interface CreateMedicinePriceData {
|
||||
householdId: string;
|
||||
|
|
@ -93,11 +96,7 @@ export class MedicinePricesRepository {
|
|||
}));
|
||||
}
|
||||
|
||||
public async getLatestForMedicine(
|
||||
householdId: string,
|
||||
medicineId: string,
|
||||
storeId?: string,
|
||||
) {
|
||||
public async getLatestForMedicine(householdId: string, medicineId: string, storeId?: string) {
|
||||
const filter: Record<string, unknown> = { householdId, medicineId };
|
||||
if (storeId) filter['storeId'] = storeId;
|
||||
return MedicinePriceModel.findOne(filter).sort({ date: -1 }).lean().exec();
|
||||
|
|
@ -132,7 +131,15 @@ export class MedicinePricesRepository {
|
|||
},
|
||||
{ $sort: { totalSpent: -1 } },
|
||||
{ $limit: 10 },
|
||||
{ $project: { _id: 0, medicineId: '$_id', medicineName: 1, totalSpent: 1, avgPricePerUnit: 1 } },
|
||||
{
|
||||
$project: {
|
||||
_id: 0,
|
||||
medicineId: '$_id',
|
||||
medicineName: 1,
|
||||
totalSpent: 1,
|
||||
avgPricePerUnit: 1,
|
||||
},
|
||||
},
|
||||
]).exec(),
|
||||
|
||||
MedicinePriceModel.aggregate([
|
||||
|
|
@ -195,9 +202,26 @@ export class MedicinePricesRepository {
|
|||
|
||||
return {
|
||||
spendingOverTime: spendingOverTime as { period: string; total: number }[],
|
||||
topBySpending: topBySpending as { medicineId: string; medicineName: string; totalSpent: number; avgPricePerUnit: number }[],
|
||||
spendingByStore: spendingByStore as { storeId: string; storeName: string; totalSpent: number; purchaseCount: number }[],
|
||||
priceAlerts: priceAlerts as { medicineId: string; medicineName: string; storeName: string; previousPrice: number; currentPrice: number; changePercent: number }[],
|
||||
topBySpending: topBySpending as {
|
||||
medicineId: string;
|
||||
medicineName: string;
|
||||
totalSpent: number;
|
||||
avgPricePerUnit: number;
|
||||
}[],
|
||||
spendingByStore: spendingByStore as {
|
||||
storeId: string;
|
||||
storeName: string;
|
||||
totalSpent: number;
|
||||
purchaseCount: number;
|
||||
}[],
|
||||
priceAlerts: priceAlerts as {
|
||||
medicineId: string;
|
||||
medicineName: string;
|
||||
storeName: string;
|
||||
previousPrice: number;
|
||||
currentPrice: number;
|
||||
changePercent: number;
|
||||
}[],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,17 +18,14 @@ vi.mock('jose', () => ({
|
|||
}),
|
||||
}));
|
||||
|
||||
const {
|
||||
mockRecordPrice,
|
||||
mockGetPriceHistory,
|
||||
mockCompareStores,
|
||||
mockGetAnalytics,
|
||||
} = vi.hoisted(() => ({
|
||||
mockRecordPrice: vi.fn(),
|
||||
mockGetPriceHistory: vi.fn(),
|
||||
mockCompareStores: vi.fn(),
|
||||
mockGetAnalytics: vi.fn(),
|
||||
}));
|
||||
const { mockRecordPrice, mockGetPriceHistory, mockCompareStores, mockGetAnalytics } = vi.hoisted(
|
||||
() => ({
|
||||
mockRecordPrice: vi.fn(),
|
||||
mockGetPriceHistory: vi.fn(),
|
||||
mockCompareStores: vi.fn(),
|
||||
mockGetAnalytics: vi.fn(),
|
||||
}),
|
||||
);
|
||||
|
||||
vi.mock('./medicine-prices.repository.js', () => ({
|
||||
MedicinePricesRepository: class {
|
||||
|
|
@ -198,11 +195,13 @@ describe('medicine-prices.routes', () => {
|
|||
});
|
||||
|
||||
it('handles Date objects in response', async () => {
|
||||
mockRecordPrice.mockResolvedValue(makeFakePriceRecord({
|
||||
_id: { toString: () => 'pr-obj' },
|
||||
date: new Date('2026-01-15T00:00:00.000Z'),
|
||||
createdAt: new Date('2026-01-15T00:00:00.000Z'),
|
||||
}));
|
||||
mockRecordPrice.mockResolvedValue(
|
||||
makeFakePriceRecord({
|
||||
_id: { toString: () => 'pr-obj' },
|
||||
date: new Date('2026-01-15T00:00:00.000Z'),
|
||||
createdAt: new Date('2026-01-15T00:00:00.000Z'),
|
||||
}),
|
||||
);
|
||||
|
||||
const res = await app.inject({
|
||||
method: 'POST',
|
||||
|
|
@ -238,7 +237,10 @@ describe('medicine-prices.routes', () => {
|
|||
});
|
||||
|
||||
it('passes query params to service', async () => {
|
||||
mockGetPriceHistory.mockResolvedValue({ data: [], pagination: { cursor: null, hasMore: false } });
|
||||
mockGetPriceHistory.mockResolvedValue({
|
||||
data: [],
|
||||
pagination: { cursor: null, hasMore: false },
|
||||
});
|
||||
|
||||
await app.inject({
|
||||
method: 'GET',
|
||||
|
|
|
|||
|
|
@ -13,8 +13,6 @@ import {
|
|||
} from '@meshitrack/shared';
|
||||
import { MedicinePricesRepository } from './medicine-prices.repository.js';
|
||||
import { MedicinePricesService } from './medicine-prices.service.js';
|
||||
import { MedicineProductsRepository } from '../medicine-products/medicine-products.repository.js';
|
||||
import { StoresRepository } from '../stores/stores.repository.js';
|
||||
|
||||
type AnyPriceDoc = {
|
||||
_id: string | { toString: () => string };
|
||||
|
|
|
|||
|
|
@ -40,7 +40,10 @@ describe(MedicinePricesService.name, () => {
|
|||
};
|
||||
|
||||
it('creates price record with computed pricePerUnit', async () => {
|
||||
mockProductsRepo.findById.mockResolvedValue({ medicineName: 'Acetaminophen', brand: 'Tylenol' });
|
||||
mockProductsRepo.findById.mockResolvedValue({
|
||||
medicineName: 'Acetaminophen',
|
||||
brand: 'Tylenol',
|
||||
});
|
||||
mockStoresRepo.findById.mockResolvedValue({ name: 'Walgreens' });
|
||||
const record = { _id: 'pr-1', pricePerUnit: 0.1 };
|
||||
mockPricesRepo.create.mockResolvedValue(record);
|
||||
|
|
@ -49,7 +52,11 @@ describe(MedicinePricesService.name, () => {
|
|||
|
||||
expect(result).toEqual(record);
|
||||
expect(mockPricesRepo.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ pricePerUnit: 0.1, medicineName: 'Acetaminophen', storeName: 'Walgreens' }),
|
||||
expect.objectContaining({
|
||||
pricePerUnit: 0.1,
|
||||
medicineName: 'Acetaminophen',
|
||||
storeName: 'Walgreens',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -66,11 +73,18 @@ describe(MedicinePricesService.name, () => {
|
|||
});
|
||||
|
||||
it('uses provided date when given', async () => {
|
||||
mockProductsRepo.findById.mockResolvedValue({ medicineName: 'Acetaminophen', brand: 'Tylenol' });
|
||||
mockProductsRepo.findById.mockResolvedValue({
|
||||
medicineName: 'Acetaminophen',
|
||||
brand: 'Tylenol',
|
||||
});
|
||||
mockStoresRepo.findById.mockResolvedValue({ name: 'Walgreens' });
|
||||
mockPricesRepo.create.mockResolvedValue({ _id: 'pr-1' });
|
||||
|
||||
await service.recordPrice({ ...validInput, date: '2026-01-15T00:00:00.000Z' }, 'hh1', 'user-1');
|
||||
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') }),
|
||||
|
|
@ -86,7 +100,10 @@ describe(MedicinePricesService.name, () => {
|
|||
});
|
||||
|
||||
it('throws NotFoundError when store not found', async () => {
|
||||
mockProductsRepo.findById.mockResolvedValue({ medicineName: 'Acetaminophen', brand: 'Tylenol' });
|
||||
mockProductsRepo.findById.mockResolvedValue({
|
||||
medicineName: 'Acetaminophen',
|
||||
brand: 'Tylenol',
|
||||
});
|
||||
mockStoresRepo.findById.mockResolvedValue(null);
|
||||
|
||||
await expect(service.recordPrice(validInput, 'hh1', 'user-1')).rejects.toThrow(
|
||||
|
|
|
|||
|
|
@ -19,7 +19,11 @@ export class MedicinePricesService {
|
|||
private readonly medicineProductsRepository: MedicineProductsRepository;
|
||||
private readonly storesRepository: StoresRepository;
|
||||
|
||||
public constructor({ medicinePricesRepository, medicineProductsRepository, storesRepository }: Deps) {
|
||||
public constructor({
|
||||
medicinePricesRepository,
|
||||
medicineProductsRepository,
|
||||
storesRepository,
|
||||
}: Deps) {
|
||||
this.medicinePricesRepository = medicinePricesRepository;
|
||||
this.medicineProductsRepository = medicineProductsRepository;
|
||||
this.storesRepository = storesRepository;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue