← 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()