← back to Handbag Authentication

scripts/export-spreadsheet.js

254 lines

require('dotenv').config();
const Database = require('../db/schema');
const { Parser } = require('json2csv');
const fs = require('fs');
const path = require('path');

class SpreadsheetExporter {
  constructor() {
    this.db = new Database(process.env.DATABASE_PATH);
    this.outputDir = path.join(__dirname, '../exports');

    // Create exports directory if it doesn't exist
    if (!fs.existsSync(this.outputDir)) {
      fs.mkdirSync(this.outputDir, { recursive: true });
    }
  }

  async exportAllListings(filename = null) {
    const outputFile = filename || `all-listings-${Date.now()}.csv`;
    const outputPath = path.join(this.outputDir, outputFile);

    return new Promise((resolve, reject) => {
      this.db.getListings({}, (err, listings) => {
        if (err) {
          reject(err);
          return;
        }

        const fields = [
          { label: 'ID', value: 'id' },
          { label: 'Brand', value: 'brand' },
          { label: 'Title', value: 'title' },
          { label: 'Source', value: 'source' },
          { label: 'Listing Type', value: 'listing_type' },
          { label: 'Price (JPY)', value: 'price_jpy' },
          { label: 'Price (USD)', value: 'price_usd' },
          { label: 'US Market Avg', value: 'avg_us_price' },
          { label: 'Deal %', value: 'deal_percentage' },
          { label: 'Potential Profit', value: row => row.avg_us_price && row.price_usd ? (row.avg_us_price - row.price_usd).toFixed(2) : '' },
          { label: 'Is Deal', value: row => row.is_deal ? 'Yes' : 'No' },
          { label: 'Condition', value: 'condition' },
          { label: 'Confidence', value: row => row.confidence_score ? (row.confidence_score * 100).toFixed(0) + '%' : '' },
          { label: 'AI Notes', value: 'ai_notes' },
          { label: 'Product URL', value: 'product_url' },
          { label: 'Image URL', value: 'image_url' },
          { label: 'Crawled At', value: 'crawled_at' }
        ];

        try {
          const parser = new Parser({ fields });
          const csv = parser.parse(listings);

          fs.writeFileSync(outputPath, csv);
          console.log(`✓ Exported ${listings.length} listings to ${outputPath}`);
          resolve(outputPath);
        } catch (error) {
          reject(error);
        }
      });
    });
  }

  async exportDealsOnly(minDealPercentage = 20, filename = null) {
    const outputFile = filename || `deals-${minDealPercentage}pct-${Date.now()}.csv`;
    const outputPath = path.join(this.outputDir, outputFile);

    return new Promise((resolve, reject) => {
      this.db.getListings({ dealOnly: true, minDealPercentage, sort: 'deal' }, (err, listings) => {
        if (err) {
          reject(err);
          return;
        }

        const fields = [
          { label: 'Brand', value: 'brand' },
          { label: 'Title', value: 'title' },
          { label: 'Deal %', value: row => row.deal_percentage.toFixed(1) + '%' },
          { label: 'JP Price (¥)', value: row => '¥' + row.price_jpy.toLocaleString() },
          { label: 'USD Price', value: row => '$' + row.price_usd.toFixed(2) },
          { label: 'US Market', value: row => '$' + (row.avg_us_price || 0).toFixed(0) },
          { label: 'Profit Potential', value: row => '$' + (row.avg_us_price - row.price_usd).toFixed(0) },
          { label: 'Condition', value: 'condition' },
          { label: 'Type', value: 'listing_type' },
          { label: 'Source', value: 'source' },
          { label: 'Confidence', value: row => (row.confidence_score * 100).toFixed(0) + '%' },
          { label: 'AI Analysis', value: 'ai_notes' },
          { label: 'Link', value: 'product_url' }
        ];

        try {
          const parser = new Parser({ fields });
          const csv = parser.parse(listings);

          fs.writeFileSync(outputPath, csv);
          console.log(`✓ Exported ${listings.length} deals to ${outputPath}`);
          resolve(outputPath);
        } catch (error) {
          reject(error);
        }
      });
    });
  }

  async exportByBrand(brand, filename = null) {
    const outputFile = filename || `${brand.toLowerCase().replace(/\s+/g, '-')}-${Date.now()}.csv`;
    const outputPath = path.join(this.outputDir, outputFile);

    return new Promise((resolve, reject) => {
      this.db.getListings({ brand }, (err, listings) => {
        if (err) {
          reject(err);
          return;
        }

        const fields = [
          { label: 'Title', value: 'title' },
          { label: 'Price (USD)', value: row => '$' + row.price_usd.toFixed(2) },
          { label: 'Deal %', value: row => row.deal_percentage ? row.deal_percentage.toFixed(1) + '%' : 'N/A' },
          { label: 'Condition', value: 'condition' },
          { label: 'Source', value: 'source' },
          { label: 'Link', value: 'product_url' }
        ];

        try {
          const parser = new Parser({ fields });
          const csv = parser.parse(listings);

          fs.writeFileSync(outputPath, csv);
          console.log(`✓ Exported ${listings.length} ${brand} listings to ${outputPath}`);
          resolve(outputPath);
        } catch (error) {
          reject(error);
        }
      });
    });
  }

  async exportDailySummary() {
    const outputFile = `daily-summary-${new Date().toISOString().split('T')[0]}.csv`;
    const outputPath = path.join(this.outputDir, outputFile);

    // Get top deals from today
    const sql = `
      SELECT
        l.brand,
        l.title,
        l.price_usd,
        d.avg_us_price,
        d.deal_percentage,
        l.condition,
        l.source,
        l.product_url
      FROM listings l
      JOIN deal_analysis d ON l.id = d.listing_id
      WHERE d.is_deal = 1
        AND DATE(l.crawled_at) = DATE('now')
      ORDER BY d.deal_percentage DESC
      LIMIT 100
    `;

    return new Promise((resolve, reject) => {
      this.db.db.all(sql, (err, listings) => {
        if (err) {
          reject(err);
          return;
        }

        if (listings.length === 0) {
          console.log('No new deals today');
          resolve(null);
          return;
        }

        const fields = [
          { label: 'Brand', value: 'brand' },
          { label: 'Title', value: 'title' },
          { label: 'Price', value: row => '$' + row.price_usd.toFixed(2) },
          { label: 'Market Value', value: row => '$' + row.avg_us_price.toFixed(0) },
          { label: 'Savings', value: row => row.deal_percentage.toFixed(1) + '%' },
          { label: 'Profit', value: row => '$' + (row.avg_us_price - row.price_usd).toFixed(0) },
          { label: 'Condition', value: 'condition' },
          { label: 'Source', value: 'source' },
          { label: 'URL', value: 'product_url' }
        ];

        try {
          const parser = new Parser({ fields });
          const csv = parser.parse(listings);

          fs.writeFileSync(outputPath, csv);
          console.log(`✓ Exported ${listings.length} today's deals to ${outputPath}`);
          resolve(outputPath);
        } catch (error) {
          reject(error);
        }
      });
    });
  }

  close() {
    this.db.close();
  }
}

// CLI interface
if (require.main === module) {
  const exporter = new SpreadsheetExporter();
  const command = process.argv[2] || 'deals';

  (async () => {
    try {
      switch (command) {
        case 'all':
          await exporter.exportAllListings();
          break;

        case 'deals':
          const minDeal = parseInt(process.argv[3]) || 20;
          await exporter.exportDealsOnly(minDeal);
          break;

        case 'brand':
          const brand = process.argv[3];
          if (!brand) {
            console.error('Please specify a brand: node export-spreadsheet.js brand "Gucci"');
            process.exit(1);
          }
          await exporter.exportByBrand(brand);
          break;

        case 'daily':
          await exporter.exportDailySummary();
          break;

        default:
          console.log('Usage:');
          console.log('  node export-spreadsheet.js all                  - Export all listings');
          console.log('  node export-spreadsheet.js deals [minDeal]      - Export deals (default 20%)');
          console.log('  node export-spreadsheet.js brand "Brand Name"   - Export by brand');
          console.log('  node export-spreadsheet.js daily                - Export today\'s deals');
      }

      exporter.close();
      process.exit(0);
    } catch (error) {
      console.error('Export error:', error);
      exporter.close();
      process.exit(1);
    }
  })();
}

module.exports = SpreadsheetExporter;