← back to Handbag Authentication

api/ebay-sold-data.js

231 lines

/**
 * eBay Sold Data Fetcher
 * Fetches actual sold/completed listings from eBay USA and eBay Japan
 */

const axios = require('axios');
const cheerio = require('cheerio');

class EbaySoldDataFetcher {
  constructor() {
    // eBay Finding API credentials (if available)
    this.ebayAppId = process.env.EBAY_APP_ID || null;
  }

  /**
   * Fetch sold listings from eBay USA
   */
  async fetchEbayUSASold(brand, model, limit = 10) {
    try {
      // Method 1: Use eBay Finding API if credentials available
      if (this.ebayAppId) {
        return await this.fetchViaAPI(brand, model, 'EBAY-US', limit);
      }

      // Method 2: Scrape sold listings page
      const searchQuery = `${brand} ${model} bag`;
      const url = `https://www.ebay.com/sch/i.html?_nkw=${encodeURIComponent(searchQuery)}&LH_Sold=1&LH_Complete=1&_sop=13`; // Sort by recently sold

      console.log(`Fetching eBay USA sold data: ${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 = [];

      $('.s-item').each((i, elem) => {
        if (i >= limit) return false;

        const title = $(elem).find('.s-item__title').text().trim();
        const priceText = $(elem).find('.s-item__price').text().trim();
        const link = $(elem).find('.s-item__link').attr('href');
        const soldDate = $(elem).find('.s-item__title--tag').text().trim();
        const image = $(elem).find('.s-item__image-img').attr('src');

        // Parse price
        const priceMatch = priceText.match(/\$([0-9,]+\.?\d*)/);
        const price = priceMatch ? parseFloat(priceMatch[1].replace(/,/g, '')) : null;

        if (title && price && title.toLowerCase() !== 'shop on ebay') {
          soldItems.push({
            platform: 'eBay USA',
            title,
            price,
            currency: 'USD',
            soldDate,
            url: link,
            image,
            source: 'ebay_usa'
          });
        }
      });

      console.log(`✓ Found ${soldItems.length} eBay USA sold items`);
      return soldItems;

    } catch (error) {
      console.error('Error fetching eBay USA sold data:', error.message);
      return [];
    }
  }

  /**
   * Fetch sold listings from eBay Japan
   */
  async fetchEbayJapanSold(brand, model, limit = 10) {
    try {
      const searchQuery = `${brand} ${model} バッグ`;
      const url = `https://www.ebay.com/sch/i.html?_nkw=${encodeURIComponent(searchQuery)}&LH_Sold=1&LH_Complete=1&LH_PrefLoc=2&_sop=13`; // LH_PrefLoc=2 = Japan

      console.log(`Fetching eBay Japan sold data: ${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 = [];

      $('.s-item').each((i, elem) => {
        if (i >= limit) return false;

        const title = $(elem).find('.s-item__title').text().trim();
        const priceText = $(elem).find('.s-item__price').text().trim();
        const link = $(elem).find('.s-item__link').attr('href');
        const soldDate = $(elem).find('.s-item__title--tag').text().trim();
        const image = $(elem).find('.s-item__image-img').attr('src');

        // Parse price (could be USD or JPY)
        let price = null;
        let currency = 'USD';

        const usdMatch = priceText.match(/\$([0-9,]+\.?\d*)/);
        const jpyMatch = priceText.match(/¥([0-9,]+)/);

        if (usdMatch) {
          price = parseFloat(usdMatch[1].replace(/,/g, ''));
          currency = 'USD';
        } else if (jpyMatch) {
          price = parseFloat(jpyMatch[1].replace(/,/g, ''));
          currency = 'JPY';
        }

        if (title && price && title.toLowerCase() !== 'shop on ebay') {
          soldItems.push({
            platform: 'eBay Japan',
            title,
            price,
            currency,
            soldDate,
            url: link,
            image,
            source: 'ebay_japan'
          });
        }
      });

      console.log(`✓ Found ${soldItems.length} eBay Japan sold items`);
      return soldItems;

    } catch (error) {
      console.error('Error fetching eBay Japan sold data:', error.message);
      return [];
    }
  }

  /**
   * Fetch via eBay Finding API (if credentials available)
   */
  async fetchViaAPI(brand, model, site = 'EBAY-US', limit = 10) {
    try {
      const keywords = `${brand} ${model} bag`;
      const url = `https://svcs.ebay.com/services/search/FindingService/v1?OPERATION-NAME=findCompletedItems&SERVICE-VERSION=1.0.0&SECURITY-APPNAME=${this.ebayAppId}&RESPONSE-DATA-FORMAT=JSON&keywords=${encodeURIComponent(keywords)}&GLOBAL-ID=${site}&paginationInput.entriesPerPage=${limit}&sortOrder=EndTimeSoonest&itemFilter(0).name=SoldItemsOnly&itemFilter(0).value=true`;

      const response = await axios.get(url, { timeout: 15000 });
      const items = response.data.findCompletedItemsResponse?.[0]?.searchResult?.[0]?.item || [];

      return items.map(item => ({
        platform: site === 'EBAY-US' ? 'eBay USA' : 'eBay Japan',
        title: item.title?.[0] || '',
        price: parseFloat(item.sellingStatus?.[0]?.currentPrice?.[0]?.__value__ || 0),
        currency: item.sellingStatus?.[0]?.currentPrice?.[0]?.['@currencyId'] || 'USD',
        soldDate: item.listingInfo?.[0]?.endTime?.[0] || '',
        url: item.viewItemURL?.[0] || '',
        image: item.galleryURL?.[0] || '',
        source: site === 'EBAY-US' ? 'ebay_usa' : 'ebay_japan'
      }));

    } catch (error) {
      console.error('Error fetching via eBay API:', error.message);
      return [];
    }
  }

  /**
   * Fetch from all sources and combine
   */
  async fetchAllSoldData(brand, model) {
    console.log(`\n🔍 Fetching sold data for: ${brand} ${model}`);

    const [ebayUSA, ebayJapan] = await Promise.all([
      this.fetchEbayUSASold(brand, model, 10),
      this.fetchEbayJapanSold(brand, model, 10)
    ]);

    const allSold = [...ebayUSA, ...ebayJapan];

    // 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 items found: ${allSold.length}`);
    return allSold;
  }

  /**
   * Calculate statistics from sold data
   */
  calculateStats(soldItems) {
    if (soldItems.length === 0) {
      return {
        count: 0,
        avgPrice: 0,
        highPrice: 0,
        lowPrice: 0,
        medianPrice: 0
      };
    }

    // Convert all to USD
    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 = EbaySoldDataFetcher;