'use strict'; const cron = require('node-cron'); const { getDb } = require('../db/database'); const { buildTrackerRow, getCycleRange } = require('../services/statusService'); const { pruneExpiredSessions } = require('../services/authService'); const { runNotifications, runDriftNotifications } = require('../services/notificationService'); const { runAllCleanup } = require('../services/cleanupService'); const { markWorkerError, markWorkerStarted, markWorkerSuccess, } = require('../services/statusRuntime'); const DAILY_CRON_HOUR = 6; function nextDailyRunIso(from = new Date()) { const next = new Date(from); next.setHours(DAILY_CRON_HOUR, 0, 0, 0); if (next <= from) next.setDate(next.getDate() + 1); return next.toISOString(); } async function runDailyTasks() { const db = getDb(); const now = new Date(); const year = now.getFullYear(); const month = now.getMonth() + 1; const todayStr = now.toISOString().slice(0, 10); const bills = db.prepare('SELECT * FROM bills WHERE active = 1 AND deleted_at IS NULL').all(); if (bills.length > 0) { // Batch-fetch payments for all active bills from the last 90 days to avoid // one query per bill. Different billing cycles (monthly/quarterly/annual) all // fit inside a 90-day window for the current month's due-date checks. const billIds = bills.map(b => b.id); const placeholders = billIds.map(() => '?').join(','); const windowStart = new Date(Date.now() - 90 * 86400000).toISOString().slice(0, 10); let allPayments = []; try { allPayments = db.prepare(` SELECT * FROM payments WHERE bill_id IN (${placeholders}) AND paid_date >= ? AND deleted_at IS NULL `).all(...billIds, windowStart); } catch (err) { console.error('[worker] Failed to batch-fetch payments:', err.message); } // Group payments by bill_id in memory const paymentsByBill = new Map(); for (const p of allPayments) { if (!paymentsByBill.has(p.bill_id)) paymentsByBill.set(p.bill_id, []); paymentsByBill.get(p.bill_id).push(p); } // Prepare autopay update statement once outside the loop const markAutopay = db.prepare( "UPDATE bills SET autodraft_status = 'assumed_paid', updated_at = datetime('now') WHERE id = ?" ); for (const bill of bills) { const range = getCycleRange(year, month, bill); if (!range) continue; // bill does not apply this cycle // Filter pre-fetched payments to the bill's cycle range const payments = (paymentsByBill.get(bill.id) || []).filter( p => p.paid_date >= range.start && p.paid_date <= range.end ); const row = buildTrackerRow(bill, payments, year, month, todayStr); if (!row) continue; // Auto-mark autopay bills as assumed_paid on due date if ( bill.autopay_enabled && bill.autodraft_status === 'pending' && todayStr >= row.due_date ) { try { markAutopay.run(bill.id); } catch (err) { console.error(`[worker] Failed to mark autopay for bill ${bill.id}:`, err.message); } } } } pruneExpiredSessions(); await runNotifications().catch(err => { console.error('[worker] Notification error (non-fatal):', err.message); }); await runDriftNotifications().catch(err => { console.error('[worker] Drift notification error (non-fatal):', err.message); }); await runAllCleanup().catch(err => { console.error('[worker] Cleanup error (non-fatal):', err.message); }); markWorkerSuccess(nextDailyRunIso()); console.log(`[worker] Daily tasks ran at ${todayStr}`); } function start() { markWorkerStarted(nextDailyRunIso()); // Run once at startup runDailyTasks().catch(err => { markWorkerError(err, nextDailyRunIso()); console.error('[worker] Startup task error:', err); }); // Run every day at 6:00 AM cron.schedule('0 6 * * *', () => { runDailyTasks().catch(err => { markWorkerError(err, nextDailyRunIso()); console.error('[worker] Daily task error:', err); }); }); } module.exports = { start, runDailyTasks, nextDailyRunIso };