SuiteScript Test Cases
Automation scripts for QuickMart Supplies - All script types plus PDF generation.
Script Types Covered
| Script Type | Test Cases | Purpose |
|---|---|---|
| Client Script | #10-11 | Real-time form validation |
| User Event | #12-14, #27, #29, #31 | Before/after save automation |
| Scheduled Script | #15-16, #28 | Timed batch jobs |
| Map/Reduce | #17-18 | High-volume processing |
| RESTlet | #19-22 | API endpoints |
| Suitelet | #23-24 | Custom UI pages |
| Advanced PDF | #25-26, #30 | Transaction templates |
Test Case #10: Client Script - Loyalty Discount Calculator
Objective: Calculate and apply customer loyalty discount in real-time on Sales Order.
Scenario
Mike (Sales Rep) needs to see the customer's loyalty tier and applicable discount immediately when selecting a customer on a Sales Order.
Script Configuration
/**
* @NApiVersion 2.1
* @NScriptType ClientScript
* @NModuleScope SameAccount
*
* QuickMart - Loyalty Discount Calculator
* Applies discount based on customer loyalty tier
*/
define(['N/search', 'N/currentRecord'], function(search, currentRecord) {
// Discount percentages by tier
const TIER_DISCOUNTS = {
1: 0, // Bronze - 0%
2: 5, // Silver - 5%
3: 10, // Gold - 10%
4: 15 // Platinum - 15%
};
function pageInit(context) {
if (context.mode === 'create') {
console.log('QuickMart Loyalty Script loaded');
}
}
function fieldChanged(context) {
var rec = context.currentRecord;
// When customer changes, lookup loyalty and apply discount
if (context.fieldId === 'entity') {
var customerId = rec.getValue('entity');
if (!customerId) return;
var loyaltyData = getLoyaltyData(customerId);
if (loyaltyData) {
// Show loyalty info to user
alert('Customer Tier: ' + loyaltyData.tierName +
'\nPoints: ' + loyaltyData.points +
'\nDiscount: ' + loyaltyData.discount + '%');
// Set discount on order (if discount field exists)
try {
rec.setValue({
fieldId: 'discountrate',
value: loyaltyData.discount
});
} catch (e) {
console.log('Discount field not available: ' + e.message);
}
}
}
}
function getLoyaltyData(customerId) {
var result = null;
search.create({
type: 'customrecord_cust_loyalty',
filters: [
['custrecord_cl_customer', 'is', customerId]
],
columns: [
'custrecord_cl_tier',
'custrecord_cl_points'
]
}).run().each(function(row) {
var tier = row.getValue('custrecord_cl_tier');
result = {
tier: tier,
tierName: row.getText('custrecord_cl_tier'),
points: row.getValue('custrecord_cl_points'),
discount: TIER_DISCOUNTS[tier] || 0
};
return false; // First result only
});
return result;
}
function saveRecord(context) {
// Optional: Confirm before saving with discount
return true;
}
return {
pageInit: pageInit,
fieldChanged: fieldChanged,
saveRecord: saveRecord
};
});
Deployment
Script Record:
Name: QuickMart Loyalty Discount CS
ID: customscript_qm_loyalty_cs
Script File: SuiteScripts/QuickMart/qm_loyalty_discount_cs.js
Deployment:
Name: QuickMart Loyalty - Sales Order
ID: customdeploy_qm_loyalty_so
Applies To: Sales Order
Status: Released
Expected Result
- User creates new Sales Order
- Selects customer "ABC Corp"
- Script looks up loyalty record
- Alert shows: "Customer Tier: Gold, Points: 1500, Discount: 10%"
- Discount rate field auto-populated
Test Case #11: Client Script - Credit Limit Warning
Objective: Warn user when order exceeds customer credit limit.
Script Configuration
/**
* @NApiVersion 2.1
* @NScriptType ClientScript
*
* QuickMart - Credit Limit Warning
* Warns when order total exceeds customer available credit
*/
define(['N/search', 'N/record'], function(search, record) {
function saveRecord(context) {
var rec = context.currentRecord;
var customerId = rec.getValue('entity');
var orderTotal = rec.getValue('total');
if (!customerId || !orderTotal) return true;
// Get customer credit info
var custRecord = record.load({
type: record.Type.CUSTOMER,
id: customerId,
isDynamic: false
});
var creditLimit = custRecord.getValue('creditlimit') || 0;
var balance = custRecord.getValue('balance') || 0;
var availableCredit = creditLimit - balance;
// Check if order exceeds available credit
if (orderTotal > availableCredit && creditLimit > 0) {
var proceed = confirm(
'WARNING: Credit Limit Exceeded!\n\n' +
'Credit Limit: $' + creditLimit.toFixed(2) + '\n' +
'Current Balance: $' + balance.toFixed(2) + '\n' +
'Available Credit: $' + availableCredit.toFixed(2) + '\n' +
'Order Total: $' + orderTotal.toFixed(2) + '\n\n' +
'Do you want to proceed anyway?'
);
if (!proceed) {
return false; // Cancel save
}
}
return true;
}
return {
saveRecord: saveRecord
};
});
Expected Result
- Order total > customer available credit
- Warning dialog appears with credit details
- User can confirm to proceed or cancel
Test Case #12: User Event - Update Loyalty Points
Objective: Add loyalty points after invoice is created.
Scenario
After an invoice is saved, the customer should receive 1 point per $10 spent.
Script Configuration
/**
* @NApiVersion 2.1
* @NScriptType UserEventScript
*
* QuickMart - Update Loyalty Points
* Awards points after invoice creation
*/
define(['N/record', 'N/search', 'N/log'], function(record, search, log) {
const POINTS_PER_DOLLAR = 0.1; // 1 point per $10
function afterSubmit(context) {
if (context.type !== context.UserEventType.CREATE) return;
var invoice = context.newRecord;
var customerId = invoice.getValue('entity');
var invoiceTotal = invoice.getValue('total');
if (!customerId || !invoiceTotal) return;
// Calculate points earned
var pointsEarned = Math.floor(invoiceTotal * POINTS_PER_DOLLAR);
if (pointsEarned <= 0) return;
// Find customer's loyalty record
var loyaltyId = findLoyaltyRecord(customerId);
if (loyaltyId) {
// Update existing loyalty record
updateLoyaltyPoints(loyaltyId, pointsEarned, invoiceTotal);
} else {
// Create new loyalty record
createLoyaltyRecord(customerId, pointsEarned, invoiceTotal);
}
log.audit('Loyalty Points', 'Customer ' + customerId +
' earned ' + pointsEarned + ' points');
}
function findLoyaltyRecord(customerId) {
var results = search.create({
type: 'customrecord_cust_loyalty',
filters: [['custrecord_cl_customer', 'is', customerId]],
columns: ['internalid']
}).run().getRange({ start: 0, end: 1 });
return results.length > 0 ? results[0].id : null;
}
function updateLoyaltyPoints(loyaltyId, pointsEarned, invoiceTotal) {
var loyalty = record.load({
type: 'customrecord_cust_loyalty',
id: loyaltyId
});
var currentPoints = loyalty.getValue('custrecord_cl_points') || 0;
var totalSpent = loyalty.getValue('custrecord_cl_totalspent') || 0;
loyalty.setValue('custrecord_cl_points', currentPoints + pointsEarned);
loyalty.setValue('custrecord_cl_totalspent', totalSpent + invoiceTotal);
// Check for tier upgrade
var newTier = calculateTier(currentPoints + pointsEarned);
loyalty.setValue('custrecord_cl_tier', newTier);
loyalty.save();
}
function calculateTier(points) {
if (points >= 5000) return 4; // Platinum
if (points >= 2000) return 3; // Gold
if (points >= 500) return 2; // Silver
return 1; // Bronze
}
function createLoyaltyRecord(customerId, points, spent) {
var loyalty = record.create({
type: 'customrecord_cust_loyalty'
});
loyalty.setValue('custrecord_cl_customer', customerId);
loyalty.setValue('custrecord_cl_points', points);
loyalty.setValue('custrecord_cl_totalspent', spent);
loyalty.setValue('custrecord_cl_tier', calculateTier(points));
loyalty.setValue('custrecord_cl_joindate', new Date());
loyalty.save();
}
return {
afterSubmit: afterSubmit
};
});
Expected Result
- Create invoice for $500
- After save, customer receives 50 points
- Loyalty record updated or created
Test Case #13: User Event - Push Order to Warehouse (Integration)
Objective: Send order details to external warehouse app when order is approved.
Script Configuration
/**
* @NApiVersion 2.1
* @NScriptType UserEventScript
*
* QuickMart - Push Order to Warehouse
* Integration Pattern: PUSH OUT
*/
define(['N/https', 'N/record', 'N/log', 'N/runtime'], function(https, record, log, runtime) {
const WAREHOUSE_API_URL = 'https://warehouse.quickmart.com/api/orders';
function afterSubmit(context) {
// Only on create or status change to Pending Fulfillment
if (context.type !== context.UserEventType.EDIT &&
context.type !== context.UserEventType.CREATE) return;
var order = context.newRecord;
var status = order.getValue('orderstatus');
// B = Pending Fulfillment
if (status !== 'B') return;
var payload = buildPayload(order);
try {
var response = https.post({
url: WAREHOUSE_API_URL,
headers: {
'Content-Type': 'application/json',
'X-API-Key': runtime.getCurrentScript().getParameter('custscript_warehouse_api_key')
},
body: JSON.stringify(payload)
});
log.audit('Warehouse Push', 'Order ' + order.id + ' sent. Response: ' + response.code);
// Log to Integration Log custom record
logIntegration(order.id, 'PUSH_OUT', 'SUCCESS', response.body);
} catch (e) {
log.error('Warehouse Push Error', e.message);
logIntegration(order.id, 'PUSH_OUT', 'ERROR', e.message);
}
}
function buildPayload(order) {
var lines = [];
var lineCount = order.getLineCount('item');
for (var i = 0; i < lineCount; i++) {
lines.push({
item: order.getSublistText('item', 'item', i),
quantity: order.getSublistValue('item', 'quantity', i),
location: order.getSublistText('item', 'location', i)
});
}
return {
orderId: order.getValue('tranid'),
nsInternalId: order.id,
customer: order.getText('entity'),
shipAddress: order.getValue('shipaddress'),
priority: order.getValue('custbody_priority_order'),
lines: lines
};
}
function logIntegration(recordId, pattern, status, message) {
var integLog = record.create({ type: 'customrecord_integration_log' });
integLog.setValue('name', 'Order Push: ' + recordId);
integLog.setValue('custrecord_il_pattern', pattern);
integLog.setValue('custrecord_il_status', status);
integLog.setValue('custrecord_il_message', message.substring(0, 4000));
integLog.save();
}
return {
afterSubmit: afterSubmit
};
});
Test Case #14: User Event - Calculate PR Line Total
Objective: Calculate line totals and header total on Purchase Request.
Script Configuration
/**
* @NApiVersion 2.1
* @NScriptType UserEventScript
*
* QuickMart - Purchase Request Line Calculator
* Calculates line total and updates header total
*/
define(['N/record', 'N/search', 'N/log'], function(record, search, log) {
// On Purchase Request LINE record
function afterSubmit(context) {
var line = context.newRecord;
var parentId = line.getValue('custrecord_prl_parent');
if (!parentId) return;
// Recalculate header total
updateHeaderTotal(parentId);
}
function updateHeaderTotal(headerId) {
// Sum all lines for this header
var total = 0;
search.create({
type: 'customrecord_purch_req_line',
filters: [
['custrecord_prl_parent', 'is', headerId]
],
columns: ['custrecord_prl_total']
}).run().each(function(result) {
total += parseFloat(result.getValue('custrecord_prl_total')) || 0;
return true;
});
// Update header
record.submitFields({
type: 'customrecord_purch_req',
id: headerId,
values: {
'custrecord_pr_total': total
}
});
log.audit('PR Total Updated', 'PR ' + headerId + ' = $' + total);
}
return {
afterSubmit: afterSubmit
};
});
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'], function(https, record, log) {
const EXCHANGE_API = 'https://api.exchangerate-api.com/v4/latest/USD';
function execute(context) {
try {
var response = https.get({ url: EXCHANGE_API });
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'], function(https, record, log) {
const SUPPLIER_API = 'https://supplier.example.com/api/catalog';
function getInputData() {
// Fetch catalog from supplier
var response = https.get({ url: SUPPLIER_API });
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 #19: RESTlet - Receive E-commerce Orders (Integration)
Objective: Receive orders from Shopify via webhook.
Script Configuration
/**
* @NApiVersion 2.1
* @NScriptType Restlet
*
* QuickMart - Receive E-commerce Orders
* Integration Pattern: RECEIVE
*/
define(['N/record', 'N/search', 'N/log'], function(record, search, log) {
function doPost(requestBody) {
log.audit('Order Received', JSON.stringify(requestBody));
try {
// Validate required fields
if (!requestBody.customer_email || !requestBody.line_items) {
return { success: false, error: 'Missing required fields' };
}
// Find or create customer
var customerId = findOrCreateCustomer(requestBody);
// Create Sales Order
var salesOrder = record.create({
type: record.Type.SALES_ORDER,
isDynamic: true
});
salesOrder.setValue('entity', customerId);
salesOrder.setValue('orderstatus', 'A'); // Pending Approval
salesOrder.setValue('memo', 'Shopify Order: ' + requestBody.order_id);
salesOrder.setValue('custbody_priority_order', requestBody.priority || false);
// Set custom form for online orders
salesOrder.setValue('customform', getOnlineOrderForm());
// Add line items
requestBody.line_items.forEach(function(item) {
var nsItemId = findItemBySku(item.sku);
if (nsItemId) {
salesOrder.selectNewLine('item');
salesOrder.setCurrentSublistValue('item', 'item', nsItemId);
salesOrder.setCurrentSublistValue('item', 'quantity', item.quantity);
salesOrder.setCurrentSublistValue('item', 'rate', item.price);
salesOrder.commitLine('item');
}
});
var orderId = salesOrder.save();
log.audit('Order Created', 'SO ID: ' + orderId);
return {
success: true,
netsuite_order_id: orderId,
message: 'Order created successfully'
};
} catch (e) {
log.error('Order Creation Failed', e.message);
return {
success: false,
error: e.message
};
}
}
function findOrCreateCustomer(orderData) {
// Search by email
var results = search.create({
type: search.Type.CUSTOMER,
filters: [['email', 'is', orderData.customer_email]],
columns: ['internalid']
}).run().getRange({ start: 0, end: 1 });
if (results.length > 0) {
return results[0].id;
}
// Create new customer
var customer = record.create({ type: record.Type.CUSTOMER });
customer.setValue('companyname', orderData.customer_name);
customer.setValue('email', orderData.customer_email);
return customer.save();
}
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 getOnlineOrderForm() {
// Return internal ID of "Sales Order - Online" form
return 123; // Replace with actual form ID
}
return {
post: doPost
};
});
Sample Request
POST /app/site/hosting/restlet.nl?script=XXX&deploy=1
{
"order_id": "SHOP-12345",
"customer_email": "john@example.com",
"customer_name": "John Doe",
"priority": true,
"line_items": [
{ "sku": "PAPER-A4", "quantity": 10, "price": 12.00 },
{ "sku": "INK-HP", "quantity": 2, "price": 35.00 }
]
}
Test Case #20: RESTlet - Receive POS Payments
Objective: Receive payment data from POS system.
/**
* @NApiVersion 2.1
* @NScriptType Restlet
*
* QuickMart - Receive POS Payments
* Integration Pattern: RECEIVE
*/
define(['N/record', 'N/search', 'N/log'], function(record, search, log) {
function doPost(requestBody) {
try {
var invoiceId = findInvoice(requestBody.invoice_number);
if (!invoiceId) {
return { success: false, error: 'Invoice not found' };
}
var payment = record.transform({
fromType: record.Type.INVOICE,
fromId: invoiceId,
toType: record.Type.CUSTOMER_PAYMENT
});
payment.setValue('payment', requestBody.amount);
payment.setValue('paymentmethod', requestBody.payment_method);
payment.setValue('memo', 'POS Payment: ' + requestBody.reference);
var paymentId = payment.save();
return { success: true, payment_id: paymentId };
} catch (e) {
return { success: false, error: e.message };
}
}
function findInvoice(invoiceNumber) {
var results = search.create({
type: search.Type.INVOICE,
filters: [['tranid', 'is', invoiceNumber]],
columns: ['internalid']
}).run().getRange({ start: 0, end: 1 });
return results.length > 0 ? results[0].id : null;
}
return { post: doPost };
});
Test Case #21: RESTlet - Expose Stock Availability (Integration)
Objective: Allow external systems to query stock levels.
/**
* @NApiVersion 2.1
* @NScriptType Restlet
*
* QuickMart - Stock Availability Query
* Integration Pattern: EXPOSE
*/
define(['N/search', 'N/log'], function(search, log) {
function doGet(requestParams) {
var itemId = requestParams.itemId;
var sku = requestParams.sku;
var location = requestParams.location;
if (!itemId && !sku) {
return { error: 'itemId or sku required' };
}
var filters = [];
if (itemId) {
filters.push(['internalid', 'is', itemId]);
} else {
filters.push(['itemid', 'is', sku]);
}
var results = search.create({
type: search.Type.INVENTORY_ITEM,
filters: filters,
columns: [
'itemid',
'displayname',
'quantityavailable',
'quantityonhand',
'quantityonorder'
]
}).run().getRange({ start: 0, end: 1 });
if (results.length === 0) {
return { error: 'Item not found' };
}
var item = results[0];
return {
item_id: item.id,
sku: item.getValue('itemid'),
name: item.getValue('displayname'),
available: parseFloat(item.getValue('quantityavailable')) || 0,
on_hand: parseFloat(item.getValue('quantityonhand')) || 0,
on_order: parseFloat(item.getValue('quantityonorder')) || 0
};
}
return { get: doGet };
});
Sample Request/Response
GET /app/site/hosting/restlet.nl?script=XXX&deploy=1&sku=PAPER-A4
Response:
{
"item_id": "12345",
"sku": "PAPER-A4",
"name": "A4 Printer Paper",
"available": 150,
"on_hand": 200,
"on_order": 50
}
Test Case #22: RESTlet - Customer Loyalty Lookup
Objective: Allow POS to lookup customer loyalty data.
/**
* @NApiVersion 2.1
* @NScriptType Restlet
*
* QuickMart - Customer Loyalty Lookup
* Integration Pattern: EXPOSE
*/
define(['N/search', 'N/log'], function(search, log) {
const TIER_NAMES = { 1: 'Bronze', 2: 'Silver', 3: 'Gold', 4: 'Platinum' };
const TIER_DISCOUNTS = { 1: 0, 2: 5, 3: 10, 4: 15 };
function doGet(requestParams) {
var email = requestParams.email;
var phone = requestParams.phone;
if (!email && !phone) {
return { error: 'email or phone required' };
}
// Find customer
var custFilters = [];
if (email) custFilters.push(['email', 'is', email]);
if (phone) custFilters.push(['phone', 'is', phone]);
var custResults = search.create({
type: search.Type.CUSTOMER,
filters: custFilters,
columns: ['entityid', 'email', 'phone']
}).run().getRange({ start: 0, end: 1 });
if (custResults.length === 0) {
return { found: false, message: 'Customer not found' };
}
var customerId = custResults[0].id;
// Find loyalty record
var loyaltyResults = search.create({
type: 'customrecord_cust_loyalty',
filters: [['custrecord_cl_customer', 'is', customerId]],
columns: [
'custrecord_cl_tier',
'custrecord_cl_points',
'custrecord_cl_totalspent'
]
}).run().getRange({ start: 0, end: 1 });
if (loyaltyResults.length === 0) {
return {
found: true,
customer_id: customerId,
customer_name: custResults[0].getValue('entityid'),
loyalty: null,
message: 'No loyalty record'
};
}
var tier = parseInt(loyaltyResults[0].getValue('custrecord_cl_tier')) || 1;
return {
found: true,
customer_id: customerId,
customer_name: custResults[0].getValue('entityid'),
loyalty: {
tier: tier,
tier_name: TIER_NAMES[tier],
points: parseInt(loyaltyResults[0].getValue('custrecord_cl_points')) || 0,
total_spent: parseFloat(loyaltyResults[0].getValue('custrecord_cl_totalspent')) || 0,
discount_percent: TIER_DISCOUNTS[tier]
}
};
}
return { get: doGet };
});
Test Case #23: Suitelet - Delivery Manifest Generator
Objective: Custom UI to generate delivery manifest PDF.
/**
* @NApiVersion 2.1
* @NScriptType Suitelet
*
* QuickMart - Delivery Manifest Generator
*/
define(['N/ui/serverWidget', 'N/search', 'N/render', 'N/record', 'N/file'],
function(serverWidget, search, render, record, file) {
function onRequest(context) {
if (context.request.method === 'GET') {
showForm(context);
} else {
generateManifest(context);
}
}
function showForm(context) {
var form = serverWidget.createForm({ title: 'Delivery Manifest Generator' });
form.addField({
id: 'custpage_date',
type: serverWidget.FieldType.DATE,
label: 'Delivery Date'
}).defaultValue = new Date();
form.addField({
id: 'custpage_location',
type: serverWidget.FieldType.SELECT,
label: 'Warehouse',
source: 'location'
});
form.addSubmitButton({ label: 'Generate Manifest' });
context.response.writePage(form);
}
function generateManifest(context) {
var deliveryDate = context.request.parameters.custpage_date;
var locationId = context.request.parameters.custpage_location;
// Find orders to deliver
var orders = findOrdersForDelivery(deliveryDate, locationId);
// Generate PDF using render module
var xml = buildManifestXml(orders, deliveryDate);
var pdf = render.xmlToPdf({ xmlString: xml });
context.response.writeFile({
file: pdf,
isInline: true
});
}
function findOrdersForDelivery(date, locationId) {
var orders = [];
search.create({
type: search.Type.SALES_ORDER,
filters: [
['shipdate', 'on', date],
'AND',
['status', 'anyof', 'SalesOrd:B'], // Pending Fulfillment
'AND',
['mainline', 'is', 'T']
],
columns: ['tranid', 'entity', 'shipaddress', 'custbody_priority_order']
}).run().each(function(result) {
orders.push({
orderNumber: result.getValue('tranid'),
customer: result.getText('entity'),
address: result.getValue('shipaddress'),
priority: result.getValue('custbody_priority_order')
});
return true;
});
return orders;
}
function buildManifestXml(orders, date) {
var xml = '<?xml version="1.0"?>\n';
xml += '<!DOCTYPE pdf PUBLIC "-//big.faceless.org//report" "report-1.1.dtd">\n';
xml += '<pdf>\n<body>\n';
xml += '<h1>Delivery Manifest - ' + date + '</h1>\n';
xml += '<table>\n';
xml += '<tr><th>Order</th><th>Customer</th><th>Address</th><th>Priority</th></tr>\n';
orders.forEach(function(order) {
xml += '<tr>';
xml += '<td>' + order.orderNumber + '</td>';
xml += '<td>' + order.customer + '</td>';
xml += '<td>' + (order.address || '-') + '</td>';
xml += '<td>' + (order.priority ? 'RUSH' : 'Normal') + '</td>';
xml += '</tr>\n';
});
xml += '</table>\n';
xml += '</body>\n</pdf>';
return xml;
}
return { onRequest: onRequest };
});
Test Case #24: Suitelet - Purchase Request Form
Objective: Custom entry form for Purchase Requests.
See SuiteBuilder Test #3-4 for the record structure.
Test Case #25: Advanced PDF - Custom Invoice Template
Objective: Create branded invoice with loyalty info.
Template Configuration
Navigation: Customization > Forms > Advanced PDF/HTML Templates > New
<?xml version="1.0"?>
<!DOCTYPE pdf PUBLIC "-//big.faceless.org//report" "report-1.1.dtd">
<pdf>
<head>
<style type="text/css">
body { font-family: sans-serif; font-size: 10pt; }
.header { background-color: #1a365d; color: white; padding: 10px; }
.customer-info { margin: 20px 0; }
.loyalty-badge {
background: #f6e05e;
padding: 5px 10px;
border-radius: 4px;
display: inline-block;
}
table.items { width: 100%; border-collapse: collapse; margin: 20px 0; }
table.items th { background: #e2e8f0; padding: 8px; text-align: left; }
table.items td { padding: 8px; border-bottom: 1px solid #e2e8f0; }
.total { text-align: right; font-size: 14pt; font-weight: bold; }
</style>
</head>
<body>
<div class="header">
<h1>QuickMart Supplies</h1>
<p>Your Office Supplies Partner</p>
</div>
<h2>INVOICE ${record.tranid}</h2>
<div class="customer-info">
<strong>Bill To:</strong><br/>
${record.entity}<br/>
${record.billaddress}
</div>
<#-- Customer Loyalty Badge -->
<#if record.entity.custentity_loyalty_record?has_content>
<div class="loyalty-badge">
${record.entity.custentity_loyalty_record.custrecord_cl_tier} Member
| ${record.entity.custentity_loyalty_record.custrecord_cl_points} Points
</div>
</#if>
<table class="items">
<tr>
<th>Item</th>
<th>Description</th>
<th>Qty</th>
<th>Rate</th>
<th>Amount</th>
</tr>
<#list record.item as item>
<tr>
<td>${item.item}</td>
<td>${item.description!''}</td>
<td>${item.quantity}</td>
<td>${item.rate}</td>
<td>${item.amount}</td>
</tr>
</#list>
</table>
<div class="total">
Subtotal: ${record.subtotal}<br/>
Tax: ${record.taxtotal}<br/>
<strong>Total: ${record.total}</strong>
</div>
<#if record.discountrate?has_content && record.discountrate != 0>
<p><em>Loyalty Discount Applied: ${record.discountrate}%</em></p>
</#if>
<hr/>
<p style="text-align: center; font-size: 8pt;">
Thank you for your business! | www.quickmart.com
</p>
</body>
</pdf>
Link to Form
Customization > Forms > Transaction Forms > Invoice - Online > Edit
> Printing Tab
> Advanced PDF Template: [Custom Invoice - QuickMart]
Test Case #26: Suitelet PDF - Daily Sales Report
Objective: Generate sales summary PDF from Suitelet.
See Test Case #23 for similar pattern using N/render.xmlToPdf().
Test Case #27: User Event - Auto-Create PO from Approved PR
Objective: When Purchase Request status changes to "Approved", automatically create a Purchase Order.
Scenario
After Sarah approves a Purchase Request, the PO should be created automatically without Lisa (Admin) having to manually copy data.
Script Configuration
/**
* @NApiVersion 2.1
* @NScriptType UserEventScript
*
* QuickMart - Auto-Create PO from Approved PR
* Triggers when PR status changes to "Approved"
*/
define(['N/record', 'N/search', 'N/log'], function(record, search, log) {
function afterSubmit(context) {
if (context.type !== context.UserEventType.EDIT) return;
var pr = context.newRecord;
var oldStatus = context.oldRecord.getValue('custrecord_pr_status');
var newStatus = pr.getValue('custrecord_pr_status');
// Only trigger when status changes TO "Approved" (value = 3)
if (oldStatus === newStatus || newStatus !== '3') return;
try {
var poId = createPurchaseOrder(pr);
// Link PO back to PR
record.submitFields({
type: 'customrecord_purch_req',
id: pr.id,
values: {
'custrecord_pr_linked_po': poId
}
});
log.audit('PO Created', 'PR ' + pr.id + ' -> PO ' + poId);
} catch (e) {
log.error('PO Creation Failed', e.message);
}
}
function createPurchaseOrder(pr) {
var po = record.create({
type: record.Type.PURCHASE_ORDER,
isDynamic: true
});
// Set vendor (from first line item's preferred vendor, or default)
var vendor = getVendorFromPRLines(pr.id);
po.setValue('entity', vendor);
// Reference original PR
po.setValue('memo', 'Auto-created from PR: ' + pr.getValue('name'));
po.setValue('custbody_source_pr', pr.id);
// Add lines from PR
var lines = getPRLines(pr.id);
lines.forEach(function(line) {
po.selectNewLine('item');
po.setCurrentSublistValue('item', 'item', line.item);
po.setCurrentSublistValue('item', 'quantity', line.qty);
po.setCurrentSublistValue('item', 'rate', line.rate);
po.commitLine('item');
});
return po.save();
}
function getPRLines(prId) {
var lines = [];
search.create({
type: 'customrecord_purch_req_line',
filters: [['custrecord_prl_parent', 'is', prId]],
columns: ['custrecord_prl_item', 'custrecord_prl_qty', 'custrecord_prl_rate']
}).run().each(function(result) {
lines.push({
item: result.getValue('custrecord_prl_item'),
qty: result.getValue('custrecord_prl_qty'),
rate: result.getValue('custrecord_prl_rate')
});
return true;
});
return lines;
}
function getVendorFromPRLines(prId) {
// Get preferred vendor from first item
var lines = getPRLines(prId);
if (lines.length === 0) return null;
var itemLookup = search.lookupFields({
type: search.Type.ITEM,
id: lines[0].item,
columns: ['preferredvendor']
});
return itemLookup.preferredvendor ? itemLookup.preferredvendor[0].value : null;
}
return {
afterSubmit: afterSubmit
};
});
Required Custom Field
Add to Purchase Request record:
| Field | Type | ID | Purpose |
|---|---|---|---|
| Linked PO | List/Record | custrecord_pr_linked_po | Points to created PO |
Test Steps
- Create Purchase Request with lines
- Submit for approval (status = "Pending Approval")
- Manager approves (status = "Approved")
- Verify: Purchase Order automatically created
- Verify: PR has link to new PO
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: Integration - Push Approved PR to Procurement System
Objective: Push approved Purchase Requests to external procurement/ERP system.
Scenario
QuickMart uses an external procurement system for supplier management. When PRs are approved, they need to sync to that system.
Architecture
PUSH OUT - PR TO PROCUREMENT
-------------------------------------------------------------------------------
NetSuite External Procurement
+------------------+ +------------------+
| | HTTP POST | |
| Purchase Request | -----------------> | /api/requests |
| Status: Approved | JSON Payload | |
| | | - Queue for RFQ |
| User Event: | Response: 200 OK | - Supplier Match |
| afterSubmit | <----------------- | |
| | | |
+------------------+ +------------------+
Script Configuration
/**
* @NApiVersion 2.1
* @NScriptType UserEventScript
*
* QuickMart - Push PR to Procurement System
* Integration Pattern: PUSH OUT
*/
define(['N/https', 'N/record', 'N/search', 'N/log'], function(https, record, search, log) {
const PROCUREMENT_API = 'https://procurement.quickmart.com/api/requests';
const API_KEY = 'proc-api-key-xxx'; // Store in script parameter
function afterSubmit(context) {
if (context.type !== context.UserEventType.EDIT) return;
var pr = context.newRecord;
var oldStatus = context.oldRecord.getValue('custrecord_pr_status');
var newStatus = pr.getValue('custrecord_pr_status');
// Only push when approved
if (oldStatus === newStatus || newStatus !== '3') return;
try {
var payload = buildPayload(pr);
var response = pushToProcurement(payload);
// Log integration
logIntegration(pr.id, payload, response);
// Store external ID
if (response.success) {
record.submitFields({
type: 'customrecord_purch_req',
id: pr.id,
values: {
'custrecord_pr_external_id': response.request_id
}
});
}
} catch (e) {
log.error('Procurement Push Failed', e.message);
}
}
function buildPayload(pr) {
var lines = getPRLines(pr.id);
return {
source: 'NETSUITE',
source_id: pr.id,
pr_number: pr.getValue('custrecord_pr_number'),
requestor: pr.getText('custrecord_pr_requestor'),
department: pr.getText('custrecord_pr_dept'),
justification: pr.getValue('custrecord_pr_justify'),
total_amount: pr.getValue('custrecord_pr_total'),
approved_by: pr.getText('custrecord_pr_approver'),
approved_date: pr.getValue('custrecord_pr_approval_date'),
items: lines.map(function(line) {
return {
item_id: line.item,
item_name: line.itemName,
quantity: line.qty,
unit_price: line.rate,
total: line.amount
};
})
};
}
function pushToProcurement(payload) {
var response = https.post({
url: PROCUREMENT_API,
headers: {
'Content-Type': 'application/json',
'X-API-Key': API_KEY
},
body: JSON.stringify(payload)
});
return JSON.parse(response.body);
}
function getPRLines(prId) {
var lines = [];
search.create({
type: 'customrecord_purch_req_line',
filters: [['custrecord_prl_parent', 'is', prId]],
columns: [
'custrecord_prl_item',
'custrecord_prl_qty',
'custrecord_prl_rate',
'custrecord_prl_amount'
]
}).run().each(function(result) {
var itemLookup = search.lookupFields({
type: search.Type.ITEM,
id: result.getValue('custrecord_prl_item'),
columns: ['displayname', 'itemid']
});
lines.push({
item: result.getValue('custrecord_prl_item'),
itemName: itemLookup.displayname || itemLookup.itemid,
qty: result.getValue('custrecord_prl_qty'),
rate: result.getValue('custrecord_prl_rate'),
amount: result.getValue('custrecord_prl_amount')
});
return true;
});
return lines;
}
function logIntegration(prId, payload, response) {
record.create({
type: 'customrecord_integration_log',
values: {
'custrecord_log_direction': 'OUTBOUND',
'custrecord_log_pattern': 'PUSH_OUT',
'custrecord_log_endpoint': PROCUREMENT_API,
'custrecord_log_payload': JSON.stringify(payload).substring(0, 3000),
'custrecord_log_response': JSON.stringify(response).substring(0, 3000),
'custrecord_log_status': response.success ? 'SUCCESS' : 'ERROR',
'custrecord_log_related': prId
}
}).save();
}
return {
afterSubmit: afterSubmit
};
});
Sample Payload
{
"source": "NETSUITE",
"source_id": 123,
"pr_number": "PR-00047",
"requestor": "David Lee",
"department": "Operations",
"justification": "Running low on packaging supplies",
"total_amount": 1250.00,
"approved_by": "Sarah Chen",
"approved_date": "2024-03-15",
"items": [
{
"item_id": 456,
"item_name": "Cardboard Boxes (Med)",
"quantity": 200,
"unit_price": 2.50,
"total": 500.00
},
{
"item_id": 457,
"item_name": "Packing Tape (6-pack)",
"quantity": 50,
"unit_price": 8.00,
"total": 400.00
}
]
}
Test Steps
- Create and approve a Purchase Request
- Verify: Integration log created
- Verify: External system receives the request
- Verify: PR has external ID stored
Test Case #30: Advanced PDF - Packing Slip with Barcodes
Objective: Generate warehouse-friendly packing slips with bin locations and scannable barcodes.
Scenario
David (Warehouse) needs a readable pick list with barcodes so he can scan items while picking.
Template Configuration
Navigation: Customization > Forms > Advanced PDF/HTML Templates > New
<?xml version="1.0"?>
<!DOCTYPE pdf PUBLIC "-//big.faceless.org//report" "report-1.1.dtd">
<pdf>
<head>
<style type="text/css">
body { font-family: sans-serif; font-size: 9pt; }
table { width: 100%; border-collapse: collapse; }
th, td { border-bottom: 1px solid #ddd; padding: 6px; }
th { background: #f0f0f0; text-align: left; }
</style>
</head>
<body>
<h2>Packing Slip - ${record.tranid}</h2>
<table>
<tr>
<th>Item</th>
<th>Qty</th>
<th>Bin</th>
<th>Barcode</th>
</tr>
<#list record.item as item>
<tr>
<td>${item.item}</td>
<td>${item.quantity}</td>
<td>${item.location!''}</td>
<td><barcode codetype="code128" showtext="true">${item.itemid}</barcode></td>
</tr>
</#list>
</table>
</body>
</pdf>
Expected Result
- Packing slip prints from Item Fulfillment or Sales Order
- Barcode renders for each line item
- Bin location is visible for picking
Test Case #31: User Event - Push Fulfillment Status to Shopify
Objective: When an Item Fulfillment is created, update the Shopify order status.
Scenario
Lisa (Admin) wants Shopify customers to see their order as "Fulfilled" immediately after NetSuite ships it.
Script Configuration
/**
* @NApiVersion 2.1
* @NScriptType UserEventScript
*
* QuickMart - Push Fulfillment Status to Shopify
*/
define(['N/https', 'N/search', 'N/log'], function(https, search, log) {
const SHOPIFY_API = 'https://your-store.myshopify.com/admin/api/2024-01/orders/';
const ACCESS_TOKEN = 'shopify-access-token'; // Store in script parameter
function afterSubmit(context) {
if (context.type !== context.UserEventType.CREATE) return;
var fulfillment = context.newRecord;
var salesOrderId = fulfillment.getValue('createdfrom');
if (!salesOrderId) return;
var lookup = search.lookupFields({
type: search.Type.SALES_ORDER,
id: salesOrderId,
columns: ['custbody_external_order_id']
});
var externalId = lookup.custbody_external_order_id;
if (!externalId) return;
var payload = {
order: { fulfillment_status: 'fulfilled' }
};
var response = https.put({
url: SHOPIFY_API + externalId + '.json',
headers: {
'Content-Type': 'application/json',
'X-Shopify-Access-Token': ACCESS_TOKEN
},
body: JSON.stringify(payload)
});
log.audit('Shopify Update', 'Order ' + externalId + ' updated. Code: ' + response.code);
}
return { afterSubmit: afterSubmit };
});
Expected Result
- Create Sales Order with
custbody_external_order_idset - Fulfill the order in NetSuite
- Verify: Shopify order status updates to Fulfilled