Skip to main content

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

  1. Create several PRs with status "Pending Approval"
  2. Run script manually (or wait for Monday)
  3. Verify: Email received with PR summary table
  4. 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

  1. Create at least 2 open invoices for the same customer with due dates in the past and partial/unpaid balances.
  2. Ensure customer record has a valid email address.
  3. Execute the Map/Reduce script deployment.
  4. Verify: Customer receives one reminder email listing all overdue, unpaid invoices.
  5. Verify: Email includes correct due dates, remaining balances, and total outstanding amount.
  6. 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

StagePurpose
getInputDataFinds all paid invoices from the current month
mapGroups invoices by customer ID (the key)
reduceAggregates totals per customer, determines loyalty tier, creates summary record
summaryLogs 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

#StepExpected Result
1Create 5+ paid invoices across 3 different customers for the current monthInvoices visible in saved search
2Give Customer A invoices totaling $12,000, Customer B $6,500, Customer C $2,000Different tier thresholds hit
3Execute the Map/Reduce deploymentScript completes all stages
4Verify map stage: Check execution log shows invoices grouped by customerEach invoice emitted with correct customer key
5Verify reduce stage: Check customrecord_qm_monthly_sales_summary records3 records created — one per customer
6Verify reduce: Customer A summary shows tier = "Gold", total = $12,000Tier logic correct for >= $10,000
7Verify reduce: Customer B summary shows tier = "Silver", total = $6,500Tier logic correct for >= $5,000
8Verify reduce: Customer C summary shows tier = "Bronze", total = $2,000Tier logic correct for < $5,000
9Verify reduce: Customer records updated with custentity_qm_loyalty_tierField reflects new tier value
10Verify summary: Check customrecord_qm_consolidation_run1 record: 3 customers, $20,500 revenue, 0 errors, tier breakdown
11Verify summary: Check execution log for "Consolidation Complete"JSON contains correct totals
12Edge case: Run script when no paid invoices exist this monthScript completes with 0 customers processed, run record shows zeros
13Error case: Set one customer as inactive before runningReduce handles error gracefully, error count = 1 in summary