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