107 lines
5.7 KiB
JavaScript
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, '&').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, 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 };
|