← back to Handbag Auth Nextjs
scripts/improve-deal-detection.js
226 lines
const { PrismaClient } = require('@prisma/client')
const prisma = new PrismaClient()
// Known model number patterns by brand
const MODEL_PATTERNS = {
'HERMÈS': /\b(birkin|kelly|constance|evelyne|garden party|picotin|bolide|lindy)\b/i,
'HERMES': /\b(birkin|kelly|constance|evelyne|garden party|picotin|bolide|lindy)\b/i,
'CHANEL': /\b(classic flap|boy bag|19 bag|22 bag|gabrielle|diana|coco handle|timeless|2\.55|mini square|woc)\b/i,
'LOUIS VUITTON': /\b(neverfull|speedy|alma|keepall|pochette|metis|capucines|twist|coussin|onthego|dauphine|multi pochette)\b/i,
'DIOR': /\b(lady dior|saddle|book tote|30 montaigne|diorissimo|miss dior|diorama)\b/i,
'GUCCI': /\b(marmont|dionysus|jackie|bamboo|soho|ophidia|horsebit|1955|sylvie)\b/i,
'PRADA': /\b(galleria|cahier|sidonie|cleo|odette|re-edition|nylon|saffiano)\b/i,
'BOTTEGA VENETA': /\b(cassette|pouch|jodie|arco|intrecciato|mount|pillow)\b/i,
'BALENCIAGA': /\b(city|classic|velo|work|mini city|first|le dix|triangle|hourglass)\b/i,
'CELINE': /\b(luggage|trapeze|trio|belt bag|phantom|nano|micro|seau|triomphe|16)\b/i,
'YSL': /\b(kate|loulou|niki|sunset|envelope|college|monogram|sac de jour|solferino)\b/i,
'SAINT LAURENT': /\b(kate|loulou|niki|sunset|envelope|college|monogram|sac de jour|solferino)\b/i,
'FENDI': /\b(peekaboo|baguette|mon tresor|sunshine|kan|3jours|by the way|spy)\b/i,
}
// Validate if listing has a real, verifiable model number
function hasVerifiableModel(listing) {
if (!listing.brand || !listing.model) {
return false
}
const brand = listing.brand.toUpperCase().trim()
const model = listing.model.toLowerCase().trim()
const title = (listing.title || '').toLowerCase()
// Check if brand has known models
const pattern = MODEL_PATTERNS[brand]
if (!pattern) {
// Unknown brand - be conservative, check if model is substantive
return listing.model.length >= 3 && !/^(bag|handbag|purse|tote)$/i.test(listing.model)
}
// Check if model or title matches known patterns
return pattern.test(model) || pattern.test(title)
}
// Enhanced deal detection algorithm
async function improveDealDetection() {
console.log('🚀 Starting enhanced deal detection algorithm...\n')
console.log('⚠️ Only processing listings with verifiable model numbers\n')
try {
// Get all active listings
const allListings = await prisma.listing.findMany({
where: { isActive: 1, priceUsd: { not: null } },
include: {
usComparisons: true,
dealAnalysis: true,
priceHistory: {
orderBy: { recordedAt: 'desc' },
take: 30,
},
},
orderBy: { id: 'asc' },
})
console.log(`📊 Found ${allListings.length} total listings`)
// Filter for verifiable models only
const listings = allListings.filter(hasVerifiableModel)
const skipped = allListings.length - listings.length
console.log(`✅ Processing ${listings.length} listings with verifiable models`)
console.log(`❌ Skipping ${skipped} listings without verifiable models\n`)
let updated = 0
let newDeals = 0
for (const listing of listings) {
// Calculate historical price stats
const priceHistory = listing.priceHistory
const historicalPrices = priceHistory.map(h => h.priceUsd).filter(p => p)
const avgHistoricalPrice = historicalPrices.length > 0
? historicalPrices.reduce((a, b) => a + b, 0) / historicalPrices.length
: listing.priceUsd
const minHistoricalPrice = historicalPrices.length > 0
? Math.min(...historicalPrices)
: listing.priceUsd
const maxHistoricalPrice = historicalPrices.length > 0
? Math.max(...historicalPrices)
: listing.priceUsd
// Calculate US comparison stats
const usComparisons = listing.usComparisons.filter(c => c.usPriceUsd)
const usPrices = usComparisons.map(c => c.usPriceUsd)
const avgUsPrice = usPrices.length > 0
? usPrices.reduce((a, b) => a + b, 0) / usPrices.length
: null
const minUsPrice = usPrices.length > 0 ? Math.min(...usPrices) : null
const maxUsPrice = usPrices.length > 0 ? Math.max(...usPrices) : null
// Calculate deal percentage (multiple factors)
let dealPercentage = 0
let confidenceScore = 0
const factors = []
// Factor 1: Comparison with US prices
if (avgUsPrice && avgUsPrice > 0) {
const usDealPercent = ((avgUsPrice - listing.priceUsd) / avgUsPrice) * 100
dealPercentage += usDealPercent * 0.6 // 60% weight
confidenceScore += 40
factors.push(`US comparison: ${usDealPercent.toFixed(1)}%`)
}
// Factor 2: Historical price trend
if (avgHistoricalPrice && avgHistoricalPrice > 0) {
const histDealPercent = ((avgHistoricalPrice - listing.priceUsd) / avgHistoricalPrice) * 100
dealPercentage += histDealPercent * 0.2 // 20% weight
confidenceScore += 30
factors.push(`Historical avg: ${histDealPercent.toFixed(1)}%`)
}
// Factor 3: Price volatility (lower volatility = more confident)
if (historicalPrices.length >= 5) {
const priceRange = maxHistoricalPrice - minHistoricalPrice
const volatility = (priceRange / avgHistoricalPrice) * 100
if (volatility < 15) {
confidenceScore += 20 // Low volatility = more confident
factors.push(`Low volatility: ${volatility.toFixed(1)}%`)
}
}
// Factor 4: Brand premium brands get confidence boost
const premiumBrands = ['HERMES', 'HERMÈS', 'CHANEL', 'LOUIS VUITTON', 'DIOR']
if (premiumBrands.includes(listing.brand?.toUpperCase())) {
confidenceScore += 10
factors.push('Premium brand')
}
// Normalize confidence score (0-100)
confidenceScore = Math.min(confidenceScore, 100)
// Determine if it's a deal (>15% savings with >40% confidence)
const isDeal = dealPercentage >= 15 && confidenceScore >= 40 ? 1 : 0
// Generate AI notes
const aiNotes = [
`Deal Score: ${dealPercentage.toFixed(1)}% | Confidence: ${confidenceScore}%`,
...factors,
usComparisons.length > 0 ? `Based on ${usComparisons.length} US comparisons` : 'No US comparisons',
historicalPrices.length > 0 ? `${historicalPrices.length} historical data points` : 'No historical data',
].join(' • ')
const analysisDate = new Date().toISOString().replace('T', ' ').split('.')[0]
// Upsert deal analysis
await prisma.dealAnalysis.upsert({
where: { listingId: listing.id },
create: {
listingId: listing.id,
avgUsPrice,
minUsPrice,
maxUsPrice,
dealPercentage: Math.round(dealPercentage * 100) / 100,
confidenceScore: Math.round(confidenceScore),
isDeal,
analysisDate,
aiNotes,
},
update: {
avgUsPrice,
minUsPrice,
maxUsPrice,
dealPercentage: Math.round(dealPercentage * 100) / 100,
confidenceScore: Math.round(confidenceScore),
isDeal,
analysisDate,
aiNotes,
},
})
updated++
if (isDeal && !listing.dealAnalysis?.isDeal) {
newDeals++
console.log(`🎯 NEW DEAL: ${listing.brand} ${listing.model} - ${dealPercentage.toFixed(1)}% off (ID: ${listing.id})`)
}
}
console.log(`\n✅ Deal detection complete!`)
console.log(` 📝 Analyzed: ${listings.length} verified listings`)
console.log(` ⏭️ Skipped: ${skipped} unverifiable listings`)
console.log(` 💾 Updated: ${updated} analyses`)
console.log(` 🎉 New deals found: ${newDeals}`)
// Get summary stats
const totalDeals = await prisma.dealAnalysis.count({
where: { isDeal: 1 },
})
const avgDealPercent = await prisma.dealAnalysis.aggregate({
where: { isDeal: 1 },
_avg: { dealPercentage: true },
})
console.log(` 💰 Total deals in database: ${totalDeals}`)
console.log(` 📊 Average deal percentage: ${avgDealPercent._avg.dealPercentage?.toFixed(1)}%`)
// Show some examples of skipped items
const skippedSamples = allListings.filter(l => !hasVerifiableModel(l)).slice(0, 5)
if (skippedSamples.length > 0) {
console.log(`\n📋 Examples of skipped items (no verifiable model):`)
skippedSamples.forEach(l => {
console.log(` • ${l.brand || 'No Brand'} - "${l.model || 'No Model'}" - ${l.title?.substring(0, 60)}...`)
})
}
} catch (error) {
console.error('❌ Error in deal detection:', error)
} finally {
await prisma.$disconnect()
}
}
improveDealDetection()