← back to Handbag Authentication
api/sold-comps-aggregator.js
198 lines
/**
* Sold Comps Aggregator
* Aggregates sold data from multiple sources:
* - eBay USA
* - eBay Japan
* - Fashionphile (scraped)
* - Rebag Clair
* - Poshmark
*/
const EbaySoldDataFetcher = require('./ebay-sold-data');
const axios = require('axios');
const cheerio = require('cheerio');
class SoldCompsAggregator {
constructor() {
this.ebayFetcher = new EbaySoldDataFetcher();
}
/**
* Fetch Fashionphile sold items
*/
async fetchFashionphileSold(brand, model, limit = 5) {
try {
const searchQuery = `${brand} ${model}`;
const url = `https://www.fashionphile.com/search?query=${encodeURIComponent(searchQuery)}&sold=true`;
console.log(`Fetching Fashionphile sold: ${url}`);
const response = await axios.get(url, {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
},
timeout: 15000
});
const $ = cheerio.load(response.data);
const soldItems = [];
// Parse Fashionphile's sold items (adjust selectors as needed)
$('.product-item, .product-card').slice(0, limit).each((i, elem) => {
const title = $(elem).find('.product-title, .product-name').text().trim();
const priceText = $(elem).find('.product-price, .price').text().trim();
const link = $(elem).find('a').attr('href');
const image = $(elem).find('img').attr('src');
const priceMatch = priceText.match(/\$([0-9,]+)/);
const price = priceMatch ? parseFloat(priceMatch[1].replace(/,/g, '')) : null;
if (title && price) {
soldItems.push({
platform: 'Fashionphile',
title,
price,
currency: 'USD',
soldDate: 'Recently',
url: link ? `https://www.fashionphile.com${link}` : url,
image,
source: 'fashionphile'
});
}
});
console.log(`✓ Found ${soldItems.length} Fashionphile sold items`);
return soldItems;
} catch (error) {
console.error('Error fetching Fashionphile:', error.message);
return [];
}
}
/**
* Fetch Poshmark sold items
*/
async fetchPoshmarkSold(brand, model, limit = 5) {
try {
const searchQuery = `${brand} ${model}`;
const url = `https://poshmark.com/search?query=${encodeURIComponent(searchQuery)}&department=Women&availability=sold_out`;
console.log(`Fetching Poshmark sold: ${url}`);
const response = await axios.get(url, {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
},
timeout: 15000
});
const $ = cheerio.load(response.data);
const soldItems = [];
$('.tile').slice(0, limit).each((i, elem) => {
const title = $(elem).find('.tile__title').text().trim();
const priceText = $(elem).find('.tile__price').text().trim();
const link = $(elem).find('a').attr('href');
const image = $(elem).find('img').attr('src');
const priceMatch = priceText.match(/\$([0-9,]+)/);
const price = priceMatch ? parseFloat(priceMatch[1].replace(/,/g, '')) : null;
if (title && price) {
soldItems.push({
platform: 'Poshmark',
title,
price,
currency: 'USD',
soldDate: 'Sold',
url: link ? `https://poshmark.com${link}` : url,
image,
source: 'poshmark'
});
}
});
console.log(`✓ Found ${soldItems.length} Poshmark sold items`);
return soldItems;
} catch (error) {
console.error('Error fetching Poshmark:', error.message);
return [];
}
}
/**
* Aggregate all sold comps from all sources
*/
async aggregateAllSoldComps(brand, model) {
console.log(`\n📊 Aggregating sold comps for: ${brand} ${model}`);
const results = await Promise.allSettled([
this.ebayFetcher.fetchEbayUSASold(brand, model, 10),
this.ebayFetcher.fetchEbayJapanSold(brand, model, 10),
this.fetchFashionphileSold(brand, model, 5),
this.fetchPoshmarkSold(brand, model, 5)
]);
// Combine all successful results
const allSold = results
.filter(r => r.status === 'fulfilled')
.flatMap(r => r.value);
// Sort by price (high to low)
allSold.sort((a, b) => {
const priceA = a.currency === 'JPY' ? a.price / 150 : a.price;
const priceB = b.currency === 'JPY' ? b.price / 150 : b.price;
return priceB - priceA;
});
console.log(`✅ Total sold comps: ${allSold.length}`);
// Calculate stats
const stats = this.calculateStats(allSold);
return {
soldItems: allSold,
stats,
sources: {
ebayUSA: allSold.filter(i => i.source === 'ebay_usa').length,
ebayJapan: allSold.filter(i => i.source === 'ebay_japan').length,
fashionphile: allSold.filter(i => i.source === 'fashionphile').length,
poshmark: allSold.filter(i => i.source === 'poshmark').length
}
};
}
calculateStats(soldItems) {
if (soldItems.length === 0) {
return {
count: 0,
avgPrice: 0,
highPrice: 0,
lowPrice: 0,
medianPrice: 0
};
}
const pricesUSD = soldItems.map(item =>
item.currency === 'JPY' ? item.price / 150 : item.price
);
pricesUSD.sort((a, b) => a - b);
const sum = pricesUSD.reduce((a, b) => a + b, 0);
const median = pricesUSD[Math.floor(pricesUSD.length / 2)];
return {
count: soldItems.length,
avgPrice: Math.round(sum / pricesUSD.length),
highPrice: Math.round(Math.max(...pricesUSD)),
lowPrice: Math.round(Math.min(...pricesUSD)),
medianPrice: Math.round(median)
};
}
}
module.exports = SoldCompsAggregator;