📑 Exercise Index
- Exercise 1: Hello World User Event
- Exercise 2: Coupon Code Validation (Client Script)
- Exercise 3: Create Task on New Customer
- Exercise 4: Product Preferences Sublist
- Exercise 5: Product Shortage Search
- Exercise 6: Scheduled Script with Cases
- Exercise 7: Map/Reduce Payment Report
- Exercise 8: Sales Order Financing Suitelet
- Exercise 9: Coupon Validation RESTlet
- Exercise 10: Workflow Action - Update Customer
Exercise 1: Hello World User Event Script
📚 Module 2: Developing SuiteScripts🎯 Scenario
Create your first SuiteScript that logs "Hello World" when an employee record is loaded. This introduces the basic structure of SuiteScript 2.x and the User Event script type.
Required Setup
- Deploy to: Employee record
- Script ID:
_urk_ue_employee - Deployment ID:
_urk_ue_employee
Complete Solution: sdr_ue_employee.js
/**
* @NApiVersion 2.1
* @NScriptType UserEventScript
* @NModuleScope SameAccount
*
* EXERCISE 1: Hello World Script
* ══════════════════════════════
* 📚 MODULE 2 CONCEPTS:
* - Required JSDoc annotations (@NApiVersion, @NScriptType)
* - The define() statement for module loading
* - User Event entry points (beforeLoad)
* - Using the N/log module
*
* Deploy to: Employee record
*/
define(['N/log'], function(log) {
/**
* beforeLoad Entry Point
* ──────────────────────
* 📚 MODULE 4 CONCEPT: Entry Points
*
* Triggered when: User opens record (view, edit, create, copy, print)
*
* @param {Object} context
* @param {Record} context.newRecord - The record being loaded
* @param {string} context.type - The trigger type (view, edit, create, etc.)
* @param {Form} context.form - The form object (for UI manipulation)
*/
function beforeLoad(context) {
// 📚 MODULE 2: log.debug() writes to Execution Log
// First parameter: title (shows in left column)
// Second parameter: details (shows in right column)
log.debug({
title: 'Hello World!',
details: 'The script executed successfully on record ID: ' + context.newRecord.id
});
// 📚 MODULE 3: Getting values from the record object
// context.newRecord gives us access to all field values
var employeeName = context.newRecord.getValue('entityid');
log.debug('Employee Name', employeeName);
}
// 📚 MODULE 2: Return object exports entry point functions
// The property name MUST match the entry point exactly
return {
beforeLoad: beforeLoad
};
});
- Upload script to File Cabinet → SuiteScripts folder
- Create Script Record: Customization → Scripting → Scripts → New
- Create Deployment: Set Status to "Testing", Log Level to "Debug"
- Open any Employee record
- Check Execution Log in the script deployment record
Exercise 2: Coupon Code Validation (Client Script)
📚 Module 4: Understanding Entry Points🎯 Scenario
SuiteDreams wants to add coupon codes to customer records. When "Apply Coupon" is checked, the coupon code field should be enabled. Validation requires exactly 5 characters.
Required Custom Fields
| Field | ID | Type |
|---|---|---|
| Apply Coupon | custentity_urk_apply_coupon | Checkbox |
| Coupon Code | custentity_urk_coupon_code | Free-Form Text |
Complete Solution: sdr_cs_customer.js
/**
* @NApiVersion 2.1
* @NScriptType ClientScript
* @NModuleScope SameAccount
*
* EXERCISE 2: Coupon Code Validation
* ═══════════════════════════════════
* 📚 MODULE 4 CONCEPTS:
* - Client Script entry points (pageInit, fieldChanged, validateField, saveRecord)
* - Enabling/disabling fields dynamically
* - Field-level and form-level validation
* - Returning boolean from validation functions
*
* Deploy to: Customer record
*/
define(['N/log'], function(log) {
/**
* pageInit - Runs when page first loads
* ─────────────────────────────────────
* 📚 MODULE 4: Initialize form state
*
* Use for: Setting defaults, enabling/disabling fields based on existing values
*/
function pageInit(context) {
var rec = context.currentRecord;
var mode = context.mode; // 'create', 'edit', 'view', 'copy'
log.debug('pageInit', 'Mode: ' + mode);
// 📚 MODULE 4: Get field reference to manipulate properties
// getField() returns a Field object for UI manipulation
var couponField = rec.getField({ fieldId: 'custentity_urk_coupon_code' });
// Check if Apply Coupon is already checked (for edit mode)
var applyCoupon = rec.getValue('custentity_urk_apply_coupon');
if (couponField) {
// 📚 MODULE 4: isDisabled property controls field editability
couponField.isDisabled = !applyCoupon;
}
}
/**
* fieldChanged - Runs when any field value changes
* ─────────────────────────────────────────────────
* 📚 MODULE 4: React to field changes
*
* context.fieldId tells you WHICH field changed
* Use to: Enable/disable dependent fields, calculate values
*/
function fieldChanged(context) {
var rec = context.currentRecord;
var fieldId = context.fieldId;
// Only react to Apply Coupon checkbox
if (fieldId === 'custentity_urk_apply_coupon') {
var applyCoupon = rec.getValue('custentity_urk_apply_coupon');
var couponField = rec.getField({ fieldId: 'custentity_urk_coupon_code' });
if (applyCoupon) {
// Enable the coupon code field
couponField.isDisabled = false;
} else {
// Disable and CLEAR the coupon code field
// 📚 MODULE 4: Best practice - clear dependent fields when disabling
couponField.isDisabled = true;
rec.setValue({
fieldId: 'custentity_urk_coupon_code',
value: ''
});
}
}
}
/**
* validateField - Runs when user leaves a field
* ──────────────────────────────────────────────
* 📚 MODULE 4: Field-level validation
*
* MUST return true or false:
* true = accept the value, move to next field
* false = reject, keep focus on current field
*/
function validateField(context) {
var rec = context.currentRecord;
var fieldId = context.fieldId;
if (fieldId === 'custentity_urk_coupon_code') {
var applyCoupon = rec.getValue('custentity_urk_apply_coupon');
var couponCode = rec.getValue('custentity_urk_coupon_code');
// 📚 MODULE 4: Validate only when checkbox is checked and code exists
if (applyCoupon && couponCode && couponCode.length !== 5) {
alert('Coupon code must be exactly 5 characters.');
return false; // Keep focus on field
}
}
return true; // Always return true for other fields!
}
/**
* saveRecord - Runs when user clicks Save
* ────────────────────────────────────────
* 📚 MODULE 4: Form-level validation before submission
*
* MUST return true or false:
* true = allow save to proceed
* false = cancel save, stay on page
*
* TIP: Use confirm() for user confirmation dialogs
*/
function saveRecord(context) {
var rec = context.currentRecord;
var applyCoupon = rec.getValue('custentity_urk_apply_coupon');
var couponCode = rec.getValue('custentity_urk_coupon_code');
// 📚 MODULE 4: Final validation before save
if (applyCoupon) {
if (!couponCode || couponCode.length !== 5) {
alert('Please enter a valid 5-character coupon code.');
return false; // Cancel save
}
}
// 📚 MODULE 4: Best practice - always return true at the end
return true;
}
return {
pageInit: pageInit,
fieldChanged: fieldChanged,
validateField: validateField,
saveRecord: saveRecord
};
});
Exercise 3: Create Task on New Customer
📚 Module 5: SuiteScript Modules🎯 Scenario
SuiteDreams wants to automatically remind sales reps to follow up with new customers. When a customer is created, automatically create a Task record assigned to their sales rep.
Complete Solution: sdr_ue_customer.js
/**
* @NApiVersion 2.1
* @NScriptType UserEventScript
*
* EXERCISE 3: Create Follow-up Task
* ══════════════════════════════════
* 📚 MODULE 5 CONCEPTS:
* - Loading multiple modules (N/record, N/log, N/email, N/runtime)
* - Creating records with record.create()
* - Using record.Type enum values
* - Checking context.type for operation filtering
*
* Deploy to: Customer record
*/
define(['N/record', 'N/log', 'N/email', 'N/runtime'],
function(record, log, email, runtime) {
/**
* afterSubmit - Runs AFTER record is saved to database
* ─────────────────────────────────────────────────────
* 📚 MODULE 4: Best entry point for creating related records
*
* Why afterSubmit? The customer ID now exists in the database,
* so we can link the task to it.
*/
function afterSubmit(context) {
// 📚 MODULE 5: Filter by operation type
// Only create task for NEW customers, not edits
if (context.type !== context.UserEventType.CREATE) {
return;
}
var customer = context.newRecord;
var customerId = customer.id;
var salesRepId = customer.getValue('salesrep');
log.debug('Creating Task', 'Customer: ' + customerId + ' | Sales Rep: ' + salesRepId);
try {
// 📚 MODULE 5: record.create() to make new records
// Use record.Type enum for type safety
var task = record.create({
type: record.Type.TASK
});
// 📚 MODULE 3: setValue() to set field values
task.setValue({
fieldId: 'title',
value: 'New Customer Follow-up'
});
task.setValue({
fieldId: 'message',
value: 'Please take care of this customer and follow-up with them soon.'
});
// 📚 MODULE 5: PRIORITY uses string values: 'HIGH', 'MEDIUM', 'LOW'
task.setValue({
fieldId: 'priority',
value: 'HIGH'
});
// Link task to the customer (COMPANY field)
task.setValue({
fieldId: 'company',
value: customerId
});
// Assign to sales rep if one exists
if (salesRepId) {
task.setValue({
fieldId: 'assigned',
value: salesRepId
});
}
// 📚 MODULE 5: save() commits the record to database
// Returns the internal ID of the new record
var taskId = task.save();
log.audit('Task Created', 'Task ID: ' + taskId);
} catch (e) {
// 📚 MODULE 14: Always handle errors gracefully
log.error('Error Creating Task', e.message);
}
}
/**
* BONUS: Send Welcome Email
* ─────────────────────────
* 📚 MODULE 5: Using N/email module
*/
function sendWelcomeEmail(customerId) {
// 📚 MODULE 5: Get current user as email sender
var currentUser = runtime.getCurrentUser();
email.send({
author: currentUser.id,
recipients: customerId, // Customer internal ID
subject: 'Welcome to SuiteDreams!',
body: 'Thank you for becoming a customer. We look forward to serving you.'
});
}
return {
afterSubmit: afterSubmit
};
});
Exercise 5: Product Shortage Search
📚 Module 7: Searching in NetSuite🎯 Scenario
Create a search to find product shortages - items where the preferred quantity exceeds available inventory. Demonstrate both saved search loading and programmatic search creation.
Complete Solution: sdr_ss_product_shortage.js
/**
* @NApiVersion 2.1
* @NScriptType ScheduledScript
*
* EXERCISE 5: Product Shortage Search
* ════════════════════════════════════
* 📚 MODULE 7 CONCEPTS:
* - Loading saved searches with search.load()
* - Creating searches with search.create()
* - Search filters and columns
* - Processing results with run().each()
* - Using getValue() vs getText()
*
* Custom Record: Product Preferences (custrecord_urk_prod_pref)
* Fields: Customer, Item, Preferred Quantity
*/
define(['N/search', 'N/log', 'N/record'], function(search, log, record) {
function execute(context) {
// ═══════════════════════════════════════════════════════════
// METHOD 1: Load a Saved Search
// 📚 MODULE 7: Use script ID (not internal ID) for portability
// ═══════════════════════════════════════════════════════════
var savedSearch = search.load({
id: 'customsearch_urk_prod_shortages' // Script ID of saved search
});
// ═══════════════════════════════════════════════════════════
// METHOD 2: Create Search Programmatically
// 📚 MODULE 7: More flexible, can add dynamic filters
// ═══════════════════════════════════════════════════════════
var productSearch = search.create({
type: 'customrecord_urk_prod_pref',
// 📚 MODULE 7: Filter Expression Syntax
// [fieldId, operator, value]
filters: [
['custrecord_urk_prod_pref_qty', 'greaterthan', 2],
'AND',
// 📚 MODULE 7: Join syntax - field on related record
['custrecord_urk_prod_pref_customer.subsidiary', 'anyof', 1]
],
// 📚 MODULE 7: Columns to return
columns: [
search.createColumn({ name: 'custrecord_urk_prod_pref_customer' }),
// Join column: get email from customer record
search.createColumn({
name: 'email',
join: 'custrecord_urk_prod_pref_customer'
}),
search.createColumn({ name: 'custrecord_urk_prod_pref_item' }),
search.createColumn({ name: 'custrecord_urk_prod_pref_qty' }),
// Join column: get available qty from item record
search.createColumn({
name: 'quantityavailable',
join: 'custrecord_urk_prod_pref_item'
})
]
});
// ═══════════════════════════════════════════════════════════
// PROCESSING RESULTS
// 📚 MODULE 7: run().each() processes up to 4000 results
// ═══════════════════════════════════════════════════════════
var resultCount = 0;
productSearch.run().each(function(result) {
// 📚 MODULE 7: getValue() returns internal ID for list fields
// getText() returns display value
var customerId = result.getValue('custrecord_urk_prod_pref_customer');
var customerName = result.getText('custrecord_urk_prod_pref_customer');
var itemId = result.getValue('custrecord_urk_prod_pref_item');
var itemName = result.getText('custrecord_urk_prod_pref_item');
var preferredQty = parseInt(result.getValue('custrecord_urk_prod_pref_qty')) || 0;
// 📚 MODULE 7: Join columns use the join parameter
var availableQty = parseInt(result.getValue({
name: 'quantityavailable',
join: 'custrecord_urk_prod_pref_item'
})) || 0;
var customerEmail = result.getValue({
name: 'email',
join: 'custrecord_urk_prod_pref_customer'
});
log.debug('Product Preference', JSON.stringify({
customer: customerName,
item: itemName,
preferred: preferredQty,
available: availableQty,
email: customerEmail
}));
// Check for shortage
if (availableQty < preferredQty) {
log.audit('SHORTAGE DETECTED',
itemName + ' - Need: ' + preferredQty + ', Have: ' + availableQty);
// Create support case (see Exercise 6)
createSupportCase(customerId, itemId, itemName, preferredQty, availableQty);
}
resultCount++;
return true; // Continue to next result (false stops)
});
log.audit('Search Complete', 'Processed ' + resultCount + ' records');
}
/**
* Helper: Create Support Case for Shortage
* 📚 MODULE 5: Creating records
*/
function createSupportCase(customerId, itemId, itemName, preferred, available) {
var supportCase = record.create({ type: record.Type.SUPPORT_CASE });
supportCase.setValue('title', 'Item low for customer');
supportCase.setValue('company', customerId);
supportCase.setValue('incomingmessage',
'Customer prefers to purchase ' + preferred + ' x ' + itemName +
' but only ' + available + ' are in stock.');
var caseId = supportCase.save();
log.audit('Case Created', 'Case ID: ' + caseId);
}
return { execute: execute };
});
Exercise 7: Map/Reduce Payment Report
📚 Module 9: Map/Reduce Scripts🎯 Scenario
Calculate total deposited and undeposited payments per customer using Map/Reduce for automatic governance handling and parallel processing.
Complete Solution: sdr_mr_payment_report.js
/**
* @NApiVersion 2.1
* @NScriptType MapReduceScript
*
* EXERCISE 7: Customer Payment Report
* ════════════════════════════════════
* 📚 MODULE 9 CONCEPTS:
* - Four M/R stages: getInputData, map, reduce, summarize
* - Automatic governance handling
* - Parallel processing
* - Key-value pair paradigm
* - JSON parsing between stages
*/
define(['N/search', 'N/log'], function(search, log) {
/**
* STAGE 1: getInputData
* ─────────────────────
* 📚 MODULE 9: Returns data source for processing
*
* Can return:
* - Search object (most common)
* - Array of objects
* - Saved search reference { type: 'search', id: 'customsearch_xxx' }
*/
function getInputData() {
log.audit('M/R Started', 'getInputData executing');
// 📚 MODULE 9: Return search - each result goes to map()
return search.create({
type: 'transaction',
filters: [
['type', 'anyof', 'CustPymt'],
'AND',
['mainline', 'is', true]
],
columns: ['entity', 'status', 'amount']
});
// Alternative: Reference saved search
// return { type: 'search', id: 'customsearch_urk_payments' };
}
/**
* STAGE 2: map
* ────────────
* 📚 MODULE 9: Process EACH input individually
*
* Called once per search result (automatically parallelized)
* context.value = JSON string of search result
* context.write() = send key-value pair to reduce stage
*/
function map(context) {
// 📚 MODULE 9: Parse JSON string from search result
var searchResult = JSON.parse(context.value);
// Extract values from the search result structure
var customerId = searchResult.values.entity.value;
var customerName = searchResult.values.entity.text;
var status = searchResult.values.status.value;
var amount = parseFloat(searchResult.values.amount) || 0;
// 📚 MODULE 9: context.write() sends to reduce stage
// All values with the SAME KEY go to the SAME reduce() call
context.write({
key: customerId,
value: JSON.stringify({
name: customerName,
status: status,
amount: amount
})
});
}
/**
* STAGE 3: reduce
* ───────────────
* 📚 MODULE 9: Process all values for each unique key
*
* context.key = the key from map()
* context.values = ARRAY of all values written with that key
*/
function reduce(context) {
var customerId = context.key;
var values = context.values; // Array of JSON strings
var depositedTotal = 0;
var undepositedTotal = 0;
var customerName = '';
// 📚 MODULE 9: Loop through all payments for this customer
values.forEach(function(valueStr) {
var payment = JSON.parse(valueStr);
customerName = payment.name;
if (payment.status === 'deposited') {
depositedTotal += payment.amount;
} else {
undepositedTotal += payment.amount;
}
});
// Log results for this customer
log.audit('Customer Totals',
customerName + ' | Deposited: $' + depositedTotal.toFixed(2) +
' | Undeposited: $' + undepositedTotal.toFixed(2));
// 📚 MODULE 9: Optional - write to summarize stage
context.write({
key: customerId,
value: depositedTotal + undepositedTotal
});
}
/**
* STAGE 4: summarize
* ──────────────────
* 📚 MODULE 9: Final processing, error handling, reporting
*
* summary object contains:
* - usage, concurrency, yields (statistics)
* - inputSummary, mapSummary, reduceSummary (errors)
* - output (final key-value pairs from reduce)
*/
function summarize(summary) {
// 📚 MODULE 9: Log execution statistics
log.audit('Execution Stats', JSON.stringify({
usage: summary.usage, // Total governance used
concurrency: summary.concurrency, // Max parallel threads
yields: summary.yields // Times script yielded
}));
// 📚 MODULE 9: Check for errors in each stage
if (summary.inputSummary.error) {
log.error('Input Error', summary.inputSummary.error);
}
summary.mapSummary.errors.iterator().each(function(key, error) {
log.error('Map Error', 'Key: ' + key + ' Error: ' + error);
return true;
});
summary.reduceSummary.errors.iterator().each(function(key, error) {
log.error('Reduce Error', 'Key: ' + key + ' Error: ' + error);
return true;
});
// 📚 MODULE 9: Process final output
var grandTotal = 0;
summary.output.iterator().each(function(key, value) {
grandTotal += parseFloat(value) || 0;
return true;
});
log.audit('GRAND TOTAL', '$' + grandTotal.toFixed(2));
}
return {
getInputData: getInputData,
map: map,
reduce: reduce,
summarize: summarize
};
});
Exercise 8: Sales Order Financing Suitelet
📚 Module 12: NetSuite Pages🎯 Scenario
Create a custom form for entering financing information on sales orders. Users are redirected to the Suitelet after saving an order, enter the financing price, and are redirected back.
Complete Solution: sdr_sl_salesorder_finance.js
/**
* @NApiVersion 2.1
* @NScriptType Suitelet
*
* EXERCISE 8: Sales Order Financing Form
* ═══════════════════════════════════════
* 📚 MODULE 12 CONCEPTS:
* - Creating forms with N/ui/serverWidget
* - GET vs POST request handling
* - URL parameters
* - Redirecting users
* - Field types and display types
*/
define(['N/ui/serverWidget', 'N/record', 'N/redirect', 'N/log'],
function(serverWidget, record, redirect, log) {
/**
* onRequest - Main entry point for Suitelets
* ───────────────────────────────────────────
* 📚 MODULE 12: Handles both GET and POST requests
*/
function onRequest(context) {
var request = context.request;
var response = context.response;
// 📚 MODULE 12: Check request method
if (request.method === 'GET') {
// Display the form
showForm(context);
} else {
// Process form submission (POST)
processForm(context);
}
}
/**
* Display the Financing Form
* 📚 MODULE 12: Building custom UI pages
*/
function showForm(context) {
var request = context.request;
// 📚 MODULE 12: Get parameters passed from User Event script
var orderId = request.parameters.custparam_orderid;
var orderNum = request.parameters.custparam_ordernum;
var customerId = request.parameters.custparam_customer;
var customerName = request.parameters.custparam_customername;
var total = request.parameters.custparam_total;
var financingPrice = request.parameters.custparam_financing || '';
// 📚 MODULE 12: Create form with serverWidget.createForm()
var form = serverWidget.createForm({
title: 'Sales Order Financing'
});
// 📚 MODULE 12: Help text field for instructions
form.addField({
id: 'custpage_help', // MUST start with custpage_
type: serverWidget.FieldType.HELP,
label: 'Please assign a financing price to this sales order, then click Submit.'
});
// 📚 MODULE 12: Display-only fields (INLINE type)
var orderNumField = form.addField({
id: 'custpage_ordernum',
type: serverWidget.FieldType.TEXT,
label: 'Order #'
});
orderNumField.defaultValue = orderNum || 'To Be Generated';
orderNumField.updateDisplayType({ displayType: serverWidget.FieldDisplayType.INLINE });
var customerField = form.addField({
id: 'custpage_customer',
type: serverWidget.FieldType.TEXT,
label: 'Customer'
});
customerField.defaultValue = customerName;
customerField.updateDisplayType({ displayType: serverWidget.FieldDisplayType.INLINE });
var totalField = form.addField({
id: 'custpage_total',
type: serverWidget.FieldType.CURRENCY,
label: 'Order Total'
});
totalField.defaultValue = total;
totalField.updateDisplayType({ displayType: serverWidget.FieldDisplayType.INLINE });
// 📚 MODULE 12: Editable field for user input
var financingField = form.addField({
id: 'custpage_financing',
type: serverWidget.FieldType.CURRENCY,
label: 'Financing Price'
});
financingField.defaultValue = financingPrice;
financingField.isMandatory = true;
// 📚 MODULE 12: Hidden field to pass order ID to POST
var orderIdField = form.addField({
id: 'custpage_orderid',
type: serverWidget.FieldType.TEXT,
label: 'Order ID'
});
orderIdField.defaultValue = orderId;
orderIdField.updateDisplayType({ displayType: serverWidget.FieldDisplayType.HIDDEN });
// 📚 MODULE 12: Submit button for form submission
form.addSubmitButton({ label: 'Save Financing Info' });
// 📚 MODULE 12: Write form to response
context.response.writePage(form);
}
/**
* Process Form Submission
* 📚 MODULE 12: Handling POST requests
*/
function processForm(context) {
var request = context.request;
// 📚 MODULE 12: Get values from submitted form
// Use the custpage_ field IDs
var orderId = request.parameters.custpage_orderid;
var financingPrice = request.parameters.custpage_financing;
log.debug('Processing Form', 'Order: ' + orderId + ' | Price: ' + financingPrice);
try {
// 📚 MODULE 5: Load and update the sales order
var salesOrder = record.load({
type: record.Type.SALES_ORDER,
id: orderId
});
salesOrder.setValue({
fieldId: 'custbody_urk_financing_price',
value: financingPrice
});
salesOrder.save();
log.audit('Order Updated', 'Financing price set to ' + financingPrice);
} catch (e) {
log.error('Update Failed', e.message);
}
// 📚 MODULE 12: Redirect back to the sales order
redirect.toRecord({
type: record.Type.SALES_ORDER,
id: orderId
});
}
return { onRequest: onRequest };
});
User Event Script to Redirect (sdr_ue_salesorder.js)
/**
* @NApiVersion 2.1
* @NScriptType UserEventScript
*
* Redirect to Suitelet after Sales Order save
* 📚 MODULE 12: Connecting UE to Suitelet
*/
define(['N/redirect', 'N/log'], function(redirect, log) {
function afterSubmit(context) {
if (context.type !== context.UserEventType.CREATE &&
context.type !== context.UserEventType.EDIT) {
return;
}
var order = context.newRecord;
// 📚 MODULE 12: redirect.toSuitelet() with parameters
redirect.toSuitelet({
scriptId: 'customscript_urk_sl_salesorder_finance',
deploymentId: 'customdeploy_urk_sl_salesorder_finance',
parameters: {
// 📚 MODULE 12: Best practice - prefix with custparam_
custparam_orderid: order.id,
custparam_ordernum: order.getValue('tranid'),
custparam_customer: order.getValue('entity'),
custparam_customername: order.getText('entity'),
custparam_total: order.getValue('total'),
custparam_financing: order.getValue('custbody_urk_financing_price')
}
});
}
return { afterSubmit: afterSubmit };
});
Exercise 9: Coupon Validation RESTlet
📚 Module 13: Web Services🎯 Scenario
Hide coupon code validation logic on the server side using a RESTlet. The client script calls the RESTlet to validate coupon codes without exposing valid codes in the browser.
Complete Solution: sdr_rl_coupon_code.js
/**
* @NApiVersion 2.1
* @NScriptType Restlet
*
* EXERCISE 9: Coupon Code Validation RESTlet
* ═══════════════════════════════════════════
* 📚 MODULE 13 CONCEPTS:
* - RESTlet entry points (get, post, put, delete)
* - URL parameters vs request body
* - Calling RESTlets from client scripts
* - Hiding business logic on server
*/
define(['N/log'], function(log) {
/**
* GET Request Handler
* ────────────────────
* 📚 MODULE 13: GET requests pass parameters via URL
* requestParams object contains URL query parameters
*/
function get(requestParams) {
// 📚 MODULE 13: Parameters come directly as object properties
var couponCode = requestParams.custparam_couponcode;
log.debug('Validating Coupon', couponCode);
// 📚 SECURITY: Valid codes are hidden on server, not in client JS
var validCodes = ['ABC12', 'XYZ99', 'SAVE5'];
if (validCodes.indexOf(couponCode) !== -1) {
return 'valid';
} else {
return 'invalid';
}
}
/**
* POST Request Handler
* ─────────────────────
* 📚 MODULE 13: POST requests pass data in request body
*/
function post(requestBody) {
// requestBody is already parsed JSON object
var couponCode = requestBody.couponCode;
var customerId = requestBody.customerId;
log.debug('POST Validation', JSON.stringify(requestBody));
// More complex validation logic could go here
return {
success: true,
valid: couponCode === 'ABC12',
message: couponCode === 'ABC12' ? 'Coupon applied!' : 'Invalid coupon'
};
}
return {
get: get,
post: post
};
});
Calling the RESTlet from Client Script
/**
* Client Script - Calling RESTlet
* 📚 MODULE 13: Use N/https and N/url modules
*/
define(['N/https', 'N/url', 'N/log'], function(https, url, log) {
function validateField(context) {
if (context.fieldId !== 'custentity_urk_coupon_code') {
return true;
}
var rec = context.currentRecord;
var couponCode = rec.getValue('custentity_urk_coupon_code');
if (!couponCode) return true;
// 📚 MODULE 13: Build RESTlet URL dynamically
var restletUrl = url.resolveScript({
scriptId: 'customscript_urk_rl_coupon_code',
deploymentId: 'customdeploy_urk_rl_coupon_code'
});
// 📚 MODULE 13: Append parameters for GET request
restletUrl += '&custparam_couponcode=' + encodeURIComponent(couponCode);
// 📚 MODULE 13: Call RESTlet with https.get()
var response = https.get({ url: restletUrl });
if (response.body === 'invalid') {
alert('Invalid coupon code. Please try again.');
return false;
}
return true;
}
return { validateField: validateField };
});
Exercise 10: Workflow Action Script
📚 Module 11: Workflow Action Scripts🎯 Scenario
Extend a Sales Order approval workflow with a custom action that updates the related customer record with order information.
Complete Solution: sdr_wf_update_customer.js
/**
* @NApiVersion 2.1
* @NScriptType WorkflowActionScript
*
* EXERCISE 10: Update Customer from Workflow
* ═══════════════════════════════════════════
* 📚 MODULE 11 CONCEPTS:
* - Workflow Action Script type
* - Getting record from workflow context
* - Passing data via workflow parameters
* - Returning values for workflow branching
*/
define(['N/record', 'N/runtime', 'N/log'], function(record, runtime, log) {
/**
* onAction - Entry point for Workflow Action Scripts
* ───────────────────────────────────────────────────
* 📚 MODULE 11: Triggered when workflow reaches this action
*
* Return value can be used for workflow branching
*/
function onAction(context) {
// 📚 MODULE 11: Get the record that triggered the workflow
var salesOrder = context.newRecord;
var orderId = salesOrder.id;
// 📚 MODULE 10: Get script parameter passed from workflow
var script = runtime.getCurrentScript();
var orderDate = script.getParameter({
name: 'custscript_urk_order_date'
});
// Get values from sales order
var customerId = salesOrder.getValue('entity');
var lineCount = salesOrder.getLineCount({ sublistId: 'item' });
// Build notes string
var notes = 'Last Order Date: ' + orderDate + '\\n';
notes += 'Unique items ordered: ' + lineCount;
log.debug('Updating Customer', 'ID: ' + customerId + ' | Notes: ' + notes);
try {
// Load and update customer
var customer = record.load({
type: record.Type.CUSTOMER,
id: customerId
});
customer.setValue({
fieldId: 'comments',
value: notes
});
var savedId = customer.save();
log.audit('Customer Updated', 'Saved ID: ' + savedId);
// 📚 MODULE 11: Return value for workflow branching
return 'SUCCESS';
} catch (e) {
log.error('Update Failed', e.message);
return 'FAILED';
}
}
return { onAction: onAction };
});
- Create workflow at Customization → Workflow → Workflows
- Add a State with the custom action
- Set TRIGGER ON to "After Record Submit"
- Create script parameter in the script record
- Map parameter in workflow action (VALUE FIELD = Date)
- Set STORE RESULTS IN to a state field for branching