require('dotenv').config(); const cron = require('node-cron'); const http = require('http'); const PHP_BASE = process.env.PHP_BASE_URL || 'http://127.0.0.1'; const PHP_WEBROOT = process.env.PHP_WEBROOT || '/brnwms/app'; const SECRET = process.env.EMIT_SECRET || ''; const NODE_PORT = process.env.PORT || 3000; if (!SECRET) { // The PHP cron endpoints refuse an empty secret; fail loudly instead of // sending unauthenticated calls every schedule tick. console.error('EMIT_SECRET is not set — scheduler not started.'); process.exit(1); } function callPhp(path, body) { return new Promise((resolve, reject) => { const payload = JSON.stringify(body); const url = new URL(PHP_BASE + PHP_WEBROOT + path); const options = { hostname: url.hostname, port: url.port || 80, path: url.pathname, method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload), 'x-cron-secret': SECRET, }, }; const req = http.request(options, (res) => { let data = ''; res.on('data', (chunk) => { data += chunk; }); res.on('end', () => { try { resolve(JSON.parse(data)); } catch (e) { reject(new Error('Bad JSON: ' + data.slice(0, 100))); } }); }); req.on('error', reject); req.write(payload); req.end(); }); } function callEmit(company_id, event, data) { const payload = JSON.stringify({ event, data, company_id }); const options = { hostname: '127.0.0.1', port: NODE_PORT, path: '/emit', method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload), 'x-emit-secret': SECRET, }, }; const req = http.request(options); req.on('error', (e) => console.error(`[alert] emit failed company=${company_id}:`, e.message)); req.write(payload); req.end(); } // ── ETL GL Summary maintenance — runs every hour ───────────────────────────── // Checks and repairs etl_gl_summary (GL aggregation table). // Each company has a fixed slot = company_id % 24 — load is spread across 24 hours. // For etl_stock_summary add a separate schedule below when ready. cron.schedule('0 * * * *', async () => { const slot = new Date().getHours(); console.log(`[scheduler] etl_gl slot=${slot}`); try { const result = await callPhp('/cron/etl_gl_maintenance.php', { slot }); const summary = (result.results || []).map(r => r.error ? `company=${r.company_id} ERROR: ${r.error}` : `company=${r.company_id} gaps=${r.gaps_found} rebuilt=${r.periods_rebuilt}` ); if (summary.length === 0) { console.log(`[scheduler] etl_gl slot=${slot} — no companies`); } else { summary.forEach(s => console.log(`[scheduler] etl_gl ${s}`)); } } catch (e) { console.error('[scheduler] etl_gl_maintenance failed:', e.message); } }); // ── ETL Stock Summary maintenance — runs every hour ────────────────────────── // Checks and repairs etl_stock_summary (stock aggregation table). // Same slot-based distribution as GL: company_id % 24. cron.schedule('30 * * * *', async () => { const slot = new Date().getHours(); console.log(`[scheduler] etl_stock slot=${slot}`); try { const result = await callPhp('/cron/etl_stock_maintenance.php', { slot }); const summary = (result.results || []).map(r => r.error ? `company=${r.company_id} ERROR: ${r.error}` : `company=${r.company_id} gaps=${r.gaps_found} rebuilt=${r.periods_rebuilt}` ); if (summary.length === 0) { console.log(`[scheduler] etl_stock slot=${slot} — no companies`); } else { summary.forEach(s => console.log(`[scheduler] etl_stock ${s}`)); } } catch (e) { console.error('[scheduler] etl_stock_maintenance failed:', e.message); } }); // ── Low stock alert — runs daily at 08:00 ──────────────────────────────────── cron.schedule('0 8 * * *', async () => { console.log('[scheduler] alert_low_stock running'); try { const result = await callPhp('/cron/alert_low_stock.php', {}); for (const { company_id, count } of (result.alerts || [])) { const msg = count === 1 ? '1 product is below minimum stock level.' : `${count} products are below minimum stock level.`; callEmit(parseInt(company_id), 'notification', { message: msg, type: 'warning' }); console.log(`[alert] low_stock company=${company_id} count=${count}`); } } catch (e) { console.error('[scheduler] alert_low_stock failed:', e.message); } }); // ── Overdue invoice alert — runs daily at 09:00 ─────────────────────────────── cron.schedule('0 9 * * *', async () => { console.log('[scheduler] alert_overdue_invoices running'); try { const result = await callPhp('/cron/alert_overdue_invoices.php', {}); for (const { company_id, count } of (result.alerts || [])) { const msg = count === 1 ? '1 invoice is overdue and awaiting payment.' : `${count} invoices are overdue and awaiting payment.`; callEmit(parseInt(company_id), 'notification', { message: msg, type: 'danger' }); console.log(`[alert] overdue_invoices company=${company_id} count=${count}`); } } catch (e) { console.error('[scheduler] alert_overdue_invoices failed:', e.message); } }); console.log('[scheduler] running — etl_gl + etl_stock (hourly) | low_stock + overdue_invoices alerts (daily)');