Skip to main content

Email Notifications

This scenario demonstrates building an automated email notification system for various business events.


Business Requirements

┌─────────────────────────────────────────────────────────────────────────────┐
│ NOTIFICATION REQUIREMENTS │
└─────────────────────────────────────────────────────────────────────────────┘

✓ Order confirmation to customers
✓ Low inventory alerts to purchasing
✓ Payment received notifications
✓ Overdue invoice reminders
✓ Daily summary reports
✓ Configurable recipients per event type

Solution Architecture

┌─────────────────────────────────────────────────────────────────────────────┐
│ NOTIFICATION ARCHITECTURE │
└─────────────────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────┐
│ TRIGGER EVENTS │
│ ────────────────────────────────────────────────────────────────│
│ • Record events (User Event scripts) │
│ • Scheduled events (Scheduled scripts) │
│ • Workflow actions │
└──────────────────────────────┬───────────────────────────────────┘


┌──────────────────────────────────────────────────────────────────┐
│ NOTIFICATION CONFIG │
│ (Custom Record) │
│ ────────────────────────────────────────────────────────────────│
│ • Event type │
│ • Recipients │
│ • Email template │
│ • Conditions │
└──────────────────────────────┬───────────────────────────────────┘


┌──────────────────────────────────────────────────────────────────┐
│ EMAIL SERVICE (Library) │
│ ────────────────────────────────────────────────────────────────│
│ • Template rendering │
│ • Recipient resolution │
│ • Email sending │
│ • Logging │
└──────────────────────────────────────────────────────────────────┘

Email Service Library

src/FileCabinet/SuiteScripts/Libraries/email_service_lib.js

/**
* @NApiVersion 2.1
* @NModuleScope SameAccount
* @description Centralized email notification service
*/
define(['N/email', 'N/render', 'N/search', 'N/record', 'N/runtime', 'N/log'],
(email, render, search, record, runtime, log) => {

/**
* Send notification based on event type
*/
const sendNotification = (eventType, recordType, recordId, additionalData = {}) => {
try {
// Get notification config
const config = getNotificationConfig(eventType);

if (!config || !config.isActive) {
log.debug('Notification Skipped', `No active config for: ${eventType}`);
return null;
}

// Load record for data
const rec = record.load({
type: recordType,
id: recordId
});

// Build email data
const emailData = buildEmailData(rec, config, additionalData);

// Get recipients
const recipients = resolveRecipients(config, rec);

if (recipients.length === 0) {
log.warn('No Recipients', `No recipients for notification: ${eventType}`);
return null;
}

// Render template if using template
let body;
if (config.templateId) {
body = renderTemplate(config.templateId, rec, additionalData);
} else {
body = buildDefaultBody(eventType, emailData);
}

// Send email
const emailId = email.send({
author: config.senderId || runtime.getCurrentUser().id,
recipients: recipients,
subject: replaceTokens(config.subject, emailData),
body: body,
relatedRecords: {
transactionId: recordType.includes('order') || recordType.includes('invoice') ? recordId : null,
entityId: rec.getValue({ fieldId: 'entity' }) || null
}
});

// Log notification
logNotification(eventType, recordId, recipients, emailId);

return emailId;

} catch (e) {
log.error('Notification Error', e.message);
throw e;
}
};

/**
* Get notification configuration
*/
const getNotificationConfig = (eventType) => {
const configSearch = search.create({
type: 'customrecord_notification_config',
filters: [
['custrecord_nc_event_type', 'is', eventType],
'AND',
['isinactive', 'is', 'F']
],
columns: [
'custrecord_nc_recipients',
'custrecord_nc_template',
'custrecord_nc_subject',
'custrecord_nc_sender',
'custrecord_nc_conditions'
]
});

let config = null;

configSearch.run().each((result) => {
config = {
isActive: true,
eventType: eventType,
recipients: result.getValue('custrecord_nc_recipients'),
templateId: result.getValue('custrecord_nc_template'),
subject: result.getValue('custrecord_nc_subject'),
senderId: result.getValue('custrecord_nc_sender'),
conditions: result.getValue('custrecord_nc_conditions')
};
return false;
});

return config;
};

/**
* Resolve recipients from config
*/
const resolveRecipients = (config, rec) => {
const recipients = [];
const recipientConfig = config.recipients;

if (!recipientConfig) return recipients;

// Parse recipient types
const parts = recipientConfig.split(',');

parts.forEach(part => {
const trimmed = part.trim();

if (trimmed === 'customer') {
const customerId = rec.getValue({ fieldId: 'entity' });
if (customerId) recipients.push(customerId);
} else if (trimmed === 'salesrep') {
const salesRep = rec.getValue({ fieldId: 'salesrep' });
if (salesRep) recipients.push(salesRep);
} else if (trimmed === 'creator') {
const creator = rec.getValue({ fieldId: 'createdby' });
if (creator) recipients.push(creator);
} else if (trimmed.includes('@')) {
// Direct email address
recipients.push(trimmed);
} else if (!isNaN(trimmed)) {
// Employee ID
recipients.push(parseInt(trimmed));
}
});

return [...new Set(recipients)]; // Remove duplicates
};

/**
* Build email data from record
*/
const buildEmailData = (rec, config, additionalData) => {
return {
recordId: rec.id,
recordType: rec.type,
tranId: rec.getValue({ fieldId: 'tranid' }) || '',
entity: rec.getText({ fieldId: 'entity' }) || '',
total: rec.getValue({ fieldId: 'total' }) || 0,
date: rec.getValue({ fieldId: 'trandate' }) || '',
status: rec.getText({ fieldId: 'status' }) || '',
memo: rec.getValue({ fieldId: 'memo' }) || '',
...additionalData
};
};

/**
* Replace tokens in string
*/
const replaceTokens = (template, data) => {
let result = template;

Object.keys(data).forEach(key => {
const regex = new RegExp(`\\{${key}\\}`, 'g');
result = result.replace(regex, data[key] || '');
});

return result;
};

/**
* Render email template
*/
const renderTemplate = (templateId, rec, additionalData) => {
const renderer = render.create();

renderer.setTemplateById({
id: templateId
});

renderer.addRecord({
templateName: 'record',
record: rec
});

Object.keys(additionalData).forEach(key => {
renderer.addCustomDataSource({
format: render.DataSource.OBJECT,
alias: key,
data: additionalData[key]
});
});

return renderer.renderAsString();
};

/**
* Build default email body
*/
const buildDefaultBody = (eventType, data) => {
const templates = {
'order_confirmation': `
<h2>Order Confirmation</h2>
<p>Thank you for your order!</p>
<p><strong>Order Number:</strong> ${data.tranId}</p>
<p><strong>Total:</strong> $${parseFloat(data.total).toFixed(2)}</p>
`,
'payment_received': `
<h2>Payment Received</h2>
<p>We have received your payment.</p>
<p><strong>Invoice:</strong> ${data.tranId}</p>
<p><strong>Amount:</strong> $${parseFloat(data.total).toFixed(2)}</p>
`,
'low_inventory': `
<h2>Low Inventory Alert</h2>
<p>The following item has low inventory:</p>
<p><strong>Item:</strong> ${data.itemName}</p>
<p><strong>Available:</strong> ${data.available} units</p>
`,
'overdue_invoice': `
<h2>Overdue Invoice Reminder</h2>
<p>Invoice ${data.tranId} is past due.</p>
<p><strong>Amount Due:</strong> $${parseFloat(data.amountDue).toFixed(2)}</p>
<p><strong>Days Overdue:</strong> ${data.daysOverdue}</p>
`
};

return templates[eventType] || `<p>Notification: ${eventType}</p>`;
};

/**
* Log notification for audit
*/
const logNotification = (eventType, recordId, recipients, emailId) => {
try {
const logRecord = record.create({
type: 'customrecord_notification_log'
});

logRecord.setValue({ fieldId: 'custrecord_nl_event', value: eventType });
logRecord.setValue({ fieldId: 'custrecord_nl_record', value: recordId });
logRecord.setValue({ fieldId: 'custrecord_nl_recipients', value: JSON.stringify(recipients) });
logRecord.setValue({ fieldId: 'custrecord_nl_date', value: new Date() });

logRecord.save();
} catch (e) {
log.error('Log Error', e.message);
}
};

return {
sendNotification
};
});

User Event - Order Confirmation

src/FileCabinet/SuiteScripts/UserEvents/order_notification_ue.js

/**
* @NApiVersion 2.1
* @NScriptType UserEventScript
*/
define(['./Libraries/email_service_lib', 'N/log'],
(emailService, log) => {

const afterSubmit = (context) => {
if (context.type !== context.UserEventType.CREATE) {
return;
}

try {
const salesOrder = context.newRecord;

// Send order confirmation
emailService.sendNotification(
'order_confirmation',
salesOrder.type,
salesOrder.id
);

log.audit('Order Confirmation', `Sent for order ${salesOrder.id}`);

} catch (e) {
log.error('Notification Error', e.message);
}
};

return { afterSubmit };
});

Scheduled Script - Overdue Reminders

src/FileCabinet/SuiteScripts/ScheduledScripts/overdue_reminders_ss.js

/**
* @NApiVersion 2.1
* @NScriptType ScheduledScript
*/
define(['N/search', 'N/email', 'N/runtime', 'N/log'],
(search, email, runtime, log) => {

const execute = (context) => {
log.audit('Overdue Reminders', 'Starting');

const overdueSearch = search.create({
type: search.Type.INVOICE,
filters: [
['mainline', 'is', 'T'],
'AND',
['status', 'anyof', 'CustInvc:A'], // Open
'AND',
['duedate', 'before', 'today']
],
columns: [
'tranid',
'entity',
'total',
'amountremaining',
'duedate',
'formulanumeric: {today} - {duedate}'
]
});

overdueSearch.run().each((result) => {
sendOverdueReminder(result);
return true;
});

log.audit('Overdue Reminders', 'Completed');
};

const sendOverdueReminder = (invoice) => {
const customerId = invoice.getValue('entity');
const tranId = invoice.getValue('tranid');
const amountDue = invoice.getValue('amountremaining');
const daysOverdue = Math.floor(invoice.getValue({
name: 'formulanumeric',
formula: '{today} - {duedate}'
}));

const body = `
<html>
<body>
<h2>Payment Reminder</h2>
<p>This is a reminder that invoice ${tranId} is past due.</p>
<table style="border-collapse: collapse; margin: 20px 0;">
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong>Invoice:</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${tranId}</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong>Amount Due:</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">$${parseFloat(amountDue).toFixed(2)}</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong>Days Overdue:</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">${daysOverdue}</td>
</tr>
</table>
<p>Please arrange payment at your earliest convenience.</p>
</body>
</html>
`;

email.send({
author: runtime.getCurrentUser().id,
recipients: [customerId],
subject: `Payment Reminder: Invoice ${tranId} is Past Due`,
body: body,
relatedRecords: {
transactionId: invoice.id
}
});

log.debug('Reminder Sent', `Invoice ${tranId} - ${daysOverdue} days overdue`);
};

return { execute };
});

Notification Config Record

<?xml version="1.0" encoding="UTF-8"?>
<customrecordtype scriptid="customrecord_notification_config">
<recordname>Notification Configuration</recordname>

<customrecordcustomfields>
<customrecordcustomfield scriptid="custrecord_nc_event_type">
<label>Event Type</label>
<fieldtype>TEXT</fieldtype>
<ismandatory>T</ismandatory>
</customrecordcustomfield>

<customrecordcustomfield scriptid="custrecord_nc_recipients">
<label>Recipients</label>
<fieldtype>TEXTAREA</fieldtype>
<description>Comma-separated: customer, salesrep, email@domain.com, employee_id</description>
</customrecordcustomfield>

<customrecordcustomfield scriptid="custrecord_nc_subject">
<label>Subject Template</label>
<fieldtype>TEXT</fieldtype>
</customrecordcustomfield>

<customrecordcustomfield scriptid="custrecord_nc_template">
<label>Email Template</label>
<fieldtype>SELECT</fieldtype>
<selectrecordtype>-120</selectrecordtype>
</customrecordcustomfield>

<customrecordcustomfield scriptid="custrecord_nc_sender">
<label>Sender</label>
<fieldtype>SELECT</fieldtype>
<selectrecordtype>-4</selectrecordtype>
</customrecordcustomfield>
</customrecordcustomfields>
</customrecordtype>

Next Steps