← back to Handbag Authentication

api/retail-price-database.js

251 lines

/**
 * Retail Price Database
 * Scrapes and maintains original MSRP for luxury handbags
 * Sources: Fashionphile archives, PurseBlog, official brand sites
 */

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

class RetailPriceDatabase {
  constructor() {
    // Hardcoded database of known retail prices (continuously updated)
    this.knownRetailPrices = {
      // HERMÈS (USD retail prices)
      'Hermes Birkin 25': { msrp: 10400, year: 2024, material: 'Togo leather' },
      'Hermes Birkin 30': { msrp: 11200, year: 2024, material: 'Togo leather' },
      'Hermes Birkin 35': { msrp: 12100, year: 2024, material: 'Togo leather' },
      'Hermes Kelly 25': { msrp: 10200, year: 2024, material: 'Epsom leather' },
      'Hermes Kelly 28': { msrp: 10600, year: 2024, material: 'Epsom leather' },
      'Hermes Kelly 32': { msrp: 11400, year: 2024, material: 'Togo leather' },
      'Hermes Constance 18': { msrp: 8500, year: 2024, material: 'Epsom leather' },
      'Hermes Constance 24': { msrp: 9200, year: 2024, material: 'Epsom leather' },
      'Hermes Picotin 18': { msrp: 3200, year: 2024, material: 'Clemence leather' },
      'Hermes Picotin 22': { msrp: 3600, year: 2024, material: 'Clemence leather' },
      'Hermes Evelyne 16': { msrp: 3100, year: 2024, material: 'Clemence leather' },
      'Hermes Evelyne 29': { msrp: 3700, year: 2024, material: 'Clemence leather' },
      'Hermes Evelyne 33': { msrp: 4100, year: 2024, material: 'Clemence leather' },
      'Hermes Garden Party 30': { msrp: 3400, year: 2024, material: 'Negonda leather' },
      'Hermes Garden Party 36': { msrp: 3800, year: 2024, material: 'Negonda leather' },
      'Hermes Bolide 27': { msrp: 8200, year: 2024, material: 'Epsom leather' },
      'Hermes Bolide 31': { msrp: 8800, year: 2024, material: 'Epsom leather' },

      // CHANEL (USD retail prices)
      'Chanel Classic Flap Small': { msrp: 9500, year: 2024, material: 'Lambskin' },
      'Chanel Classic Flap Medium': { msrp: 10200, year: 2024, material: 'Lambskin' },
      'Chanel Classic Flap Jumbo': { msrp: 11100, year: 2024, material: 'Lambskin' },
      'Chanel Boy Bag Small': { msrp: 6200, year: 2024, material: 'Calfskin' },
      'Chanel Boy Bag Medium': { msrp: 7400, year: 2024, material: 'Calfskin' },
      'Chanel Boy Bag Large': { msrp: 7900, year: 2024, material: 'Calfskin' },
      'Chanel 19 Small': { msrp: 6800, year: 2024, material: 'Lambskin' },
      'Chanel 19 Medium': { msrp: 7900, year: 2024, material: 'Lambskin' },
      'Chanel 19 Large': { msrp: 8700, year: 2024, material: 'Lambskin' },
      'Chanel Gabrielle Small': { msrp: 4200, year: 2024, material: 'Calfskin' },
      'Chanel Gabrielle Medium': { msrp: 4700, year: 2024, material: 'Calfskin' },
      'Chanel Gabrielle Large': { msrp: 5200, year: 2024, material: 'Calfskin' },
      'Chanel Deauville Tote': { msrp: 3800, year: 2024, material: 'Canvas' },
      'Chanel GST Grand Shopping Tote': { msrp: 3200, year: 2023, material: 'Caviar', discontinued: true },

      // LOUIS VUITTON (USD retail prices)
      'Louis Vuitton Speedy 25': { msrp: 1760, year: 2024, material: 'Monogram canvas' },
      'Louis Vuitton Speedy 30': { msrp: 1900, year: 2024, material: 'Monogram canvas' },
      'Louis Vuitton Speedy 35': { msrp: 2040, year: 2024, material: 'Monogram canvas' },
      'Louis Vuitton Neverfull MM': { msrp: 2030, year: 2024, material: 'Monogram canvas' },
      'Louis Vuitton Neverfull GM': { msrp: 2170, year: 2024, material: 'Monogram canvas' },
      'Louis Vuitton Alma BB': { msrp: 2100, year: 2024, material: 'Epi leather' },
      'Louis Vuitton Alma PM': { msrp: 2300, year: 2024, material: 'Epi leather' },
      'Louis Vuitton Pochette Metis': { msrp: 2840, year: 2024, material: 'Monogram canvas' },
      'Louis Vuitton Capucines BB': { msrp: 6300, year: 2024, material: 'Taurillon leather' },
      'Louis Vuitton Capucines MM': { msrp: 7050, year: 2024, material: 'Taurillon leather' },
      'Louis Vuitton Onthego MM': { msrp: 3100, year: 2024, material: 'Monogram canvas' },
      'Louis Vuitton Onthego GM': { msrp: 3350, year: 2024, material: 'Monogram canvas' },

      // GUCCI (USD retail prices)
      'Gucci Marmont Small': { msrp: 2490, year: 2024, material: 'Matelasse leather' },
      'Gucci Marmont Medium': { msrp: 2980, year: 2024, material: 'Matelasse leather' },
      'Gucci Marmont Large': { msrp: 3490, year: 2024, material: 'Matelasse leather' },
      'Gucci Dionysus Small': { msrp: 2850, year: 2024, material: 'Suede' },
      'Gucci Dionysus Medium': { msrp: 3200, year: 2024, material: 'Suede' },
      'Gucci Jackie 1961 Small': { msrp: 3100, year: 2024, material: 'Leather' },
      'Gucci Jackie 1961 Medium': { msrp: 3600, year: 2024, material: 'Leather' },
      'Gucci Ophidia GG Small': { msrp: 1850, year: 2024, material: 'GG Supreme canvas' },
      'Gucci Ophidia GG Medium': { msrp: 2300, year: 2024, material: 'GG Supreme canvas' },

      // DIOR (USD retail prices)
      'Dior Lady Dior Small': { msrp: 5800, year: 2024, material: 'Lambskin' },
      'Dior Lady Dior Medium': { msrp: 6500, year: 2024, material: 'Lambskin' },
      'Dior Lady Dior Large': { msrp: 7200, year: 2024, material: 'Lambskin' },
      'Dior Saddle Bag': { msrp: 3900, year: 2024, material: 'Oblique canvas' },
      'Dior Book Tote Small': { msrp: 3500, year: 2024, material: 'Embroidered canvas' },
      'Dior Book Tote Medium': { msrp: 3900, year: 2024, material: 'Embroidered canvas' },
      'Dior Book Tote Large': { msrp: 4300, year: 2024, material: 'Embroidered canvas' },

      // PRADA (USD retail prices)
      'Prada Galleria Small': { msrp: 3150, year: 2024, material: 'Saffiano leather' },
      'Prada Galleria Medium': { msrp: 3750, year: 2024, material: 'Saffiano leather' },
      'Prada Galleria Large': { msrp: 4200, year: 2024, material: 'Saffiano leather' },
      'Prada Re-Edition 2005': { msrp: 1350, year: 2024, material: 'Nylon' },
      'Prada Cleo': { msrp: 2950, year: 2024, material: 'Brushed leather' },
      'Prada Cahier': { msrp: 3200, year: 2024, material: 'Calf leather' },

      // FENDI (USD retail prices)
      'Fendi Baguette': { msrp: 3590, year: 2024, material: 'Leather' },
      'Fendi Peekaboo Small': { msrp: 4790, year: 2024, material: 'Leather' },
      'Fendi Peekaboo Medium': { msrp: 5490, year: 2024, material: 'Leather' },
      'Fendi First Small': { msrp: 2490, year: 2024, material: 'Leather' },
      'Fendi First Medium': { msrp: 2990, year: 2024, material: 'Leather' }
    };
  }

  /**
   * Find retail price for a bag based on title/description
   */
  findRetailPrice(title, brand = null) {
    const titleLower = title.toLowerCase();

    // Try exact matches first
    for (const [key, data] of Object.entries(this.knownRetailPrices)) {
      if (titleLower.includes(key.toLowerCase())) {
        return {
          ...data,
          modelName: key,
          confidence: 'high'
        };
      }
    }

    // Try partial matches by extracting key terms
    const models = [
      'birkin', 'kelly', 'constance', 'picotin', 'evelyne', 'garden party', 'bolide',
      'classic flap', 'boy bag', 'chanel 19', 'gabrielle', 'deauville',
      'speedy', 'neverfull', 'alma', 'pochette metis', 'capucines', 'onthego',
      'marmont', 'dionysus', 'jackie', 'ophidia',
      'lady dior', 'saddle', 'book tote',
      'galleria', 're-edition', 'cleo', 'cahier',
      'baguette', 'peekaboo', 'first'
    ];

    for (const model of models) {
      if (titleLower.includes(model)) {
        // Find size if present
        const sizes = ['25', '30', '35', '18', '22', '16', '29', '33', '27', '31',
                       'small', 'medium', 'large', 'bb', 'pm', 'mm', 'gm'];

        let matchedSize = null;
        for (const size of sizes) {
          if (titleLower.includes(size)) {
            matchedSize = size;
            break;
          }
        }

        // Try to find matching entry
        const brandPrefix = brand || this.detectBrand(title);
        const searchKey = matchedSize
          ? `${brandPrefix} ${model} ${matchedSize}`
          : `${brandPrefix} ${model}`;

        for (const [key, data] of Object.entries(this.knownRetailPrices)) {
          if (key.toLowerCase().includes(searchKey.toLowerCase())) {
            return {
              ...data,
              modelName: key,
              confidence: 'medium'
            };
          }
        }
      }
    }

    return null;
  }

  /**
   * Detect brand from title
   */
  detectBrand(title) {
    const brands = ['Hermes', 'Chanel', 'Louis Vuitton', 'Gucci', 'Dior', 'Prada', 'Fendi'];
    const titleLower = title.toLowerCase();

    for (const brand of brands) {
      if (titleLower.includes(brand.toLowerCase()) ||
          titleLower.includes(brand.replace(' ', '').toLowerCase())) {
        return brand;
      }
    }

    return null;
  }

  /**
   * Scrape current retail prices from Fashionphile price guide
   */
  async scrapeFashionphilePriceGuide(brand, model) {
    try {
      const url = `https://www.fashionphile.com/guide/${brand.toLowerCase()}-${model.toLowerCase().replace(/\s+/g, '-')}`;

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

      // Extract MSRP if available
      const msrpText = $('.retail-price, .msrp, [class*="retail"]').text();
      const msrpMatch = msrpText.match(/\$?([\d,]+)/);
      const msrp = msrpMatch ? parseInt(msrpMatch[1].replace(/,/g, '')) : null;

      return {
        brand,
        model,
        msrp,
        source: 'fashionphile_guide',
        lastUpdated: new Date().toISOString()
      };

    } catch (error) {
      console.error('Error scraping Fashionphile price guide:', error.message);
      return null;
    }
  }

  /**
   * Get all retail prices for a brand
   */
  getBrandRetailPrices(brand) {
    const results = {};

    for (const [key, data] of Object.entries(this.knownRetailPrices)) {
      if (key.startsWith(brand)) {
        results[key] = data;
      }
    }

    return results;
  }

  /**
   * Calculate depreciation/appreciation
   */
  calculateValueChange(retailPrice, currentPrice) {
    if (!retailPrice || !currentPrice) return null;

    const change = currentPrice - retailPrice;
    const changePercent = ((change / retailPrice) * 100).toFixed(1);

    return {
      retailPrice,
      currentPrice,
      change,
      changePercent: parseFloat(changePercent),
      appreciation: change > 0,
      status: change > 0 ? 'Appreciated' : change === 0 ? 'Holding value' : 'Depreciated'
    };
  }
}

module.exports = RetailPriceDatabase;