import type { PricesRepository } from './prices.repository.js'; import type { ProductsRepository } from '../products/products.repository.js'; import type { StoresRepository } from '../stores/stores.repository.js'; import type { CreatePriceRecordInput, BulkPriceRecordInput, PriceHistoryQueryInput, } from '@meshitrack/shared'; import { NotFoundError } from '../../common/errors.js'; interface Deps { pricesRepository: PricesRepository; productsRepository: ProductsRepository; storesRepository: StoresRepository; } export class PricesService { private readonly pricesRepository: PricesRepository; private readonly productsRepository: ProductsRepository; private readonly storesRepository: StoresRepository; public constructor({ pricesRepository, productsRepository, storesRepository }: Deps) { this.pricesRepository = pricesRepository; this.productsRepository = productsRepository; this.storesRepository = storesRepository; } /** * Validates entity existence, computes pricePerUnit, and persists record */ public async recordPrice( data: CreatePriceRecordInput, householdId: string, userId: string ) { const [product, store] = await Promise.all([ this.productsRepository.findById(data.productId, householdId), this.storesRepository.findById(data.storeId, householdId), ]); if (!product) throw new NotFoundError(`Product not found: ${data.productId}`); if (!store) throw new NotFoundError(`Store not found: ${data.storeId}`); const pricePerUnit = data.quantity > 0 ? data.price / data.quantity : data.price; return this.pricesRepository.create({ householdId, productId: data.productId, productName: product.name as string, storeId: data.storeId, storeName: store.name as string, price: data.price, currency: data.currency, quantity: data.quantity, unit: data.unit, pricePerUnit, date: data.date ? new Date(data.date) : new Date(), receiptImageUrl: data.receiptImageUrl, notes: data.notes, createdBy: userId, }); } /** * Ingests a list of purchased products in a single transaction */ public async recordBulkPrices( data: BulkPriceRecordInput, householdId: string, userId: string ) { const store = await this.storesRepository.findById(data.storeId, householdId); if (!store) throw new NotFoundError(`Store not found: ${data.storeId}`); const recordDate = data.date ? new Date(data.date) : new Date(); const productIds = data.items.map((it) => it.productId); const products = await this.productsRepository.findByIds(householdId, productIds); const productMap = new Map(products.map((p) => [p._id.toString(), p])); const creationPayloads = data.items.map((item) => { const product = productMap.get(item.productId); if (!product) { throw new NotFoundError(`Product not found in catalog: ${item.productId}`); } const pricePerUnit = item.quantity > 0 ? item.price / item.quantity : item.price; return { householdId, productId: item.productId, productName: product.name as string, storeId: data.storeId, storeName: store.name as string, price: item.price, currency: 'USD', // Base fallback or pulled from household settings in future quantity: item.quantity, unit: item.unit, pricePerUnit, date: recordDate, notes: item.notes, createdBy: userId, }; }); return this.pricesRepository.createMany(creationPayloads); } public async getPriceHistory( productId: string, householdId: string, query: PriceHistoryQueryInput ) { return this.pricesRepository.findByProduct(householdId, productId, query); } public async compareStores(productId: string, householdId: string) { return this.pricesRepository.compareStores(householdId, productId); } public async getAnalytics(householdId: string) { return this.pricesRepository.getAnalytics(householdId); } /** * Retrieves the most recent price recorded for this item (optionally restricted to a store) * to enable quick population of estimated basket subtotals on fresh lists. */ public async estimatePrice( productId: string, householdId: string, storeId?: string ): Promise { const latest = await this.pricesRepository.getLatestForProduct(householdId, productId, storeId); if (!latest) { // If a specific store was requested but has no history, fall back to the generic latest across all stores if (storeId) { const genericLatest = await this.pricesRepository.getLatestForProduct(householdId, productId); return genericLatest ? genericLatest.price : null; } return null; } return latest.price; } }