← back to Wine Finder Next

lib/services/priceHistoryService.js

152 lines

/**
 * Price History Service
 * Aggregates price data from Google Sheets, auctions, and retailers
 * Calculates deal scores and price trends
 */

const googleSheets = require('./googleSheets');

class PriceHistoryService {
  constructor() {
    this.priceCache = new Map();
  }

  /**
   * Get comprehensive price history for a wine
   */
  async getPriceHistory(wineName, vintage) {
    const cacheKey = `${wineName}_${vintage}`;
    
    if (this.priceCache.has(cacheKey)) {
      const cached = this.priceCache.get(cacheKey);
      if (Date.now() - cached.timestamp < 300000) { // 5 min cache
        return cached.data;
      }
    }

    try {
      // Fetch from Google Sheets historical data
      const sheetHistory = await googleSheets.getWineHistory(wineName);
      
      // Transform to price points
      const pricePoints = sheetHistory
        .filter(entry => entry.vintage === vintage)
        .map(entry => ({
          date: new Date(entry.timestamp),
          price: parseFloat(entry.price),
          source: entry.source,
          rating: parseFloat(entry.rating) || null
        }))
        .sort((a, b) => a.date - b.date);

      if (pricePoints.length === 0) {
        return {
          success: false,
          pricePoints: [],
          stats: null
        };
      }

      // Calculate statistics
      const prices = pricePoints.map(p => p.price);
      const currentPrice = prices[prices.length - 1];
      const avgPrice = prices.reduce((a, b) => a + b, 0) / prices.length;
      const minPrice = Math.min(...prices);
      const maxPrice = Math.max(...prices);
      const firstPrice = prices[0];
      
      // Calculate price change
      const priceChange = ((currentPrice - firstPrice) / firstPrice * 100).toFixed(2);
      const dealScore = this.calculateDealScore(currentPrice, avgPrice, minPrice, maxPrice);
      
      // Detect trend
      const recentPrices = prices.slice(-5); // Last 5 data points
      const trend = this.detectTrend(recentPrices);

      const result = {
        success: true,
        pricePoints,
        stats: {
          current: currentPrice,
          average: avgPrice,
          min: minPrice,
          max: maxPrice,
          priceChange: parseFloat(priceChange),
          dealScore,
          trend,
          dataPoints: pricePoints.length,
          firstRecorded: pricePoints[0].date,
          lastUpdated: pricePoints[pricePoints.length - 1].date
        }
      };

      // Cache result
      this.priceCache.set(cacheKey, {
        data: result,
        timestamp: Date.now()
      });

      return result;
    } catch (error) {
      console.error('Price history error:', error.message);
      return {
        success: false,
        pricePoints: [],
        stats: null,
        error: error.message
      };
    }
  }

  /**
   * Calculate deal score (0-100)
   * Higher score = better deal
   */
  calculateDealScore(currentPrice, avgPrice, minPrice, maxPrice) {
    const priceRange = maxPrice - minPrice;
    if (priceRange === 0) return 50;

    // Score based on how close to minimum price
    const positionInRange = (currentPrice - minPrice) / priceRange;
    const baseScore = (1 - positionInRange) * 100;

    // Bonus if below average
    const avgBonus = currentPrice < avgPrice ? 10 : 0;

    return Math.min(100, Math.max(0, baseScore + avgBonus)).toFixed(0);
  }

  /**
   * Detect price trend
   */
  detectTrend(prices) {
    if (prices.length < 2) return 'stable';

    const firstHalf = prices.slice(0, Math.floor(prices.length / 2));
    const secondHalf = prices.slice(Math.floor(prices.length / 2));

    const firstAvg = firstHalf.reduce((a, b) => a + b, 0) / firstHalf.length;
    const secondAvg = secondHalf.reduce((a, b) => a + b, 0) / secondHalf.length;

    const change = ((secondAvg - firstAvg) / firstAvg) * 100;

    if (change > 5) return 'rising';
    if (change < -5) return 'falling';
    return 'stable';
  }

  /**
   * Generate chart data for visualization
   */
  generateChartData(pricePoints) {
    return pricePoints.map(point => ({
      date: point.date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }),
      timestamp: point.date.getTime(),
      price: point.price,
      source: point.source
    }));
  }
}

module.exports = new PriceHistoryService();