// 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, '>'); } function buildHtml({ title, lines, url }) { const rows = (lines || []).map(([k, v]) => `${esc(k)}${esc(v)}`).join(''); return `

${esc(title)}

${rows}
${url ? `

Open in Portal

` : ''}

Automated notification from the SAP ERP Portal — Production module.

`; } // 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 };