← back to Handbag Authentication

ai/analyzer.js

206 lines

require('dotenv').config();
const OpenAI = require('openai');
const Database = require('../db/schema');
const RetailPriceDatabase = require('../api/retail-price-database');

class PriceAnalyzer {
  constructor() {
    this.openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
    this.db = new Database(process.env.DATABASE_PATH);
    this.retailPriceDB = new RetailPriceDatabase();
    this.dealThreshold = parseFloat(process.env.DEAL_THRESHOLD) || 20;
  }

  async analyzeListings() {
    console.log('=== Starting AI Price Analysis ===\n');

    // Get all active listings without analysis
    this.db.db.all(`
      SELECT l.*
      FROM listings l
      LEFT JOIN deal_analysis d ON l.id = d.listing_id
      WHERE l.is_active = 1
        AND (d.id IS NULL OR d.analysis_date < datetime('now', '-1 day'))
      LIMIT 50
    `, async (err, listings) => {
      if (err) {
        console.error('Error fetching listings:', err);
        return;
      }

      console.log(`Analyzing ${listings.length} listings...`);

      for (const listing of listings) {
        await this.analyzeListing(listing);
        await new Promise(resolve => setTimeout(resolve, 1000)); // Rate limit
      }

      console.log('Analysis complete!');
      this.db.close();
    });
  }

  async analyzeListing(listing) {
    try {
      console.log(`Analyzing: ${listing.title}`);

      // STEP 1: Extract detailed product info using AI
      const extractedDetails = await this.extractProductDetails(listing);

      if (extractedDetails) {
        // Update listing with extracted details
        this.db.db.run(`
          UPDATE listings
          SET model = ?, size = ?, color = ?, material = ?
          WHERE id = ?
        `, [
          extractedDetails.model,
          extractedDetails.size,
          extractedDetails.color,
          extractedDetails.material,
          listing.id
        ]);

        // Update in-memory listing object
        listing.model = extractedDetails.model;
        listing.size = extractedDetails.size;
        listing.color = extractedDetails.color;
        listing.material = extractedDetails.material;
      }

      // STEP 2: Look up original retail price
      const retailPriceInfo = this.retailPriceDB.findRetailPrice(listing.title, listing.brand);

      // STEP 3: Use AI to estimate US market price
      const usMarketData = await this.getUSMarketEstimate(listing, retailPriceInfo);

      if (!usMarketData || !usMarketData.avg_price) {
        console.log('  -> Could not estimate US price');
        return;
      }

      // Calculate deal percentage
      const dealPercentage = ((usMarketData.avg_price - listing.price_usd) / usMarketData.avg_price) * 100;
      const isDeal = dealPercentage >= this.dealThreshold;

      // Calculate retail appreciation if we have MSRP
      let retailAppreciation = null;
      if (retailPriceInfo) {
        retailAppreciation = this.retailPriceDB.calculateValueChange(
          retailPriceInfo.msrp,
          usMarketData.avg_price
        );
      }

      const analysis = {
        listing_id: listing.id,
        avg_us_price: usMarketData.avg_price,
        min_us_price: usMarketData.min_price,
        max_us_price: usMarketData.max_price,
        retail_msrp: retailPriceInfo ? retailPriceInfo.msrp : null,
        retail_year: retailPriceInfo ? retailPriceInfo.year : null,
        appreciation_percent: retailAppreciation ? retailAppreciation.changePercent : null,
        deal_percentage: Math.round(dealPercentage * 10) / 10,
        confidence_score: usMarketData.confidence,
        is_deal: isDeal ? 1 : 0,
        ai_notes: usMarketData.notes + (retailAppreciation ? ` | ${retailAppreciation.status} ${Math.abs(retailAppreciation.changePercent)}% vs retail` : '')
      };

      // Save to database
      this.db.saveDealAnalysis(analysis, (err) => {
        if (err) {
          console.error('Error saving analysis:', err);
        } else {
          console.log(`  -> ${isDeal ? '🔥 DEAL' : 'Normal'}: ${dealPercentage.toFixed(1)}% below market`);
        }
      });

      // Save US comparisons if available
      if (usMarketData.comparisons) {
        usMarketData.comparisons.forEach(comp => {
          this.db.saveUSComparison({
            listing_id: listing.id,
            ...comp
          }, () => {});
        });
      }

    } catch (error) {
      console.error(`Error analyzing listing ${listing.id}:`, error.message);
    }
  }

  async extractProductDetails(listing) {
    // Simple extraction - just return null to skip for now
    return null;
  }

  async getUSMarketEstimate(listing) {
    try {
      const prompt = `You are a luxury handbag pricing expert. Analyze this Japanese listing and estimate its current market value in USD on US platforms (eBay, The RealReal, Vestiaire Collective, Fashionphile).

Japanese Listing:
- Title: ${listing.title}
- Brand: ${listing.brand}
- Price: ¥${listing.price_jpy} (${listing.price_usd} USD)
- Condition: ${listing.condition}
- Source: ${listing.source}

Provide a JSON response with:
1. avg_price: Average US market price in USD
2. min_price: Minimum reasonable US price
3. max_price: Maximum reasonable US price
4. confidence: Confidence score 0-1
5. notes: Brief reasoning (1-2 sentences)
6. comparisons: Array of {us_source, us_price_usd, us_condition} for 2-3 comparable US listings

Focus on actual market data. Consider brand, condition, and current trends.`;

      const response = await this.openai.chat.completions.create({
        model: 'gpt-4o-mini',
        messages: [
          {
            role: 'system',
            content: 'You are a luxury handbag pricing expert. Always respond with valid JSON only, no markdown.'
          },
          { role: 'user', content: prompt }
        ],
        temperature: 0.3,
        max_tokens: 500
      });

      const content = response.choices[0].message.content.trim();

      // Remove markdown code blocks if present
      const jsonContent = content.replace(/```json\n?/g, '').replace(/```\n?/g, '');

      const data = JSON.parse(jsonContent);

      return {
        avg_price: data.avg_price || 0,
        min_price: data.min_price || 0,
        max_price: data.max_price || 0,
        confidence: data.confidence || 0.5,
        notes: data.notes || '',
        comparisons: data.comparisons || []
      };

    } catch (error) {
      console.error('Error getting AI estimate:', error.message);
      return null;
    }
  }

  close() {
    this.db.close();
  }
}

// Run if called directly
if (require.main === module) {
  const analyzer = new PriceAnalyzer();
  analyzer.analyzeListings();
}

module.exports = PriceAnalyzer;