Files
wms-app/nodejs/scheduler.js
T

146 lines
5.9 KiB
JavaScript

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 || '/mn3wms/app';
const SECRET = process.env.EMIT_SECRET || '';
const NODE_PORT = process.env.PORT || 3000;
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)');