Files
sap-erp/services/notifyStore.js
T
John 69b4e68baf
SAP-ERP Portal CI/CD / build (push) Failing after 5m20s
first commit
2026-09-23 17:31:02 +05:30

107 lines
5.7 KiB
JavaScript

// services/notifyStore.js
// Stage-change email notifications for the Production module (Work Order,
// Production Order, Issue for Production, Verify Work Order, Batch Issuance
// Intimation). Given an approval-step fullKey ("workflow:key") and/or a
// module key, works out who should be emailed — whoever currently holds that
// step/module (reverse-lookup over hanaUsers.listUsers(), there is no such
// lookup built into hanaUsers/auth today) PLUS any admin-configured fixed
// extra recipients (services/appSettingsStore.notifyExtraEmails()) — and
// sends via services/mailer.js. Every entry point here is fire-and-forget
// safe: it never throws, so a notification failure can never break the
// mutation that triggered it.
'use strict';
const hanaUsers = require('./hanaUsers');
const { normalizeSteps, ALL_PERMS } = require('../middleware/auth');
const appSettingsStore = require('./appSettingsStore');
const mailer = require('./mailer');
function moduleKeyOf(m) { return `module:${m}`; }
// Does this user explicitly hold `fullKey`? Deliberately NOT using
// middleware/auth's hasStepAssigned() — that helper auto-passes every admin
// for every step (a permission-check bypass, correct for gating actions),
// which would otherwise email admin@company.com on literally every stage
// transition in the whole app regardless of whether they're actually
// assigned to it. Email recipients should reflect real assignment only.
function isExplicitlyAssigned(user, fullKey) {
const steps = normalizeSteps(user?.approvalSteps);
const entry = steps.find(s => s.step === fullKey);
return !!entry && ALL_PERMS.some(p => entry.perms.includes(p));
}
// All active users holding ANY permission on approval step `fullKey`, who
// haven't opted out of email notifications (Admin → user → "Send
// stage-change email notifications to this user").
async function usersForStep(fullKey) {
const all = await hanaUsers.listUsers();
return all.filter(u => u.active && u.emailNotify !== false && isExplicitlyAssigned(u, fullKey));
}
// All active users granted a given sidebar module (public/sidebar.js's
// module keys, e.g. 'production-batch-issuance'). Admins are not
// auto-included — only users explicitly granted the module. Same
// email-notify opt-out as usersForStep() above.
async function usersForModule(moduleKey) {
const all = await hanaUsers.listUsers();
return all.filter(u => u.active && u.emailNotify !== false && Array.isArray(u.modules) && u.modules.includes(moduleKey));
}
function extraEmailsFor(key) {
const map = appSettingsStore.notifyExtraEmails();
return String(map[key] || '').split(',').map(s => s.trim()).filter(Boolean);
}
function esc(s) { return String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); }
function buildHtml({ title, lines, url }) {
const rows = (lines || []).map(([k, v]) => `<tr><td style="padding:3px 10px 3px 0;color:#667;font-size:13px">${esc(k)}</td><td style="padding:3px 0;font-size:13px;font-weight:600">${esc(v)}</td></tr>`).join('');
return `<div style="font-family:Segoe UI,Arial,sans-serif;max-width:520px">
<h2 style="margin:0 0 12px;font-size:16px;color:#1a2b4a">${esc(title)}</h2>
<table>${rows}</table>
${url ? `<p style="margin-top:16px"><a href="${esc(url)}" style="background:#1a6fff;color:#fff;text-decoration:none;padding:8px 16px;border-radius:6px;font-size:13px">Open in Portal</a></p>` : ''}
<p style="margin-top:20px;color:#98a;font-size:11px">Automated notification from the SAP ERP Portal — Production module.</p>
</div>`;
}
// Core dispatcher. `stepFullKey`/`moduleKey` may each be omitted; recipients
// from both are combined and de-duplicated. `excludeUsernames` drops the
// actor who just performed the action (no need to notify yourself) — by
// username rather than email since the JWT payload doesn't carry email.
async function notify({ stepFullKey, moduleKey, title, lines, url, excludeUsernames }) {
try {
if (!appSettingsStore.notifyEmailsEnabled()) return;
const exclU = new Set((excludeUsernames || []).filter(Boolean).map(u => u.toLowerCase()));
const emails = new Set();
if (stepFullKey) {
(await usersForStep(stepFullKey)).forEach(u => u.email && !exclU.has((u.username || '').toLowerCase()) && emails.add(u.email.toLowerCase()));
extraEmailsFor(stepFullKey).forEach(e => emails.add(e.toLowerCase()));
}
if (moduleKey) {
(await usersForModule(moduleKey)).forEach(u => u.email && !exclU.has((u.username || '').toLowerCase()) && emails.add(u.email.toLowerCase()));
extraEmailsFor(moduleKeyOf(moduleKey)).forEach(e => emails.add(e.toLowerCase()));
}
if (!emails.size) return;
await mailer.sendMail({ to: [...emails], subject: title, html: buildHtml({ title, lines, url }) });
} catch (e) {
console.error('[NOTIFY] failed:', e.message);
}
}
// Notify specific users directly by username (e.g. "tell the creator their
// order was rejected/closed") — bypasses step/module lookup entirely.
async function notifyUsernames(usernames, { title, lines, url }) {
try {
if (!appSettingsStore.notifyEmailsEnabled()) return;
const want = new Set((usernames || []).filter(Boolean).map(u => String(u).toLowerCase()));
if (!want.size) return;
const all = await hanaUsers.listUsers();
const emails = all.filter(u => u.active && u.emailNotify !== false && want.has((u.username || '').toLowerCase()) && u.email).map(u => u.email);
if (!emails.length) return;
await mailer.sendMail({ to: emails, subject: title, html: buildHtml({ title, lines, url }) });
} catch (e) {
console.error('[NOTIFY] notifyUsernames failed:', e.message);
}
}
module.exports = { notify, notifyUsernames, usersForStep, usersForModule, moduleKeyOf };