← back to Vendor Discount Agent

server.js

255 lines

const express = require('express');
const helmet = require('helmet');
const path = require('path');
const { Pool } = require('pg');
const { checkVendorDiscount } = require('./discount-checker');

const app = express();
// Security headers via helmet (added 2026-05-04 overnight YOLO loop)
app.use(helmet({ contentSecurityPolicy: false }));
const PORT = 9860;

const pool = new Pool({
  connectionString: (process.env.DATABASE_URL || 'postgresql://dw_admin@127.0.0.1:15432/dw_unified')
});

app.use(express.json());

// Hard 404 for snapshot / backup files so they never serve from the static root.
app.use((req, res, next) => {
  if (/\.(bak|pre-)/i.test(req.path)) return res.status(404).end();
  next();
});

app.use(express.static(path.join(__dirname, 'public')));

// Basic auth
app.use((req, res, next) => {
  if (req.path === '/' || req.path.startsWith('/public')) return next();
  if (req.path.startsWith('/api')) {
    const auth = req.headers.authorization;
    if (!auth || auth !== 'Basic ' + Buffer.from('admin:DWSecure2024!').toString('base64')) {
      // Also allow no-auth for dashboard GET requests
      if (req.method === 'GET') return next();
      return res.status(401).json({ error: 'Unauthorized' });
    }
  }
  next();
});

// Get all vendors with credentials
app.get('/api/vendors', async (req, res) => {
  try {
    const result = await pool.query(`
      SELECT vendor_name, vendor_code, login_url, trade_username,
        CASE WHEN trade_password IS NOT NULL AND trade_password != '' THEN true ELSE false END AS has_password,
        vendor_discount_pct, pricing_unit, pricing_notes, catalog_count,
        has_credentials
      FROM vendor_registry
      WHERE is_active = true
      ORDER BY
        CASE WHEN trade_password IS NOT NULL AND trade_password != '' THEN 0 ELSE 1 END,
        catalog_count DESC
    `);
    res.json(result.rows);
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// Get vendors with stored credentials (ready to check)
app.get('/api/vendors/ready', async (req, res) => {
  try {
    const result = await pool.query(`
      SELECT vendor_name, vendor_code, login_url, trade_username,
        vendor_discount_pct, pricing_unit, pricing_notes, catalog_count
      FROM vendor_registry
      WHERE is_active = true
        AND trade_username IS NOT NULL AND trade_username != ''
        AND trade_password IS NOT NULL AND trade_password != ''
        AND login_url IS NOT NULL AND login_url != ''
      ORDER BY catalog_count DESC
    `);
    res.json(result.rows);
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// Get discount check history
app.get('/api/checks', async (req, res) => {
  try {
    const result = await pool.query(`
      SELECT * FROM vendor_discount_checks
      ORDER BY checked_at DESC
      LIMIT 100
    `);
    res.json(result.rows);
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// Run discount check for a specific vendor
app.post('/api/check/:vendorCode', async (req, res) => {
  const { vendorCode } = req.params;
  try {
    const vendorResult = await pool.query(
      `SELECT * FROM vendor_registry WHERE vendor_code = $1`,
      [vendorCode]
    );
    if (vendorResult.rows.length === 0) {
      return res.status(404).json({ error: 'Vendor not found' });
    }
    const vendor = vendorResult.rows[0];
    if (!vendor.trade_username || !vendor.trade_password || !vendor.login_url) {
      return res.status(400).json({ error: 'Vendor missing credentials or login URL' });
    }

    res.json({ status: 'started', vendor: vendor.vendor_name, message: 'Discount check initiated' });

    // Run async - don't block response
    checkVendorDiscount(vendor, pool).catch(err => {
      console.error(`Discount check failed for ${vendorCode}:`, err.message);
    });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// Run all ready vendors
app.post('/api/check-all', async (req, res) => {
  try {
    const result = await pool.query(`
      SELECT * FROM vendor_registry
      WHERE is_active = true
        AND trade_username IS NOT NULL AND trade_username != ''
        AND trade_password IS NOT NULL AND trade_password != ''
        AND login_url IS NOT NULL AND login_url != ''
      ORDER BY catalog_count DESC
    `);
    const vendors = result.rows;
    res.json({ status: 'started', count: vendors.length, vendors: vendors.map(v => v.vendor_name) });

    // Run sequentially to avoid overwhelming browser
    for (const vendor of vendors) {
      try {
        await checkVendorDiscount(vendor, pool);
        console.log(`✓ Checked ${vendor.vendor_name}`);
      } catch (err) {
        console.error(`✗ Failed ${vendor.vendor_name}: ${err.message}`);
      }
    }
    console.log('All vendor discount checks complete');
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// Update vendor discount manually
app.post('/api/vendors/:vendorCode/discount', async (req, res) => {
  const { vendorCode } = req.params;
  const { discount_pct, pricing_notes, pricing_unit } = req.body;
  try {
    const updates = [];
    const values = [];
    let idx = 1;

    if (discount_pct !== undefined) {
      updates.push(`vendor_discount_pct = $${idx++}`);
      values.push(discount_pct);
    }
    if (pricing_notes !== undefined) {
      updates.push(`pricing_notes = $${idx++}`);
      values.push(pricing_notes);
    }
    if (pricing_unit !== undefined) {
      updates.push(`pricing_unit = $${idx++}`);
      values.push(pricing_unit);
    }
    updates.push(`updated_at = NOW()`);
    values.push(vendorCode);

    const result = await pool.query(
      `UPDATE vendor_registry SET ${updates.join(', ')} WHERE vendor_code = $${idx} RETURNING vendor_name, vendor_discount_pct, pricing_notes`,
      values
    );
    if (result.rows.length === 0) return res.status(404).json({ error: 'Vendor not found' });
    res.json(result.rows[0]);
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// Add/update credentials for a vendor
app.post('/api/vendors/:vendorCode/credentials', async (req, res) => {
  const { vendorCode } = req.params;
  const { login_url, trade_username, trade_password } = req.body;
  try {
    const result = await pool.query(`
      UPDATE vendor_registry
      SET login_url = COALESCE($1, login_url),
          trade_username = COALESCE($2, trade_username),
          trade_password = COALESCE($3, trade_password),
          has_credentials = true,
          updated_at = NOW()
      WHERE vendor_code = $4
      RETURNING vendor_name, vendor_code, login_url, trade_username
    `, [login_url, trade_username, trade_password, vendorCode]);
    if (result.rows.length === 0) return res.status(404).json({ error: 'Vendor not found' });
    res.json(result.rows[0]);
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// Discount summary stats
app.get('/api/stats', async (req, res) => {
  try {
    const result = await pool.query(`
      SELECT
        COUNT(*) FILTER (WHERE is_active) AS total_active,
        COUNT(*) FILTER (WHERE is_active AND vendor_discount_pct IS NOT NULL) AS has_discount,
        COUNT(*) FILTER (WHERE is_active AND has_credentials) AS has_credentials,
        COUNT(*) FILTER (WHERE is_active AND trade_password IS NOT NULL AND trade_password != '') AS has_full_creds,
        COUNT(*) FILTER (WHERE is_active AND vendor_discount_pct IS NULL) AS missing_discount
      FROM vendor_registry
    `);
    res.json(result.rows[0]);
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

async function ensureTable() {
  await pool.query(`
    CREATE TABLE IF NOT EXISTS vendor_discount_checks (
      id SERIAL PRIMARY KEY,
      vendor_code TEXT NOT NULL,
      vendor_name TEXT,
      checked_at TIMESTAMP DEFAULT NOW(),
      status TEXT DEFAULT 'pending',
      login_success BOOLEAN,
      discount_found NUMERIC(5,2),
      price_type TEXT,
      sample_product TEXT,
      sample_list_price NUMERIC(10,2),
      sample_net_price NUMERIC(10,2),
      calculated_discount NUMERIC(5,2),
      screenshot_path TEXT,
      notes TEXT,
      error TEXT
    )
  `);
}

ensureTable().then(() => {
  app.listen(PORT, '0.0.0.0', () => {
    console.log(`Vendor Discount Agent running on port ${PORT}`);
    console.log(`Dashboard: http://45.61.58.125:${PORT}`);
  });
}).catch(err => {
  console.error('Failed to start:', err);
  process.exit(1);
});