SuiteScript Tests - Scheduled & Map/Reduce
This page contains QuickMart Scheduled Script and Map/Reduce test cases.
Test Case #15: Scheduled Script - Daily Low Stock Alert
Objective: Check inventory levels daily and create purchase orders for low stock items.
Script Configuration
/**
* @NApiVersion 2.1
* @NScriptType ScheduledScript
*
* QuickMart - Daily Low Stock Alert
* Runs: Daily at 6:00 AM
*/
define(['N/search', 'N/record', 'N/email', 'N/log', 'N/runtime'], function(search, record, email, log, runtime) {
function execute(context) {
var lowStockItems = findLowStockItems();
if (lowStockItems.length === 0) {
log.audit('Low Stock Check', 'All items above reorder level');
return;
}
// Group by preferred vendor
var vendorGroups = groupByVendor(lowStockItems);
// Create PO for each vendor
for (var vendorId in vendorGroups) {
createPurchaseOrder(vendorId, vendorGroups[vendorId]);
}
// Send email summary
sendSummaryEmail(lowStockItems);
}
function findLowStockItems() {
var items = [];
search.create({
type: search.Type.INVENTORY_ITEM,
filters: [
['custitem_reorder_level', 'isnotempty', ''],
'AND',
['quantityavailable', 'lessthan', 'custitem_reorder_level']
],
columns: [
'itemid',
'displayname',
'quantityavailable',
'custitem_reorder_level',
'preferredvendor'
]
}).run().each(function(result) {
items.push({
id: result.id,
name: result.getValue('displayname') || result.getValue('itemid'),
available: result.getValue('quantityavailable'),
reorderLevel: result.getValue('custitem_reorder_level'),
vendor: result.getValue('preferredvendor')
});
return true;
});
return items;
}
function groupByVendor(items) {
var groups = {};
items.forEach(function(item) {
var vendor = item.vendor || 'NO_VENDOR';
if (!groups[vendor]) groups[vendor] = [];
groups[vendor].push(item);
});
return groups;
}
function createPurchaseOrder(vendorId, items) {
if (vendorId === 'NO_VENDOR') {
log.audit('Skipped', 'Items without vendor: ' + items.length);
return;
}
var po = record.create({
type: record.Type.PURCHASE_ORDER,
isDynamic: true
});
po.setValue('entity', vendorId);
po.setValue('memo', 'Auto-generated from Low Stock Alert');
items.forEach(function(item) {
po.selectNewLine('item');
po.setCurrentSublistValue('item', 'item', item.id);
var reorderQty = (item.reorderLevel * 2) - item.available; // Order to 2x reorder level
po.setCurrentSublistValue('item', 'quantity', reorderQty);
po.commitLine('item');
});
var poId = po.save();
log.audit('PO Created', 'PO ID: ' + poId + ' for vendor ' + vendorId);
}
function sendSummaryEmail(items) {
var body = 'Low Stock Alert Summary\n\n';
body += 'Items below reorder level: ' + items.length + '\n\n';
items.forEach(function(item) {
body += '- ' + item.name + ': ' + item.available + ' (min: ' + item.reorderLevel + ')\n';
});
email.send({
author: runtime.getCurrentUser().id,
recipients: runtime.getCurrentScript().getParameter('custscript_alert_email'),
subject: 'QuickMart Low Stock Alert - ' + new Date().toLocaleDateString(),
body: body
});
}
return {
execute: execute
};
});
Deployment Schedule
Script Deployment:
Name: Daily Low Stock Check
ID: customdeploy_qm_lowstock
Status: Scheduled
Schedule:
Type: Daily
Time: 6:00 AM
Parameters:
custscript_alert_email: purchasing@quickmart.com
Test Case #16: Scheduled Script - Pull Exchange Rates (Integration)
Objective: Fetch daily exchange rates from external API.
Script Configuration
/**
* @NApiVersion 2.1
* @NScriptType ScheduledScript
*
* QuickMart - Pull Exchange Rates
* Integration Pattern: PULL IN
*/
define(['N/https', 'N/record', 'N/log', 'N/runtime'], function(https, record, log, runtime) {
const EXCHANGE_API = 'https://quickmart-warehouse-api.appsvein.workers.dev/api/rates';
const API_KEY_PARAM = 'custscript_external_api_key';
function execute(context) {
try {
var response = https.get({
url: EXCHANGE_API,
headers: {
'X-API-Key': runtime.getCurrentScript().getParameter(API_KEY_PARAM)
}
});
var data = JSON.parse(response.body);
updateCurrencyRates(data.rates);
log.audit('Exchange Rates', 'Updated successfully');
} catch (e) {
log.error('Exchange Rate Error', e.message);
}
}
function updateCurrencyRates(rates) {
// Update specific currencies QuickMart uses
var currencies = ['EUR', 'GBP', 'SGD', 'IDR'];
currencies.forEach(function(curr) {
if (rates[curr]) {
try {
// Update currency exchange rate
// Note: This may require specific setup
log.audit('Rate Update', curr + ': ' + rates[curr]);
} catch (e) {
log.error('Rate Update Failed', curr + ': ' + e.message);
}
}
});
}
return {
execute: execute
};
});
Test Case #17: Map/Reduce - Bulk Invoice Generator
Objective: Generate invoices for all fulfilled orders at month end.
Scenario
Tom (Accountant) spends 2 days creating invoices manually. This script automates the process.
Script Configuration
/**
* @NApiVersion 2.1
* @NScriptType MapReduceScript
*
* QuickMart - Bulk Invoice Generator
* Creates invoices for all fulfilled orders
*/
define(['N/search', 'N/record', 'N/log', 'N/email', 'N/runtime'], function(search, record, log, email, runtime) {
function getInputData() {
// Find all Sales Orders that are fulfilled but not invoiced
return search.create({
type: search.Type.SALES_ORDER,
filters: [
['status', 'anyof', 'SalesOrd:C'], // Pending Billing
'AND',
['mainline', 'is', 'T']
],
columns: ['tranid', 'entity', 'total']
});
}
function map(context) {
var searchResult = JSON.parse(context.value);
var orderId = searchResult.id;
try {
// Transform Sales Order to Invoice
var invoice = record.transform({
fromType: record.Type.SALES_ORDER,
fromId: orderId,
toType: record.Type.INVOICE
});
var invoiceId = invoice.save();
context.write({
key: orderId,
value: {
success: true,
invoiceId: invoiceId
}
});
} catch (e) {
context.write({
key: orderId,
value: {
success: false,
error: e.message
}
});
}
}
function reduce(context) {
// Not needed for this script
}
function summarize(summary) {
var created = 0;
var errors = 0;
var errorDetails = [];
summary.output.iterator().each(function(key, value) {
var result = JSON.parse(value);
if (result.success) {
created++;
} else {
errors++;
errorDetails.push('SO ' + key + ': ' + result.error);
}
return true;
});
log.audit('Bulk Invoice Complete', 'Created: ' + created + ', Errors: ' + errors);
// Send summary email
email.send({
author: runtime.getCurrentUser().id,
recipients: 'accounting@quickmart.com',
subject: 'Bulk Invoice Generation Complete',
body: 'Invoices Created: ' + created + '\n' +
'Errors: ' + errors + '\n\n' +
(errorDetails.length > 0 ? 'Error Details:\n' + errorDetails.join('\n') : '')
});
}
return {
getInputData: getInputData,
map: map,
reduce: reduce,
summarize: summarize
};
});
Test Case #18: Map/Reduce - Mass Price Update (Integration)
Objective: Update item prices from supplier catalog API.
Script Configuration
/**
* @NApiVersion 2.1
* @NScriptType MapReduceScript
*
* QuickMart - Mass Price Update
* Integration Pattern: PULL IN (High Volume)
*/
define(['N/https', 'N/record', 'N/log', 'N/runtime'], function(https, record, log, runtime) {
const SUPPLIER_API = 'https://quickmart-warehouse-api.appsvein.workers.dev/api/catalog';
const API_KEY_PARAM = 'custscript_external_api_key';
function getInputData() {
// Fetch catalog from supplier
var response = https.get({
url: SUPPLIER_API,
headers: {
'X-API-Key': runtime.getCurrentScript().getParameter(API_KEY_PARAM)
}
});
var catalog = JSON.parse(response.body);
// Return array of items to process
return catalog.items;
}
function map(context) {
var item = JSON.parse(context.value);
context.write({
key: item.sku,
value: item
});
}
function reduce(context) {
var sku = context.key;
var itemData = JSON.parse(context.values[0]);
try {
// Find NetSuite item by SKU
var itemId = findItemBySku(sku);
if (itemId) {
record.submitFields({
type: record.Type.INVENTORY_ITEM,
id: itemId,
values: {
'cost': itemData.cost,
'baseprice': itemData.price
}
});
log.audit('Price Updated', sku + ': $' + itemData.price);
}
} catch (e) {
log.error('Price Update Failed', sku + ': ' + e.message);
}
}
function findItemBySku(sku) {
var results = search.create({
type: search.Type.INVENTORY_ITEM,
filters: [['itemid', 'is', sku]],
columns: ['internalid']
}).run().getRange({ start: 0, end: 1 });
return results.length > 0 ? results[0].id : null;
}
function summarize(summary) {
log.audit('Price Update Complete',
'Processed: ' + summary.reduceSummary.keys.length);
}
return {
getInputData: getInputData,
map: map,
reduce: reduce,
summarize: summarize
};
});
Test Case #28: Scheduled Script - Weekly Pending PR Report
Objective: Send weekly summary of pending Purchase Requests to managers.
Script Configuration
/**
* @NApiVersion 2.1
* @NScriptType ScheduledScript
*
* QuickMart - Weekly Pending PR Report
* Runs: Every Monday at 8:00 AM
*/
define(['N/search', 'N/email', 'N/runtime', 'N/log'], function(search, email, runtime, log) {
const MANAGER_EMAIL = 'sarah@quickmart.com'; // Or use role/employee lookup
function execute(context) {
var pendingPRs = findPendingPRs();
if (pendingPRs.length === 0) {
log.audit('Weekly PR Report', 'No pending PRs');
return;
}
var emailBody = buildReportHtml(pendingPRs);
email.send({
author: runtime.getCurrentUser().id,
recipients: MANAGER_EMAIL,
subject: 'Weekly Report: ' + pendingPRs.length + ' Purchase Requests Pending',
body: emailBody
});
log.audit('Weekly PR Report Sent', pendingPRs.length + ' PRs reported');
}
function findPendingPRs() {
var prs = [];
search.create({
type: 'customrecord_purch_req',
filters: [
['custrecord_pr_status', 'anyof', ['2']] // Pending Approval
],
columns: [
'name',
'custrecord_pr_number',
'custrecord_pr_requestor',
'custrecord_pr_dept',
'custrecord_pr_total',
'created',
search.createColumn({ name: 'created', sort: search.Sort.ASC })
]
}).run().each(function(result) {
var created = new Date(result.getValue('created'));
var daysOld = Math.floor((new Date() - created) / (1000 * 60 * 60 * 24));
prs.push({
id: result.id,
number: result.getValue('custrecord_pr_number'),
requestor: result.getText('custrecord_pr_requestor'),
dept: result.getText('custrecord_pr_dept'),
total: result.getValue('custrecord_pr_total'),
created: result.getValue('created'),
daysOld: daysOld,
overdue: daysOld > 3 // Flag if waiting > 3 days
});
return true;
});
return prs;
}
function buildReportHtml(prs) {
var html = '<h2>Pending Purchase Requests</h2>';
html += '<p>The following PRs are awaiting approval:</p>';
html += '<table border="1" cellpadding="5" style="border-collapse: collapse;">';
html += '<tr style="background-color: #f0f0f0;">';
html += '<th>PR #</th><th>Requestor</th><th>Department</th><th>Amount</th><th>Days Waiting</th>';
html += '</tr>';
var totalAmount = 0;
var overdueCount = 0;
prs.forEach(function(pr) {
var rowStyle = pr.overdue ? 'background-color: #ffcccc;' : '';
html += '<tr style="' + rowStyle + '">';
html += '<td>' + pr.number + '</td>';
html += '<td>' + pr.requestor + '</td>';
html += '<td>' + pr.dept + '</td>';
html += '<td>$' + parseFloat(pr.total).toFixed(2) + '</td>';
html += '<td>' + pr.daysOld + (pr.overdue ? ' ⚠️' : '') + '</td>';
html += '</tr>';
totalAmount += parseFloat(pr.total) || 0;
if (pr.overdue) overdueCount++;
});
html += '</table>';
html += '<p><strong>Total Pending:</strong> $' + totalAmount.toFixed(2) + '</p>';
if (overdueCount > 0) {
html += '<p style="color: red;"><strong>⚠️ ' + overdueCount + ' PRs have been waiting more than 3 days!</strong></p>';
}
return html;
}
return { execute: execute };
});
Deployment
Script: Weekly Pending PR Report
Schedule: Every Monday at 8:00 AM
Status: Active
Test Steps
- Create several PRs with status "Pending Approval"
- Run script manually (or wait for Monday)
- Verify: Email received with PR summary table
- Verify: Overdue PRs highlighted in red
Test Case #29: Map/Reduce - Overdue Invoice Reminder Emails
Objective: Send reminder emails to customers with invoices that are overdue and not fully paid.
Script Configuration
/**
* @NApiVersion 2.1
* @NScriptType MapReduceScript
*
* QuickMart - Overdue Invoice Reminder Emails
* Sends reminder emails for open invoices past due date
*/
define(['N/search', 'N/email', 'N/runtime', 'N/log'], function(search, email, runtime, log) {
function getInputData() {
return search.create({
type: search.Type.INVOICE,
filters: [
['mainline', 'is', 'T'],
'AND',
['status', 'anyof', 'CustInvc:A'], // Open
'AND',
['amountremaining', 'greaterthan', '0.00'],
'AND',
['duedate', 'before', 'today']
],
columns: [
'internalid',
'tranid',
'entity',
'email',
'duedate',
'amountremaining'
]
});
}
function map(context) {
var result = JSON.parse(context.value);
var customerId = result.values.entity.value;
context.write({
key: customerId,
value: {
invoiceId: result.id,
invoiceNumber: result.values.tranid,
dueDate: result.values.duedate,
amountRemaining: result.values.amountremaining,
customerName: result.values.entity.text,
customerEmail: result.values.email
}
});
}
function reduce(context) {
var invoices = context.values.map(function(v) { return JSON.parse(v); });
var customerEmail = invoices[0].customerEmail;
var customerName = invoices[0].customerName;
if (!customerEmail) {
log.error('Missing Customer Email', 'Customer ID ' + context.key + ' has overdue invoices but no email.');
return;
}
var totalDue = 0;
var body = 'Dear ' + customerName + ',\n\n';
body += 'This is a reminder that the following invoice(s) are overdue and still unpaid:\n\n';
invoices.forEach(function(inv) {
totalDue += parseFloat(inv.amountRemaining) || 0;
body += '- Invoice #' + inv.invoiceNumber +
' | Due: ' + inv.dueDate +
' | Outstanding: $' + parseFloat(inv.amountRemaining).toFixed(2) + '\n';
});
body += '\nTotal Outstanding: $' + totalDue.toFixed(2) + '\n\n';
body += 'Please settle payment at your earliest convenience.\n\n';
body += 'Thank you,\nQuickMart Accounts Receivable';
email.send({
author: runtime.getCurrentUser().id,
recipients: customerEmail,
subject: 'Payment Reminder: Overdue Invoice(s)',
body: body
});
log.audit('Reminder Sent', 'Customer ID ' + context.key + ' | Invoices: ' + invoices.length);
}
function summarize(summary) {
var customersNotified = 0;
summary.reduceSummary.keys.iterator().each(function() {
customersNotified++;
return true;
});
log.audit('Overdue Reminder Complete', 'Customers notified: ' + customersNotified);
}
return {
getInputData: getInputData,
map: map,
reduce: reduce,
summarize: summarize
};
});
Test Steps
- Create at least 2 open invoices for the same customer with due dates in the past and partial/unpaid balances.
- Ensure customer record has a valid email address.
- Execute the Map/Reduce script deployment.
- Verify: Customer receives one reminder email listing all overdue, unpaid invoices.
- Verify: Email includes correct due dates, remaining balances, and total outstanding amount.
- Verify: No reminder is sent for fully paid or not-yet-due invoices.
Test Case #36: Map/Reduce - Monthly Customer Sales Consolidation
Objective: Consolidate monthly paid invoices per customer, calculate loyalty tiers, and create audit trail records.
Scenario
Sarah (Sales Manager) needs a monthly summary of total sales per customer to track loyalty tiers. She currently exports CSVs and uses spreadsheets. This script automates consolidation into a custom record.
Why Each Stage Is Used
| Stage | Purpose |
|---|---|
getInputData | Finds all paid invoices from the current month |
map | Groups invoices by customer ID (the key) |
reduce | Aggregates totals per customer, determines loyalty tier, creates summary record |
summary | Logs final statistics, creates a master "run" record for audit trail |
Custom Records Required
Custom Record: Monthly Sales Summary (customrecord_qm_monthly_sales_summary)
Fields:
custrecord_mss_customer - List/Record (Customer)
custrecord_mss_period - Free-Form Text (e.g., "2/2026")
custrecord_mss_total_sales - Currency
custrecord_mss_invoice_count - Integer
custrecord_mss_tier - Free-Form Text (Gold / Silver / Bronze)
Custom Record: Consolidation Run (customrecord_qm_consolidation_run)
Fields:
custrecord_cr_run_date - Date
custrecord_cr_customers_processed - Integer
custrecord_cr_total_revenue - Currency
custrecord_cr_errors - Integer
custrecord_cr_gold_count - Integer
custrecord_cr_silver_count - Integer
custrecord_cr_bronze_count - Integer
Custom Entity Field on Customer:
custentity_qm_loyalty_tier - Free-Form Text
Script Configuration
/**
* @NApiVersion 2.1
* @NScriptType MapReduceScript
*
* QuickMart - Monthly Customer Sales Consolidation
* Creates/updates customer monthly summary records
*/
define(['N/search', 'N/record', 'N/log', 'N/runtime', 'N/format'],
function(search, record, log, runtime, format) {
function getInputData() {
// Find all paid invoices from the current month
var today = new Date();
var firstDay = new Date(today.getFullYear(), today.getMonth(), 1);
return search.create({
type: search.Type.INVOICE,
filters: [
['mainline', 'is', 'T'],
'AND',
['status', 'anyof', 'CustInvc:B'], // Paid In Full
'AND',
['trandate', 'onorafter', format.format({
value: firstDay, type: format.Type.DATE
})]
],
columns: [
'tranid',
'entity',
'amount',
'trandate',
'quantity'
]
});
}
function map(context) {
var result = JSON.parse(context.value);
var customerId = result.values.entity.value;
// Emit each invoice keyed by customer — reduce will aggregate
context.write({
key: customerId,
value: {
invoiceId: result.id,
invoiceNumber: result.values.tranid,
amount: parseFloat(result.values.amount) || 0,
date: result.values.trandate,
customerName: result.values.entity.text
}
});
}
function reduce(context) {
var customerId = context.key;
var invoices = context.values.map(function(v) { return JSON.parse(v); });
var totalSales = 0;
var invoiceCount = invoices.length;
var customerName = invoices[0].customerName;
invoices.forEach(function(inv) {
totalSales += inv.amount;
});
// Determine loyalty tier based on monthly spend
var tier = 'Bronze';
if (totalSales >= 10000) tier = 'Gold';
else if (totalSales >= 5000) tier = 'Silver';
try {
// Create monthly summary custom record
var summaryRec = record.create({
type: 'customrecord_qm_monthly_sales_summary'
});
var today = new Date();
var period = (today.getMonth() + 1) + '/' + today.getFullYear();
summaryRec.setValue('custrecord_mss_customer', customerId);
summaryRec.setValue('custrecord_mss_period', period);
summaryRec.setValue('custrecord_mss_total_sales', totalSales);
summaryRec.setValue('custrecord_mss_invoice_count', invoiceCount);
summaryRec.setValue('custrecord_mss_tier', tier);
var summaryId = summaryRec.save();
// Also update customer record with current tier
record.submitFields({
type: record.Type.CUSTOMER,
id: customerId,
values: {
'custentity_qm_loyalty_tier': tier
}
});
context.write({
key: customerId,
value: JSON.stringify({
success: true,
summaryId: summaryId,
customerName: customerName,
totalSales: totalSales,
invoiceCount: invoiceCount,
tier: tier
})
});
} catch (e) {
log.error('Reduce Error', 'Customer ' + customerId + ': ' + e.message);
context.write({
key: customerId,
value: JSON.stringify({
success: false,
customerName: customerName,
error: e.message
})
});
}
}
function summarize(summary) {
// --- Handle input stage errors ---
if (summary.inputSummary.error) {
log.error('Input Error', summary.inputSummary.error);
}
// --- Handle map stage errors ---
summary.mapSummary.errors.iterator().each(function(key, error) {
log.error('Map Error - Key: ' + key, error);
return true;
});
// --- Handle reduce stage errors ---
summary.reduceSummary.errors.iterator().each(function(key, error) {
log.error('Reduce Error - Key: ' + key, error);
return true;
});
// --- Collect final results from reduce output ---
var totalCustomers = 0;
var totalRevenue = 0;
var errorCount = 0;
var tierCounts = { Gold: 0, Silver: 0, Bronze: 0 };
summary.output.iterator().each(function(key, value) {
var result = JSON.parse(value);
totalCustomers++;
if (result.success) {
totalRevenue += result.totalSales;
tierCounts[result.tier] = (tierCounts[result.tier] || 0) + 1;
} else {
errorCount++;
}
return true;
});
// --- Create a master run record for audit trail ---
try {
var runRec = record.create({
type: 'customrecord_qm_consolidation_run'
});
var today = new Date();
runRec.setValue('custrecord_cr_run_date', today);
runRec.setValue('custrecord_cr_customers_processed', totalCustomers);
runRec.setValue('custrecord_cr_total_revenue', totalRevenue);
runRec.setValue('custrecord_cr_errors', errorCount);
runRec.setValue('custrecord_cr_gold_count', tierCounts.Gold);
runRec.setValue('custrecord_cr_silver_count', tierCounts.Silver);
runRec.setValue('custrecord_cr_bronze_count', tierCounts.Bronze);
runRec.save();
} catch (e) {
log.error('Summary Record Error', e.message);
}
log.audit('Consolidation Complete', JSON.stringify({
customersProcessed: totalCustomers,
totalRevenue: totalRevenue.toFixed(2),
errors: errorCount,
tiers: tierCounts
}));
}
return {
getInputData: getInputData,
map: map,
reduce: reduce,
summarize: summarize
};
});
Deployment
Script: Monthly Customer Sales Consolidation
Deployment ID: customdeploy_qm_sales_consolidation
Schedule: 1st of every month at 2:00 AM
Status: Active
Test Steps
| # | Step | Expected Result |
|---|---|---|
| 1 | Create 5+ paid invoices across 3 different customers for the current month | Invoices visible in saved search |
| 2 | Give Customer A invoices totaling $12,000, Customer B $6,500, Customer C $2,000 | Different tier thresholds hit |
| 3 | Execute the Map/Reduce deployment | Script completes all stages |
| 4 | Verify map stage: Check execution log shows invoices grouped by customer | Each invoice emitted with correct customer key |
| 5 | Verify reduce stage: Check customrecord_qm_monthly_sales_summary records | 3 records created — one per customer |
| 6 | Verify reduce: Customer A summary shows tier = "Gold", total = $12,000 | Tier logic correct for >= $10,000 |
| 7 | Verify reduce: Customer B summary shows tier = "Silver", total = $6,500 | Tier logic correct for >= $5,000 |
| 8 | Verify reduce: Customer C summary shows tier = "Bronze", total = $2,000 | Tier logic correct for < $5,000 |
| 9 | Verify reduce: Customer records updated with custentity_qm_loyalty_tier | Field reflects new tier value |
| 10 | Verify summary: Check customrecord_qm_consolidation_run | 1 record: 3 customers, $20,500 revenue, 0 errors, tier breakdown |
| 11 | Verify summary: Check execution log for "Consolidation Complete" | JSON contains correct totals |
| 12 | Edge case: Run script when no paid invoices exist this month | Script completes with 0 customers processed, run record shows zeros |
| 13 | Error case: Set one customer as inactive before running | Reduce handles error gracefully, error count = 1 in summary |