Phase 5 cleanup

This commit is contained in:
Aerilyn Weber 2026-04-26 18:44:59 +09:00
parent 5536acd67d
commit 76a516a417
136 changed files with 6322 additions and 1985 deletions

View file

@ -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' });

View file

@ -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;
}[],
};
}
}

View file

@ -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',

View file

@ -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 };

View file

@ -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(

View file

@ -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;