133 lines
7.3 KiB
JavaScript
133 lines
7.3 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);
|
|
}
|
|
|
|
// Item-Group-routed recipients (Admin → System Settings → "Notify by Item
|
|
// Group") — ALWAYS notified in addition to the step/module recipients above,
|
|
// keyed by the order's MAIN PRODUCT's SAP Item Group (ItmsGrpCod), not every
|
|
// component's group. Callers resolve the group code themselves (they already
|
|
// have SAP/HANA access; notifyStore.js deliberately doesn't) and pass it in
|
|
// as `itemGroupCode`.
|
|
function groupExtraEmailsFor(groupCode) {
|
|
if (groupCode == null || groupCode === '') return [];
|
|
const map = appSettingsStore.notifyItemGroupEmails();
|
|
return String(map[String(groupCode)] || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
}
|
|
|
|
function esc(s) { return String(s == null ? '' : s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); }
|
|
|
|
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, itemGroupCode, title, lines, url, excludeUsernames }) {
|
|
try {
|
|
if (!appSettingsStore.notifyEmailsEnabled()) return;
|
|
const exclU = new Set((excludeUsernames || []).filter(Boolean).map(u => u.toLowerCase()));
|
|
const emails = new Set();
|
|
let anyStageEnabled = false;
|
|
// Per-step opt-out (Admin → System Settings → "Stage-Change Email
|
|
// Notifications" → per-step list) — when a step/module is listed there,
|
|
// NOTHING fires for it: not the assigned users, not the fixed extra
|
|
// recipients either. Distinct from the global notifyEmailsEnabled
|
|
// switch checked above (silences everything) and a user's own
|
|
// emailNotify opt-out (silences everything for just them) — this keeps
|
|
// one specific stage silent for everyone, on purpose.
|
|
if (stepFullKey && appSettingsStore.isNotifyStepEnabled(stepFullKey)) {
|
|
anyStageEnabled = true;
|
|
(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 && appSettingsStore.isNotifyStepEnabled(moduleKeyOf(moduleKey))) {
|
|
anyStageEnabled = true;
|
|
(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()));
|
|
}
|
|
// Item-Group routing rides on the same stage being enabled at all — if
|
|
// this step/module's email is switched off entirely, the group-mapped
|
|
// recipients shouldn't fire for it either.
|
|
if (anyStageEnabled) groupExtraEmailsFor(itemGroupCode).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 };
|