← back to Designer Wallcoverings
DW-Agents/dw-agents/daily-price-tracker.ts
289 lines
/**
* Daily Price Tracker for Past Orders
* Automatically scrapes prices every morning for items you've ordered before
* Shows best current prices for easy reordering
*/
import fetch from 'node-fetch';
import * as cheerio from 'cheerio';
import * as fs from 'fs';
interface Order {
id: string;
item: string;
vendor: string;
price: number;
status: string;
timestamp: Date;
}
interface PriceTracking {
item: string;
originalPrice: number;
currentPrice: number | null;
priceChange: number;
percentChange: number;
url: string;
lastChecked: Date;
bestDeal: boolean;
}
interface PriceHistory {
item: string;
prices: Array<{
date: Date;
price: number;
vendor: string;
}>;
lowestPrice: number;
highestPrice: number;
averagePrice: number;
}
/**
* Scrape current Amazon price and product URL for an item
*/
async function scrapeCurrentPrice(itemName: string): Promise<{ price: number; url: string } | null> {
try {
const amazonUrl = `https://www.amazon.com/s?k=${encodeURIComponent(itemName)}`;
const response = await fetch(amazonUrl, {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Accept': 'text/html,application/xhtml+xml',
'Accept-Language': 'en-US,en;q=0.5'
}
});
if (response.ok) {
const html = await response.text();
const $ = cheerio.load(html);
// Find first product result
const firstProduct = $('div[data-component-type="s-search-result"]').first();
if (firstProduct.length > 0) {
const priceWhole = firstProduct.find('.a-price-whole').first().text().replace(/[^0-9]/g, '');
const priceFraction = firstProduct.find('.a-price-fraction').first().text().replace(/[^0-9]/g, '');
const productLink = firstProduct.find('h2 a').attr('href');
if (priceWhole && productLink) {
const price = parseFloat(`${priceWhole}.${priceFraction || '00'}`);
const fullUrl = `https://www.amazon.com${productLink}`;
const urlWithTag = fullUrl + (fullUrl.includes('?') ? '&' : '?') + 'tag=designerwal0b-20';
console.log(` ✅ ${itemName.substring(0, 40)}: $${price}`);
return { price, url: urlWithTag };
} else {
console.log(` ⚠️ Could not find price/link for: ${itemName.substring(0, 40)}`);
}
} else {
console.log(` ⚠️ No product results found for: ${itemName.substring(0, 40)}`);
}
} else {
console.log(` ⚠️ HTTP ${response.status} for: ${itemName.substring(0, 40)}`);
}
} catch (e) {
console.log(` ⚠️ Failed to scrape: ${itemName.substring(0, 40)}`);
}
return null;
}
/**
* Track prices for all past orders
*/
async function trackAllPrices(): Promise<PriceTracking[]> {
console.log('🔍 Daily Price Tracker - Starting...\n');
console.log(`📅 Date: ${new Date().toLocaleDateString()}`);
console.log(`⏰ Time: ${new Date().toLocaleTimeString()}\n`);
// Load past orders
const ordersPath = '/root/Projects/Designer-Wallcoverings/DW-Agents/data/amazon-orders.json';
if (!fs.existsSync(ordersPath)) {
console.log('❌ No order history found');
return [];
}
const orders: Order[] = JSON.parse(fs.readFileSync(ordersPath, 'utf-8'));
console.log(`📦 Tracking ${orders.length} items from past orders\n`);
const priceTracking: PriceTracking[] = [];
// Get unique items (avoid duplicates)
const uniqueItems = new Map<string, Order>();
orders.forEach(order => {
const key = order.item.toLowerCase();
if (!uniqueItems.has(key) || new Date(order.timestamp) > new Date(uniqueItems.get(key)!.timestamp)) {
uniqueItems.set(key, order);
}
});
// Scrape current prices
let count = 0;
for (const [, order] of uniqueItems) {
count++;
console.log(`[${count}/${uniqueItems.size}] Checking: ${order.item.substring(0, 50)}...`);
const result = await scrapeCurrentPrice(order.item);
if (result) {
const priceChange = result.price - order.price;
const percentChange = ((priceChange / order.price) * 100);
priceTracking.push({
item: order.item,
originalPrice: order.price,
currentPrice: result.price,
priceChange,
percentChange,
url: result.url,
lastChecked: new Date(),
bestDeal: priceChange < 0
});
}
// Rate limiting - wait 2 seconds between requests
await new Promise(resolve => setTimeout(resolve, 2000));
}
return priceTracking;
}
/**
* Save price tracking data
*/
function savePriceTracking(tracking: PriceTracking[]): void {
const outputPath = '/root/Projects/Designer-Wallcoverings/DW-Agents/data/price-tracking.json';
fs.writeFileSync(outputPath, JSON.stringify(tracking, null, 2));
console.log(`\n💾 Saved price tracking to: ${outputPath}`);
// Also update price history
updatePriceHistory(tracking);
}
/**
* Update price history over time
*/
function updatePriceHistory(tracking: PriceTracking[]): void {
const historyPath = '/root/Projects/Designer-Wallcoverings/DW-Agents/data/price-history.json';
let history: Record<string, PriceHistory> = {};
if (fs.existsSync(historyPath)) {
history = JSON.parse(fs.readFileSync(historyPath, 'utf-8'));
}
tracking.forEach(track => {
if (!track.currentPrice) return;
if (!history[track.item]) {
history[track.item] = {
item: track.item,
prices: [],
lowestPrice: track.currentPrice,
highestPrice: track.currentPrice,
averagePrice: track.currentPrice
};
}
const item = history[track.item];
item.prices.push({
date: new Date(),
price: track.currentPrice,
vendor: 'Amazon'
});
// Keep only last 90 days
item.prices = item.prices.filter(p =>
(new Date().getTime() - new Date(p.date).getTime()) < 90 * 24 * 60 * 60 * 1000
);
// Update stats
const prices = item.prices.map(p => p.price);
item.lowestPrice = Math.min(...prices);
item.highestPrice = Math.max(...prices);
item.averagePrice = prices.reduce((a, b) => a + b, 0) / prices.length;
history[track.item] = item;
});
fs.writeFileSync(historyPath, JSON.stringify(history, null, 2));
console.log(`📈 Updated price history`);
}
/**
* Generate daily report
*/
async function generateDailyReport(): Promise<void> {
const tracking = await trackAllPrices();
if (tracking.length === 0) {
console.log('\n❌ No prices tracked');
return;
}
savePriceTracking(tracking);
// Generate report
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
console.log('📊 DAILY PRICE REPORT');
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
// Price drops
const drops = tracking.filter(t => t.priceChange < 0).sort((a, b) => a.percentChange - b.percentChange);
if (drops.length > 0) {
console.log('🎉 PRICE DROPS - BUY NOW!\n');
drops.forEach(item => {
console.log(`✅ ${item.item.substring(0, 60)}`);
console.log(` Was: $${item.originalPrice.toFixed(2)} → Now: $${item.currentPrice?.toFixed(2)}`);
console.log(` 💰 Save $${Math.abs(item.priceChange).toFixed(2)} (${Math.abs(item.percentChange).toFixed(1)}% off)`);
console.log(` 🛒 ${item.url}\n`);
});
}
// Price increases
const increases = tracking.filter(t => t.priceChange > 0);
if (increases.length > 0) {
console.log(`\n⚠️ PRICE INCREASES (${increases.length} items)\n`);
increases.slice(0, 3).forEach(item => {
console.log(`❗ ${item.item.substring(0, 60)}`);
console.log(` Was: $${item.originalPrice.toFixed(2)} → Now: $${item.currentPrice?.toFixed(2)} (+${item.percentChange.toFixed(1)}%)\n`);
});
}
// No change
const noChange = tracking.filter(t => Math.abs(t.priceChange) < 0.50);
console.log(`\n📊 Summary:`);
console.log(` Price Drops: ${drops.length} items`);
console.log(` Price Increases: ${increases.length} items`);
console.log(` Stable Prices: ${noChange.length} items`);
console.log(` Total Tracked: ${tracking.length} items\n`);
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
}
/**
* Get reorder recommendations based on price tracking
*/
export function getReorderRecommendations(): PriceTracking[] {
const trackingPath = '/root/Projects/Designer-Wallcoverings/DW-Agents/data/price-tracking.json';
if (!fs.existsSync(trackingPath)) {
return [];
}
const tracking: PriceTracking[] = JSON.parse(fs.readFileSync(trackingPath, 'utf-8'));
// Filter for best deals (price drops)
return tracking
.filter(t => t.priceChange < 0)
.sort((a, b) => a.percentChange - b.percentChange)
.slice(0, 10);
}
// Run if called directly
if (require.main === module) {
generateDailyReport().catch(console.error);
}
export { trackAllPrices, savePriceTracking, generateDailyReport, PriceTracking };