← back to Handbag Auth Nextjs
scripts/snapshotPrices.ts
232 lines
// Daily price snapshot collection script
// Collects current prices from all RSS items and stores historical data
import fs from "fs";
import path from "path";
import crypto from "crypto";
import type { PriceSnapshot, HandbagPriceHistory } from "../src/types/priceHistory";
import type { HandbagFeedItem } from "../src/types/handbagRss";
const SNAPSHOTS_FILE = path.resolve("data/price_snapshots.json");
const RSS_ITEMS_FILE = path.resolve("data/handbag_rss_items.json");
function loadRssItems(): HandbagFeedItem[] {
if (!fs.existsSync(RSS_ITEMS_FILE)) return [];
return JSON.parse(fs.readFileSync(RSS_ITEMS_FILE, "utf8"));
}
function loadSnapshots(): PriceSnapshot[] {
if (!fs.existsSync(SNAPSHOTS_FILE)) return [];
return JSON.parse(fs.readFileSync(SNAPSHOTS_FILE, "utf8"));
}
function saveSnapshots(snapshots: PriceSnapshot[]) {
const dir = path.dirname(SNAPSHOTS_FILE);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(SNAPSHOTS_FILE, JSON.stringify(snapshots, null, 2));
}
function makeSnapshotId(feedItem: HandbagFeedItem, date: string): string {
const base = `${feedItem.link}-${date}`;
return crypto.createHash("md5").update(base).digest("hex");
}
// Generate handbag ID from brand + model
// In production, this should match against your actual handbag database
function makeHandbagId(item: HandbagFeedItem): string {
const brand = item.brand || "unknown";
const model = item.model || item.title.slice(0, 30);
return crypto
.createHash("md5")
.update(`${brand}-${model}`.toLowerCase())
.digest("hex")
.slice(0, 12);
}
function collectSnapshots() {
const items = loadRssItems();
const existingSnapshots = loadSnapshots();
const today = new Date().toISOString().split("T")[0]; // "2025-11-15"
const now = new Date().toISOString();
// Create map of existing snapshots by ID
const existingById = new Map(existingSnapshots.map((s) => [s.id, s]));
let newCount = 0;
console.log(`Collecting price snapshots for ${today}...`);
for (const item of items) {
// Only snapshot items with prices
if (!item.price) continue;
const snapshotId = makeSnapshotId(item, today);
// Skip if already snapshotted today
if (existingById.has(snapshotId)) {
continue;
}
// Map condition to valid PriceSnapshot condition
let condition: "new" | "used" | "auction" = "used";
if (item.condition === "new") condition = "new";
else if (item.condition === "auction") condition = "auction";
const snapshot: PriceSnapshot = {
id: snapshotId,
handbagId: makeHandbagId(item),
merchantId: item.sourceId,
merchantName: item.sourceId.split("-")[0], // e.g. "fashionphile-new" -> "fashionphile"
condition,
price: item.price,
currency: item.currency || "USD",
url: item.link,
snapshotDate: today,
timestamp: now,
inStock: true, // Assume in stock if found in RSS feed
notes: item.summary?.slice(0, 200),
};
existingSnapshots.push(snapshot);
existingById.set(snapshot.id, snapshot);
newCount++;
}
if (newCount > 0) {
saveSnapshots(existingSnapshots);
}
console.log(`\n=== Price Snapshot Summary ===`);
console.log(`Date: ${today}`);
console.log(`New snapshots: ${newCount}`);
console.log(`Total snapshots: ${existingSnapshots.length}`);
// Show some stats
const todaySnapshots = existingSnapshots.filter((s) => s.snapshotDate === today);
const avgPrice =
todaySnapshots.reduce((sum, s) => sum + s.price, 0) / todaySnapshots.length;
console.log(`Today's snapshots: ${todaySnapshots.length}`);
console.log(`Average price: ${todaySnapshots[0]?.currency || "$"}${avgPrice.toFixed(2)}`);
return newCount;
}
function analyzeHistory(handbagId: string): HandbagPriceHistory | null {
const snapshots = loadSnapshots();
const handbagSnapshots = snapshots.filter((s) => s.handbagId === handbagId);
if (handbagSnapshots.length === 0) {
return null;
}
// Sort by date
handbagSnapshots.sort((a, b) => a.snapshotDate.localeCompare(b.snapshotDate));
const prices = handbagSnapshots.map((s) => s.price);
const currentLowestPrice = Math.min(...prices);
const allTimeLowestPrice = Math.min(...prices);
const allTimeHighestPrice = Math.max(...prices);
const averagePrice = prices.reduce((a, b) => a + b, 0) / prices.length;
// Calculate 30-day change
const now = new Date();
const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
const recentSnapshots = handbagSnapshots.filter(
(s) => new Date(s.snapshotDate) >= thirtyDaysAgo
);
let priceChange30d: number | undefined;
if (recentSnapshots.length >= 2) {
const oldPrice = recentSnapshots[0].price;
const newPrice = recentSnapshots[recentSnapshots.length - 1].price;
priceChange30d = ((newPrice - oldPrice) / oldPrice) * 100;
}
// Sample brand/model from first snapshot
const sample = handbagSnapshots[0];
return {
handbagId,
brand: sample.merchantName, // TODO: extract actual brand from data
model: "Model", // TODO: extract from matched handbag
snapshots: handbagSnapshots,
currentLowestPrice,
allTimeLowestPrice,
allTimeHighestPrice,
averagePrice,
priceChange30d,
};
}
// CLI commands
const isMainModule = import.meta.url === `file://${process.argv[1]}`;
if (isMainModule) {
const command = process.argv[2];
switch (command) {
case "collect":
collectSnapshots();
break;
case "analyze": {
const handbagId = process.argv[3];
if (!handbagId) {
console.error("Usage: npm run price-history analyze <handbagId>");
process.exit(1);
}
const history = analyzeHistory(handbagId);
if (!history) {
console.error(`No price history found for handbag: ${handbagId}`);
process.exit(1);
}
console.log(`\n=== Price History: ${history.brand} ${history.model} ===`);
console.log(`Total snapshots: ${history.snapshots.length}`);
console.log(`Current lowest: $${history.currentLowestPrice}`);
console.log(`All-time low: $${history.allTimeLowestPrice}`);
console.log(`All-time high: $${history.allTimeHighestPrice}`);
console.log(`Average: $${history.averagePrice?.toFixed(2)}`);
if (history.priceChange30d) {
console.log(
`30-day change: ${history.priceChange30d > 0 ? "+" : ""}${history.priceChange30d.toFixed(1)}%`
);
}
break;
}
case "stats": {
const snapshots = loadSnapshots();
const uniqueDates = new Set(snapshots.map((s) => s.snapshotDate));
const uniqueHandbags = new Set(snapshots.map((s) => s.handbagId));
console.log(`\n=== Price Snapshot Statistics ===`);
console.log(`Total snapshots: ${snapshots.length}`);
console.log(`Unique handbags tracked: ${uniqueHandbags.size}`);
console.log(`Days of data: ${uniqueDates.size}`);
const sortedDates = Array.from(uniqueDates).sort();
if (sortedDates.length > 0) {
console.log(`Date range: ${sortedDates[0]} - ${sortedDates[sortedDates.length - 1]}`);
}
break;
}
default:
console.log("Daily Price Snapshot Tool");
console.log("\nUsage:");
console.log(" ts-node scripts/snapshotPrices.ts <command>");
console.log("\nCommands:");
console.log(" collect - Collect today's price snapshots from RSS");
console.log(" analyze <id> - Analyze price history for a handbag");
console.log(" stats - Show snapshot statistics");
process.exit(1);
}
}
export { collectSnapshots, analyzeHistory };