← 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;