Files
John eead8f5ffd
SAP-ERP Portal CI/CD / build (push) Successful in 3m57s
sale order
2026-10-05 18:45:17 +05:30

187 lines
14 KiB
JavaScript

// 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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); }
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 `<div style="font-family:Segoe UI,Arial,sans-serif;font-size:14px;color:#1f2937">
<h3 style="margin:0 0 10px;color:#0f3d91">${esc(title)}</h3>
${lines.map(l => `<p style="margin:4px 0">${l}</p>`).join('')}
${link ? `<p style="margin:14px 0 0"><a href="${esc(link)}" style="background:#1a6fff;color:#fff;padding:8px 14px;border-radius:6px;text-decoration:none">Open</a></p>` : ''}
<p style="margin-top:18px;font-size:12px;color:#6b7280">Mitra Industries Pvt. Ltd. — Sales Order Portal</p></div>`;
}
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 = `<div style="background:#fff7e6;border:1px solid #f5a623;padding:8px 10px;margin-bottom:12px;font-size:12px">
<b>TEST MODE</b> — this email was NOT sent to its real recipients.<br>Would have gone to: <b>${list.length ? list.map(esc).join(', ') : '(nobody — no recipient found)'}</b></div>`;
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 = `<b>${esc(order.cardName)}</b> (${esc(order.cardCode)})`;
const ref = `<b>${esc(order.refNo)}</b>`;
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: <b>₹ ${Number(order.grandTotal).toLocaleString('en-IN', { minimumFractionDigits: 2 })}</b>.`, '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 = `<table style="border-collapse:collapse;font-size:13px;margin:6px 0" cellpadding="5">
<tr style="background:#eef3ff"><th align="left">Item</th><th align="left">Description</th><th align="right">Qty</th></tr>
${lines.map(l => `<tr><td style="border-top:1px solid #e5e7eb">${esc(l.itemCode)}</td><td style="border-top:1px solid #e5e7eb">${esc(l.description)}</td><td align="right" style="border-top:1px solid #e5e7eb">${esc(l.qty)}</td></tr>`).join('')}</table>`;
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 <b>${esc(order.refNo)}</b> of <b>${esc(order.cardName)}</b> (${esc(order.cardCode)})${order.custOrderNo ? ` — your ref. <b>${esc(order.custOrderNo)}</b>` : ''}.`,
`Invoice no.: <b>${esc(inv.invoiceNo)}</b> &nbsp; Date: <b>${esc(fmt(inv.invoiceDate))}</b> &nbsp; Amount: <b>${money(inv.total)}</b>`,
table,
d.lrNo || d.transporter || d.vehicle ? `Dispatch: LR/GR/AWB <b>${esc(d.lrNo || '—')}</b>${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: <a href="${esc(link)}">${esc(link)}</a>`,
`Customer code: <b>${esc(cardCode)}</b>`,
password ? `Temporary password: <b>${esc(password)}</b> — 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 };