// 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, '>'); } 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, 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 };