import scanRepository from '../repositories/scanRepository'; import licenseRepository from '../repositories/licenseRepository'; import aiEngine from './aiEngine'; import { BadRequestError, ForbiddenError, NotFoundError } from '../utils/helpers'; import { Marketplace, ScanStatus } from '@prisma/client'; interface ScanProductInput { productName: string; priceRaw?: string; priceNumeric: number; rating?: number; soldCount?: number; storeName?: string; productUrl?: string; } export class ScanService { async createScan( userId: string, marketplace: Marketplace, keyword: string | undefined, category: string | undefined, products: ScanProductInput[] ) { if (!products || products.length === 0) { throw new BadRequestError('No products to scan'); } // Check license limits const license = await licenseRepository.findActiveByUserId(userId); if (!license) { throw new ForbiddenError('No active license found'); } if (license.maxScan > 0 && products.length > license.maxScan) { throw new ForbiddenError( `Your plan allows max ${license.maxScan} products per scan. You sent ${products.length}.` ); } // Calculate statistics const prices = products.map((p) => p.priceNumeric).filter((p) => p > 0); const stats = aiEngine.calculateStats(prices); if (!stats) { throw new BadRequestError('Unable to process price data'); } // K-means clustering const clusters = aiEngine.kMeansClustering(prices, 3); // Detect outliers const outlierPrices = new Set( prices.filter( (p) => p < stats.outlierBounds.lower || p > stats.outlierBounds.upper ) ); // AI Recommendation const ratings = products.map((p) => p.rating || 0).filter((r) => r > 0); const avgRating = ratings.length > 0 ? ratings.reduce((a, b) => a + b, 0) / ratings.length : 0; const totalSold = products.reduce((sum, p) => sum + (p.soldCount || 0), 0); const ratingDist: Record = {}; ratings.forEach((r) => { const key = Math.floor(r).toString(); ratingDist[key] = (ratingDist[key] || 0) + 1; }); let aiRecommendation = null; if (license.plan !== 'FREE') { aiRecommendation = aiEngine.recommend({ avgPrice: stats.avg, medianPrice: stats.median, minPrice: stats.min, maxPrice: stats.max, stdDeviation: stats.stdDeviation, top10Lowest: stats.top10Lowest, top10Highest: stats.top10Highest, totalProducts: products.length, avgRating, totalSold, ratingDistribution: ratingDist, }); } // Create scan record const scan = await scanRepository.create({ marketplace, keyword, category, totalProducts: products.length, minPrice: stats.min, maxPrice: stats.max, avgPrice: stats.avg, medianPrice: stats.median, stdDeviation: stats.stdDeviation, status: ScanStatus.COMPLETED, aiRecommendation: aiRecommendation as any, user: { connect: { id: userId } }, }); // Create scan items const scanItems = products.map((p, idx) => { const clusterIndex = clusters.findIndex((c) => c.items.includes(p.priceNumeric) ); return { productName: p.productName, priceRaw: p.priceRaw, priceNumeric: p.priceNumeric, rating: p.rating, soldCount: p.soldCount, storeName: p.storeName, productUrl: p.productUrl, isOutlier: outlierPrices.has(p.priceNumeric), cluster: clusterIndex >= 0 ? clusterIndex : null, }; }); await scanRepository.createItems(scan.id, scanItems); return { scan: { id: scan.id, marketplace: scan.marketplace, keyword: scan.keyword, category: scan.category, totalProducts: scan.totalProducts, stats: { min: stats.min, max: stats.max, avg: stats.avg, median: stats.median, stdDeviation: stats.stdDeviation, q1: stats.q1, q3: stats.q3, outlierCount: stats.outlierCount, }, clusters: clusters.map((c) => ({ centroid: c.centroid, count: c.items.length, min: Math.min(...c.items), max: Math.max(...c.items), })), aiRecommendation, top10Cheapest: stats.top10Lowest, top10Expensive: stats.top10Highest, createdAt: scan.createdAt, }, }; } async getScanHistory( userId: string, page: number, limit: number, filters?: { marketplace?: Marketplace; keyword?: string; startDate?: string; endDate?: string; } ) { const parsedFilters = { marketplace: filters?.marketplace, keyword: filters?.keyword, startDate: filters?.startDate ? new Date(filters.startDate) : undefined, endDate: filters?.endDate ? new Date(filters.endDate) : undefined, }; return scanRepository.findByUserId(userId, page, limit, parsedFilters); } async getScanDetail(userId: string, scanId: string) { const scan = await scanRepository.findById(scanId); if (!scan) { throw new NotFoundError('Scan not found'); } if (scan.userId !== userId) { throw new ForbiddenError('Access denied'); } return scan; } async getUserScanCount(userId: string) { return scanRepository.countByUser(userId); } } export default new ScanService();