← back to Wine Finder Next

lib/services/newItemsTracker.js

119 lines

/**
 * New Items Tracker
 *
 * Tracks which wines have been seen before and identifies NEW listings
 * by comparing current crawl against historical database
 */

const fs = require('fs').promises;
const path = require('path');

class NewItemsTracker {
  constructor() {
    this.dbPath = path.join(__dirname, '..', 'data', 'seen_wines.json');
    this.seenWines = new Map();
    this.newWinesFound = [];
  }

  async load() {
    try {
      const data = await fs.readFile(this.dbPath, 'utf8');
      const parsed = JSON.parse(data);
      this.seenWines = new Map(Object.entries(parsed.wines || {}));
      console.log(`✓ Loaded ${this.seenWines.size} previously seen wines`);
    } catch (error) {
      // File doesn't exist yet - first run
      console.log('⚠ No previous wine history found - all wines will be marked as NEW');
    }
  }

  async save() {
    const data = {
      lastUpdated: new Date().toISOString(),
      totalWines: this.seenWines.size,
      wines: Object.fromEntries(this.seenWines)
    };

    await fs.writeFile(this.dbPath, JSON.stringify(data, null, 2));
    console.log(`✓ Saved ${this.seenWines.size} wines to history`);
  }

  // Generate unique key for a wine
  generateKey(wine) {
    const name = (wine.name || '').toLowerCase().replace(/[^a-z0-9]/g, '');
    const vintage = wine.vintage || '';
    const source = wine.source || '';
    return `${name}_${vintage}_${source}`.substring(0, 100);
  }

  // Check if wine is new (not seen before)
  isNew(wine) {
    const key = this.generateKey(wine);
    return !this.seenWines.has(key);
  }

  // Mark wine as seen
  markAsSeen(wine) {
    const key = this.generateKey(wine);
    this.seenWines.set(key, {
      firstSeen: new Date().toISOString(),
      name: wine.name,
      source: wine.source,
      price: wine.price
    });
  }

  // Process wines and identify new ones
  async processWines(wines) {
    await this.load();

    this.newWinesFound = [];

    for (const wine of wines) {
      if (this.isNew(wine)) {
        wine.isNew = true;
        wine.newItemDate = new Date().toISOString();
        this.newWinesFound.push(wine);
        this.markAsSeen(wine);
      } else {
        wine.isNew = false;
      }
    }

    await this.save();

    console.log(`\n🆕 NEW WINES FOUND: ${this.newWinesFound.length} out of ${wines.length} total`);

    return {
      allWines: wines,
      newWines: this.newWinesFound,
      newCount: this.newWinesFound.length,
      totalCount: wines.length
    };
  }

  // Get summary of new items
  getNewItemsSummary() {
    if (this.newWinesFound.length === 0) {
      return 'No new wines found in this crawl.';
    }

    const bySource = {};
    this.newWinesFound.forEach(wine => {
      if (!bySource[wine.source]) {
        bySource[wine.source] = 0;
      }
      bySource[wine.source]++;
    });

    let summary = `Found ${this.newWinesFound.length} NEW wines:\n`;
    Object.keys(bySource).sort().forEach(source => {
      summary += `  - ${source}: ${bySource[source]} new\n`;
    });

    return summary;
  }
}

module.exports = NewItemsTracker;