// services/sales/notify.js // Email routing for the Sales Order module. Mirrors msale's recipients // (BUH for the division, Logistics, Accounts, the customer, mapped sales // persons as CC) but resolves "who is BUH/Logistics/Accounts" from the ERP's // Approval Steps + each user's Sales divisions instead of hardcoded user_type. // Never throws — a mail problem must not break an order action. 'use strict'; const { sendMail } = require('../mailer'); const { usersForStep } = require('../notifyStore'); const masters = require('./masters'); const { query } = require('./db'); const appSettingsStore = require('../appSettingsStore'); function esc(s) { return String(s == null ? '' : s).replace(/&/g, '&').replace(//g, '>'); } const base = () => (process.env.APP_BASE_URL || '').replace(/\/$/, ''); // Active users explicitly assigned `stepKey` whose sales divisions include // `divisionId` (users with no division restriction configured are skipped // unless they're admin, so a BUH of TM isn't mailed about PD orders). async function emailsForStep(stepKey, divisionId) { try { // Admin → System Settings → per-step email opt-out (same list the // Production module honours) — a listed step mails nobody. if (!appSettingsStore.isNotifyStepEnabled(stepKey)) return []; const users = await usersForStep(stepKey); if (!users.length) return []; const profs = await query(`SELECT USER_ID, DIVISIONS FROM dbo.ZSO_USER_PROFILES`); const divMap = new Map(profs.map(p => [p.USER_ID, String(p.DIVISIONS || '').split(',').map(Number).filter(Boolean)])); return users.filter(u => u.email && (u.role === 'admin' || !divisionId || (divMap.get(u.id) || []).includes(Number(divisionId)))).map(u => u.email); } catch (e) { console.warn('[SALES-NOTIFY] recipient lookup failed:', e.message); return []; } } function html({ title, lines, link }) { return `

${esc(title)}

${lines.map(l => `

${l}

`).join('')} ${link ? `

Open

` : ''}

Mitra Industries Pvt. Ltd. — Sales Order Portal

`; } async function send(to, cc, subject, body) { // Two switches, both must be on: the ERP-wide email switch (Admin → // System Settings → Stage-Change Email Notifications) and the Sales-only // switch (Sales Admin → Settings → "Send Sales email notifications"). if (!appSettingsStore.notifyEmailsEnabled()) return; const s = await masters.getSettings(); if (s.emailNotifications === false) return; const list = [...new Set([...(to || []), ...(cc || []), ...String(s.orderEmailCc || '').split(',')].map(x => String(x || '').trim()).filter(Boolean))]; // Test mode never falls through to real recipients: if it's on, the mail // goes to the test address only (even when nobody real would have got it, // so routing gaps are visible too) — or nowhere if no test address is set. if (s.emailTestMode !== false) { const testTo = String(s.emailTestRecipient || '').split(',').map(x => x.trim()).filter(Boolean); if (!testTo.length) { console.log(`[SALES-NOTIFY] test mode, no test recipient — skipped "${subject}"`); return; } const note = `
TEST MODE — this email was NOT sent to its real recipients.
Would have gone to: ${list.length ? list.map(esc).join(', ') : '(nobody — no recipient found)'}
`; try { await sendMail({ to: testTo, subject: `[TEST] ${subject}`, html: note + html(body) }); } catch (e) { console.warn('[SALES-NOTIFY] send failed:', e.message); } return; } if (!list.length) return; try { await sendMail({ to: list, subject, html: html(body) }); } catch (e) { console.warn('[SALES-NOTIFY] send failed:', e.message); } } // Order status-change notifications. `order` is the full order object. async function orderEvent(event, order, { customerEmail = '', remarks = '' } = {}) { try { const div = order.divisionId; const spCc = await masters.salesPersonEmailsFor(order.cardCode, div); const who = `${esc(order.cardName)} (${esc(order.cardCode)})`; const ref = `${esc(order.refNo)}`; const empLink = `${base()}/sales-orders?id=${order.id}`; const custLink = `${base()}/customer-portal#order/${order.id}`; const rem = remarks ? [`Remarks: ${esc(remarks)}`] : []; switch (event) { case 'created': { if (order.orderType === 'DIRECT') { return send(await emailsForStep('sales_order:logistics_confirm', div), [], `Direct Sales Order ${order.refNo} generated`, { title: 'Direct sales order generated', lines: [`${esc(order.createdByName)} generated direct order ${ref} for ${who}.`, 'Please confirm quantity & dispatch time.'], link: empLink }); } const to = order.status === 2 ? await emailsForStep('sales_order:logistics_confirm', div) : await emailsForStep('sales_order:review', div); return send(to, spCc, `Sales Order ${order.refNo} generated`, { title: 'New sales order', lines: [`${who} placed sales order ${ref}.`, order.status === 2 ? 'Prices are standard — sent straight to Logistics for confirmation.' : 'Special prices were requested — please review.'], link: empLink }); } case 'to_logistics': return send(await emailsForStep('sales_order:logistics_confirm', div), spCc, `Confirm quantity & dispatch date — ${order.refNo}`, { title: 'Confirmation required', lines: [`Please confirm quantity & dispatch date for order ${ref} of ${who}.`, ...rem], link: empLink }); case 'qty_confirmed': return send([...await emailsForStep('sales_order:final_approve', div), ...await emailsForStep('sales_order:review', div)], [], `Logistics confirmed ${order.refNo}`, { title: 'Quantity & dispatch time confirmed', lines: [`Logistics confirmed order ${ref} of ${who}.`, ...rem], link: empLink }); case 'approved': return send([customerEmail], [...spCc, ...await emailsForStep('sales_order:logistics_confirm', div)], `Sales Order ${order.refNo} approved — please make payment`, { title: 'Order approved', lines: [`Your sales order ${ref} is approved.`, `Amount payable: ₹ ${Number(order.grandTotal).toLocaleString('en-IN', { minimumFractionDigits: 2 })}.`, 'Please log in and submit the payment details.'], link: custLink }); case 'modify': return send(order.orderType === 'DIRECT' ? await emailsForStep('sales_order:create_direct', div) : [customerEmail], [...spCc, ...await emailsForStep('sales_order:review', div)], `Modification required — ${order.refNo}`, { title: 'Order sent back for modification', lines: [`Order ${ref} of ${who} needs modification.`, ...rem], link: order.orderType === 'DIRECT' ? empLink : custLink }); case 'payment_submitted': return send(await emailsForStep('sales_order:payment_verify', div), [...spCc, ...await emailsForStep('sales_order:review', div)], `Payment submitted for ${order.refNo} — verification required`, { title: 'Payment submitted', lines: [`${who} submitted payment for order ${ref}.`, `Mode: ${esc(order.paymentMode)} · Ref: ${esc(order.paymentRef)} · Amount: ₹ ${esc(order.paidAmount)}`], link: `${base()}/sales-orders?tab=payments` }); case 'payment_received': return send([customerEmail], [...spCc, ...await emailsForStep('sales_order:sap_post', div)], `Payment received for ${order.refNo}`, { title: 'Payment received', lines: [`Payment for order ${ref} is confirmed. The order is now being processed for invoicing.`], link: custLink }); case 'payment_rejected': return send([customerEmail], spCc, `Payment not verified for ${order.refNo}`, { title: 'Payment could not be verified', lines: [`The payment submitted for order ${ref} could not be verified. Please check and submit again.`, ...rem], link: custLink }); case 'ready_for_sap': return send(await emailsForStep('sales_order:sap_post', div), [], `Order ${order.refNo} ready to upload in SAP`, { title: 'Ready for SAP', lines: [`Order ${ref} of ${who} is ready to be posted to SAP.`], link: empLink }); case 'in_sap': return send([customerEmail], spCc, `Order ${order.refNo} placed in SAP`, { title: 'Order placed', lines: [`Your order ${ref} has been placed (SAP SO ${esc(order.sapDocNum || '')}).`], link: custLink }); case 'cancelled': return send([customerEmail], [...spCc, ...await emailsForStep('sales_order:review', div)], `Sales Order ${order.refNo} cancelled`, { title: 'Order cancelled', lines: [`Order ${ref} of ${who} has been cancelled.`, ...rem], link: custLink }); default: return undefined; } } catch (e) { console.warn('[SALES-NOTIFY] orderEvent failed:', e.message); } } async function sampleEvent(kind, title, lines, stepKey, divisionId, extraTo = []) { try { const to = [...(stepKey ? await emailsForStep(stepKey, divisionId) : []), ...extraTo]; await send(to, [], title, { title, lines, link: `${base()}/sales-samples${kind === 'export' ? '?tab=export' : ''}` }); } catch (e) { console.warn('[SALES-NOTIFY] sampleEvent failed:', e.message); } } // "Invoice raised" — to the customer and everyone concerned with the order: // mapped sales persons, BUH of the category, Logistics, Accounts and (Direct // orders) the employee who raised it. async function invoiceEvent(order, inv, lines, { customerEmail = '' } = {}) { try { const div = order.divisionId; const staff = [ ...await masters.salesPersonEmailsFor(order.cardCode, div), ...await emailsForStep('sales_order:review', div), ...await emailsForStep('sales_order:logistics_confirm', div), ...await emailsForStep('sales_order:payment_verify', div), ]; if (order.createdByType === 'user' && order.createdBy) { const u = await query(`SELECT EMAIL FROM dbo.ZCUST_USERS WHERE ID=?`, [parseInt(order.createdBy) || 0]).catch(() => []); if (u[0] && u[0].EMAIL) staff.push(u[0].EMAIL); } const d = inv.dispatch || {}; const fmt = v => (v ? new Date(v).toLocaleDateString('en-GB').replace(/\//g, '-') : ''); const money = v => '₹ ' + Number(v || 0).toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); const table = ` ${lines.map(l => ``).join('')}
ItemDescriptionQty
${esc(l.itemCode)}${esc(l.description)}${esc(l.qty)}
`; await send([customerEmail], staff, `Invoice ${inv.invoiceNo} raised for your order ${order.refNo}`, { title: 'Invoice raised', lines: [ `An invoice has been raised against sales order ${esc(order.refNo)} of ${esc(order.cardName)} (${esc(order.cardCode)})${order.custOrderNo ? ` — your ref. ${esc(order.custOrderNo)}` : ''}.`, `Invoice no.: ${esc(inv.invoiceNo)}   Date: ${esc(fmt(inv.invoiceDate))}   Amount: ${money(inv.total)}`, table, d.lrNo || d.transporter || d.vehicle ? `Dispatch: LR/GR/AWB ${esc(d.lrNo || '—')}${d.lrDate ? ' dated ' + esc(fmt(d.lrDate)) : ''}${d.transporter ? ' · ' + esc(d.transporter) : ''}${d.vehicle ? ' · vehicle ' + esc(d.vehicle) : ''}${d.ewayBill ? ' · e-Way bill ' + esc(d.ewayBill) : ''}` : '', 'The invoice copy and COA certificates can be downloaded from the order in the Customer Portal.', ].filter(Boolean), link: `${base()}/customer-portal#order/${order.id}`, }); } catch (e) { console.warn('[SALES-NOTIFY] invoiceEvent failed:', e.message); } } // Customer portal login enabled / password reset by MIPL. The password is a // one-time password — the customer is forced to change it at first login. async function customerLoginEvent(kind, { cardCode, cardName, email, password }) { try { const link = `${base()}/customer-portal`; const created = kind === 'created'; await send([email], [], created ? 'Your MIPL Customer Portal login' : 'Your MIPL Customer Portal password was reset', { title: created ? 'Welcome to the MIPL Customer Portal' : 'Password reset', lines: [ `Dear ${esc(cardName || cardCode)},`, created ? 'A login has been created for you on the Mitra Industries Customer Portal, where you can place and track sales orders, make payments, view your ledger and download invoices & COA certificates.' : 'Your Customer Portal password has been reset by MIPL.', `Portal: ${esc(link)}`, `Customer code: ${esc(cardCode)}`, password ? `Temporary password: ${esc(password)} — you will be asked to set your own password when you log in.` : '', 'If you did not expect this email, please contact your MIPL sales representative.', ].filter(Boolean), link, }); } catch (e) { console.warn('[SALES-NOTIFY] customerLoginEvent failed:', e.message); } } module.exports = { orderEvent, sampleEvent, emailsForStep, customerLoginEvent, invoiceEvent };