📝 Appendix B: Hands-On Exercises with Full Solutions

Complete working code from Oracle Student Guide • SuiteDreams Training Scenario

📘 About These Exercises These exercises are from Oracle's official SuiteScript 2.0 training course. They build upon each other using a fictional company called "SuiteDreams" - a global custom furniture manufacturer. Each exercise includes the complete working code with annotations pointing to the relevant module concepts.

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

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
    };
});
✅ How to Test
  1. Upload script to File Cabinet → SuiteScripts folder
  2. Create Script Record: Customization → Scripting → Scripts → New
  3. Create Deployment: Set Status to "Testing", Log Level to "Debug"
  4. Open any Employee record
  5. 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

FieldIDType
Apply Couponcustentity_urk_apply_couponCheckbox
Coupon Codecustentity_urk_coupon_codeFree-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 };
});
⚠️ Workflow Setup Required
  1. Create workflow at Customization → Workflow → Workflows
  2. Add a State with the custom action
  3. Set TRIGGER ON to "After Record Submit"
  4. Create script parameter in the script record
  5. Map parameter in workflow action (VALUE FIELD = Date)
  6. Set STORE RESULTS IN to a state field for branching