#!/usr/bin/env node /** * TENDER EMAIL ALERTS * * Sends email notifications for: * 1. High-value tenders (>£100k) * 2. New tenders matching keywords * 3. Daily digest of all new tenders */ import pg from 'pg'; import nodemailer from 'nodemailer'; import dotenv from 'dotenv'; dotenv.config(); const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL || 'postgresql://tenderpilot:tenderpilot123@localhost:5432/tenderpilot' }); // Email configuration const transporter = nodemailer.createTransporter({ host: process.env.SMTP_HOST || 'smtp.dynu.com', port: parseInt(process.env.SMTP_PORT || '587'), secure: false, // STARTTLS auth: { user: process.env.SMTP_USER || 'peter.foster@ukdataservices.co.uk', pass: process.env.SMTP_PASS } }); const ALERT_EMAIL = process.env.ALERT_EMAIL || 'peter.foster@ukdataservices.co.uk'; async function sendAlerts(mode = 'digest') { try { console.log(`[${new Date().toISOString()}] Starting tender alerts (mode: ${mode})...`); let tenders; let subject; let intro; if (mode === 'high-value') { // High-value tenders (>£100k or equivalent) tenders = await pool.query( `SELECT * FROM tenders WHERE status = 'open' AND (value_low > 100000 OR value_high > 100000) AND created_at > NOW() - INTERVAL '24 hours' ORDER BY value_high DESC NULLS LAST, created_at DESC LIMIT 50` ); subject = `🔔 TenderRadar: ${tenders.rows.length} High-Value Tenders (>£100k)`; intro = 'High-value tender opportunities identified in the last 24 hours:'; } else if (mode === 'digest') { // Daily digest - all new tenders tenders = await pool.query( `SELECT * FROM tenders WHERE status = 'open' AND created_at > NOW() - INTERVAL '24 hours' ORDER BY created_at DESC LIMIT 100` ); subject = `📊 TenderRadar Daily Digest: ${tenders.rows.length} New Tenders`; intro = 'New tender opportunities from the last 24 hours:'; } else { throw new Error(`Unknown mode: ${mode}`); } if (tenders.rows.length === 0) { console.log(`No tenders found for mode: ${mode}`); return; } // Build HTML email let html = `

🎯 TenderRadar Alerts

${intro}

`; for (const tender of tenders.rows) { const value = tender.value_high || tender.value_low; const valueStr = value ? `${tender.currency || '£'}${value.toLocaleString()}` : 'Value not specified'; const deadline = tender.deadline ? new Date(tender.deadline).toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' }) : 'No deadline'; const daysLeft = tender.deadline ? Math.ceil((new Date(tender.deadline) - new Date()) / (1000 * 60 * 60 * 24)) : null; const daysLeftStr = daysLeft ? ` (${daysLeft} day${daysLeft !== 1 ? 's' : ''} left)` : ''; html += `
${tender.title}
Authority: ${tender.authority_name || 'Unknown'}
Location: ${tender.location || 'Not specified'}
Sector: ${tender.sector || 'General'}
Source: ${tender.source}
Value: ${valueStr} | Deadline: ${deadline}${daysLeftStr}
${tender.description ? `

${tender.description.substring(0, 300)}${tender.description.length > 300 ? '...' : ''}

` : ''} View Tender →
`; } html += ` `; // Send email const mailOptions = { from: '"TenderRadar" ', to: ALERT_EMAIL, subject: subject, html: html }; const info = await transporter.sendMail(mailOptions); console.log(`✅ Email sent: ${info.messageId}`); console.log(` Tenders: ${tenders.rows.length}`); console.log(` To: ${ALERT_EMAIL}`); } catch (error) { console.error('Error sending alerts:', error); } finally { await pool.end(); } } // Get mode from command line argument const mode = process.argv[2] || 'digest'; sendAlerts(mode);