← back to Handbag Authentication
scripts/deal-notifier.js
168 lines
require('dotenv').config();
const Database = require('../db/schema');
const SlackNotifier = require('../ai/slack-notifier');
class DealNotifier {
constructor() {
this.db = new Database(process.env.DATABASE_PATH);
this.slack = new SlackNotifier();
this.minDealPercentage = parseFloat(process.env.DEAL_THRESHOLD) || 30;
}
async checkAndNotify() {
console.log('=== Checking for High-Value Deals ===\n');
// Get new deals that haven't been notified
const sql = `
SELECT
l.*,
d.deal_percentage,
d.avg_us_price,
d.confidence_score,
d.ai_notes,
d.is_deal
FROM listings l
JOIN deal_analysis d ON l.id = d.listing_id
WHERE d.is_deal = 1
AND d.deal_percentage >= ?
AND l.is_active = 1
AND NOT EXISTS (
SELECT 1 FROM notifications n
WHERE n.listing_id = l.id
)
ORDER BY d.deal_percentage DESC
LIMIT 20
`;
return new Promise((resolve, reject) => {
this.db.db.all(sql, [this.minDealPercentage], async (err, deals) => {
if (err) {
reject(err);
return;
}
console.log(`Found ${deals.length} new high-value deals`);
for (const deal of deals) {
await this.notifyDeal(deal);
}
resolve(deals.length);
});
});
}
async notifyDeal(listing) {
const dealAnalysis = {
deal_percentage: listing.deal_percentage,
avg_us_price: listing.avg_us_price,
confidence_score: listing.confidence_score,
ai_notes: listing.ai_notes
};
try {
await this.slack.notifyDeal(listing, dealAnalysis);
// Mark as notified
this.markNotified(listing.id);
console.log(`✓ Notified: ${listing.title} (${listing.deal_percentage.toFixed(1)}% off)`);
} catch (error) {
console.error(`Error notifying deal ${listing.id}:`, error.message);
}
// Rate limit
await new Promise(resolve => setTimeout(resolve, 1000));
}
markNotified(listingId) {
// Create notifications table if it doesn't exist
this.db.db.run(`
CREATE TABLE IF NOT EXISTS notifications (
id INTEGER PRIMARY KEY AUTOINCREMENT,
listing_id INTEGER UNIQUE,
notified_at TEXT DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(listing_id) REFERENCES listings(id) ON DELETE CASCADE
)
`);
// Insert notification record
this.db.db.run(
'INSERT OR IGNORE INTO notifications (listing_id) VALUES (?)',
[listingId]
);
}
async sendDailySummary() {
console.log('=== Generating Daily Summary ===\n');
const queries = [
`SELECT COUNT(*) as total FROM listings WHERE is_active = 1 AND DATE(crawled_at) = DATE('now')`,
`SELECT COUNT(*) as deals FROM deal_analysis WHERE is_deal = 1 AND DATE(analysis_date) = DATE('now')`,
`SELECT AVG(deal_percentage) as avg FROM deal_analysis WHERE is_deal = 1 AND DATE(analysis_date) = DATE('now')`,
`SELECT MAX(deal_percentage) as max FROM deal_analysis WHERE is_deal = 1 AND DATE(analysis_date) = DATE('now')`
];
try {
const results = await Promise.all(queries.map(query =>
new Promise((resolve, reject) => {
this.db.db.get(query, (err, row) => {
if (err) reject(err);
else resolve(row);
});
})
));
const stats = {
total_listings: results[0].total || 0,
new_deals: results[1].deals || 0,
avg_deal_percentage: Math.round((results[2].avg || 0) * 10) / 10,
best_deal_percentage: Math.round((results[3].max || 0) * 10) / 10
};
if (stats.new_deals > 0) {
await this.slack.notifyDailySummary(stats);
console.log('✓ Daily summary sent to Slack');
} else {
console.log('No new deals today, skipping summary');
}
} catch (error) {
console.error('Error sending daily summary:', error);
}
}
close() {
this.db.close();
}
}
// Run if called directly
if (require.main === module) {
const notifier = new DealNotifier();
const command = process.argv[2] || 'check';
(async () => {
try {
if (command === 'check') {
await notifier.checkAndNotify();
} else if (command === 'summary') {
await notifier.sendDailySummary();
} else {
console.log('Usage:');
console.log(' node deal-notifier.js check - Check for new deals and notify');
console.log(' node deal-notifier.js summary - Send daily summary');
}
notifier.close();
process.exit(0);
} catch (error) {
console.error('Notifier error:', error);
notifier.close();
process.exit(1);
}
})();
}
module.exports = DealNotifier;