/** *@NApiVersion 2.x *@NScriptType MapReduceScript */ define(['N/record', 'N/search', 'N/error', 'N/log', 'N/runtime', 'N/cache', 'N/format'], function (record, search, error, log, runtime, cache, format) { function findById(type, id) { return record.load({ type: type, id: id, isDynamic: true }); } function parseJSONStringsRecursively(obj) { if (typeof obj === 'string') { try { var parsed = JSON.parse(obj); if (parsed !== null && typeof parsed === 'object') { return parseJSONStringsRecursively(parsed); } return obj; } catch (e) { return obj; } } else if (Array.isArray(obj)) { return obj.map(parseJSONStringsRecursively); } else if (typeof obj === 'object' && obj !== null) { var result = {}; for (var key in obj) { if (obj.hasOwnProperty(key)) { result[key] = parseJSONStringsRecursively(obj[key]); } } return result; } return obj; } function isControlField(fieldId, sublistId) { // Common control fields that stay at sublist level var controlFields = ['label', 'defaultbilling', 'defaultshipping', 'isresidential', 'isinactive']; return controlFields.indexOf(fieldId.toLowerCase()) !== -1; } function processSublist(rec, sublistId, items) { log.debug('Processing sublist', {sublistId: sublistId, itemCount: items ? items.length : 0}); try { // Clear existing lines var lineCount = rec.getLineCount({sublistId: sublistId}); for (var j = lineCount - 1; j >= 0; j--) { rec.removeLine({sublistId: sublistId, line: j}); } log.debug('Cleared existing lines', lineCount); // Process each line for (var i = 0; i < items.length; i++) { var item = items[i]; log.debug('Processing line ' + i, { sublistId: sublistId, itemKeys: Object.keys(item) }); rec.selectNewLine({sublistId: sublistId}); // Separate fields into control fields and subrecord fields var controlFields = {}; var subrecordFields = {}; for (var fieldId in item) { if (item.hasOwnProperty(fieldId)) { var value = item[fieldId]; if (value && typeof value === 'object' && !Array.isArray(value)) { // This is a subrecord subrecordFields[fieldId] = value; } else if (isControlField(fieldId, sublistId)) { // This is a control field controlFields[fieldId] = value; } else { // Regular field - try to set at sublist level controlFields[fieldId] = value; } } } log.debug('Field separation', { line: i, controlFields: Object.keys(controlFields), subrecordFields: Object.keys(subrecordFields) }); // Set control fields at sublist level var fieldsSet = 0; for (var ctrlField in controlFields) { if (controlFields.hasOwnProperty(ctrlField)) { var ctrlValue = controlFields[ctrlField]; try { // Handle boolean/checkbox fields var processedValue = ctrlValue; if (typeof ctrlValue === 'string' && (ctrlValue.toUpperCase() === 'T' || ctrlValue.toUpperCase() === 'F' || ctrlValue.toLowerCase() === 'true' || ctrlValue.toLowerCase() === 'false')) { processedValue = (ctrlValue.toUpperCase() === 'T' || ctrlValue.toLowerCase() === 'true'); } // Handle date fields in sublists try { var lineField = rec.getCurrentSublistField({ sublistId: sublistId, fieldId: ctrlField }); if (lineField && lineField.type === 'date' && ctrlValue) { processedValue = format.parse({ value: ctrlValue, type: format.Type.DATE }); log.debug('Parsed date for sublist field', { fieldId: ctrlField, originalValue: ctrlValue, parsedValue: processedValue }); } } catch (fieldCheckError) { // Field check failed, continue with original/processed value log.debug('Could not check field type for ' + ctrlField, fieldCheckError.message); } rec.setCurrentSublistValue({ sublistId: sublistId, fieldId: ctrlField, value: processedValue }); fieldsSet++; log.debug('Set control field', { fieldId: ctrlField, value: processedValue, line: i }); } catch (fieldError) { log.debug('Could not set control field ' + ctrlField, { error: fieldError.message, value: ctrlValue, line: i }); } } } log.debug('Control fields set on line ' + i, fieldsSet); // Now process subrecords BEFORE committing the line for (var subrecordFieldId in subrecordFields) { if (subrecordFields.hasOwnProperty(subrecordFieldId)) { var subrecordData = subrecordFields[subrecordFieldId]; try { log.debug('Processing subrecord on current line', { line: i, subrecordFieldId: subrecordFieldId, dataKeys: Object.keys(subrecordData) }); var subrecord = rec.getCurrentSublistSubrecord({ sublistId: sublistId, fieldId: subrecordFieldId }); if (subrecord) { var subFieldsSet = 0; // Set values on the subrecord for (var subFieldId in subrecordData) { if (subrecordData.hasOwnProperty(subFieldId)) { var subValue = subrecordData[subFieldId]; try { // Handle boolean fields in subrecords too var processedSubValue = subValue; if (typeof subValue === 'string' && (subValue.toUpperCase() === 'T' || subValue.toUpperCase() === 'F' || subValue.toLowerCase() === 'true' || subValue.toLowerCase() === 'false')) { processedSubValue = (subValue.toUpperCase() === 'T' || subValue.toLowerCase() === 'true'); } try { var subField = subrecord.getField({fieldId: subFieldId}); if (subField && subField.type === 'date' && subValue) { processedSubValue = format.parse({ value: subValue, type: format.Type.DATE }); } } catch (e) { // Field check failed, continue with original value } subrecord.setValue({ fieldId: subFieldId, value: processedSubValue }); subFieldsSet++; log.debug('Set subrecord value', { subrecordField: subrecordFieldId, fieldId: subFieldId, value: processedSubValue, line: i }); } catch (fieldError) { log.debug('Could not set subrecord field ' + subFieldId, { error: fieldError.message, value: subValue, line: i }); } } } log.debug('Subrecord fields set', { subrecordField: subrecordFieldId, fieldsSet: subFieldsSet, line: i }); } else { log.error('Could not get subrecord', { sublistId: sublistId, fieldId: subrecordFieldId, line: i }); } } catch (subrecordError) { log.error('Error processing subrecord', { sublistId: sublistId, line: i, fieldId: subrecordFieldId, error: subrecordError.message, stack: subrecordError.stack }); } } } rec.commitLine({sublistId: sublistId}); log.debug('Committed line ' + i); } log.debug('Sublist processing complete', { sublistId: sublistId, linesProcessed: items.length }); } catch (e) { log.error('Error processing sublist ' + sublistId, { error: e.message, stack: e.stack }); throw e; } } function processField(rec, fieldId, value, recordType) { log.debug('Processing field', {fieldId: fieldId, valueType: typeof value}); // Check if this is a sublist field (array of items) if (Array.isArray(value)) { log.debug('Field is array, treating as sublist', { fieldId: fieldId, itemCount: value.length }); try { processSublist(rec, fieldId, value); } catch (sublistError) { log.error('Error processing sublist ' + fieldId, { error: sublistError.message, stack: sublistError.stack }); } } else { // Regular field value try { var field = rec.getField({fieldId: fieldId}); if (field) { if (field.type === 'date' && value) { // Parse the date string using N/format var dateObj = format.parse({ value: value, type: format.Type.DATE }); rec.setValue({fieldId: fieldId, value: dateObj}); log.debug('Set date field value', {fieldId: fieldId, value: value}); } else if (field.type === 'checkbox') { rec.setValue({ fieldId: fieldId, value: value === 't' || value === 'T' || value === 'true' || value === 'True' || value === true }); } else { // For non-numeric field types, ensure number values are coerced // to strings. The MapReduce framework can convert JSON string // values like "29524481704" into JS numbers between getInputData // and map, which breaks text fields in NetSuite. var coercedValue = value; if (typeof value === 'number' && field.type !== 'integer' && field.type !== 'float' && field.type !== 'currency' && field.type !== 'percent') { coercedValue = String(value); } rec.setValue({fieldId: fieldId, value: coercedValue}); } log.debug('Set field value', {fieldId: fieldId, value: value}); } else { log.debug('Field not found, may be sublist or unsupported', fieldId); } } catch (e) { log.debug('Error setting field ' + fieldId, { error: e.message, value: value }); } } } function getInputData() { try { log.debug('getInputData', 'Starting getInputData'); var currentScript = runtime.getCurrentScript(); // Log ALL parameters log.audit('All Script Parameters', { recordType: currentScript.getParameter({name: 'custscript_mr_recordtype'}), data: currentScript.getParameter({name: 'custscript_mr_data'}), jobId: currentScript.getParameter({name: 'custscript_mr_jobid'}) }); var recordType = currentScript.getParameter({ name: 'custscript_mr_recordtype' }); var inputData = currentScript.getParameter({ name: 'custscript_mr_data' }); var jobIdFromParam = currentScript.getParameter({ name: 'custscript_mr_jobid' }); if (!inputData) { log.error('getInputData', 'No input data provided'); return []; } var parsedData = []; var jobId = jobIdFromParam; // Try parameter first try { var dataPayload = JSON.parse(inputData); // Extract jobId from payload if parameter didn't work if (!jobId && dataPayload.jobId) { jobId = dataPayload.jobId; log.audit('JobId extracted from payload', jobId); } // Extract records array var recordsArray = dataPayload.records || dataPayload; if (!Array.isArray(recordsArray)) { if (recordsArray && typeof recordsArray === 'object') { recordsArray = [recordsArray]; } else { log.error('getInputData - Invalid Data Type', { type: typeof recordsArray, value: recordsArray }); return []; } } parsedData = recordsArray.map(function (record) { if (record && typeof record === 'object') { return parseJSONStringsRecursively(record); } return record; }); } catch (parseError) { log.error('getInputData - Parse Error', { error: parseError.toString(), inputData: inputData }); return []; } log.audit('JobId final', { jobId: jobId, fromParam: jobIdFromParam, jobIdType: typeof jobId }); var validRecords = parsedData.filter(function (record) { return record && typeof record === 'object' && !Array.isArray(record); }); if (validRecords.length !== parsedData.length) { log.error('getInputData - Invalid Records', { total: parsedData.length, valid: validRecords.length, invalid: parsedData.length - validRecords.length }); } var keyValueData = []; for (var i = 0; i < validRecords.length; i++) { if (validRecords[i]) { keyValueData.push({ key: i.toString(), value: { record: validRecords[i], recordType: recordType, jobId: jobId, } }); } } log.debug('getInputData - Complete', { recordCount: keyValueData.length, jobId: jobId, sampleValue: keyValueData.length > 0 ? JSON.stringify(keyValueData[0].value) : 'none' }); return keyValueData; } catch (e) { log.error('getInputData Error', { error: e.toString(), stack: e.stack }); return []; } } function map(context) { try { log.debug('map', 'Processing key: ' + context.key); log.debug('map - Raw context.value type', typeof context.value); var data; if (typeof context.value === 'string') { try { data = JSON.parse(context.value); } catch (parseError) { log.error('map - JSON Parse Error', { key: context.key, value: context.value, error: parseError.message }); throw new Error('Failed to parse context.value as JSON'); } } else { data = context.value; } log.debug('map - Parsed data structure', { key: context.key, hasData: !!data, dataKeys: data ? Object.keys(data) : [], hasValue: data ? !!data.value : false, hasRecord: data ? !!data.record : false }); var recordData, recordType; if (data && data.value && data.value.record) { recordData = data.value.record; recordType = data.value.recordType; } else if (data && data.record) { recordData = data.record; recordType = data.recordType; } else { log.error('map - Invalid Data Structure', { key: context.key, data: JSON.stringify(data) }); throw new Error('Invalid record data structure - missing record property'); } if (typeof recordData !== 'object' || recordData === null) { log.error('map - Invalid Record Data', { key: context.key, recordData: recordData, recordDataType: typeof recordData }); throw new Error('Record data must be an object'); } log.debug('map - Record data validated', { recordId: recordData.id, recordType: recordType, fields: Object.keys(recordData) }); var rec = null; if (recordData.id) { rec = findById(recordType, recordData.id); log.debug('Loaded existing record', rec.id); } else { rec = record.create({ type: recordType, isDynamic: true }); log.debug('Creating new record'); } for (var field in recordData) { if (field !== 'id' && recordData.hasOwnProperty(field)) { log.debug('Processing field: ' + field, { fieldType: typeof recordData[field], isArray: Array.isArray(recordData[field]) }); processField(rec, field, recordData[field], recordType); } } var recordId = rec.save(); log.debug('Successfully saved record', 'Record ID: ' + recordId); context.write({ key: 'success', value: JSON.stringify({ recordId: recordId, originalId: recordData.id, action: recordData.id ? 'updated' : 'created', key: context.key, }) }); } catch (e) { log.error('Map Error', { key: context.key, error: e.toString(), stack: e.stack }); context.write({ key: 'error', value: JSON.stringify({ key: context.key, error: e.toString(), message: e.message, stack: e.stack }) }); } } function reduce(context) { try { var successCount = 0; var errorCount = 0; var successes = []; var errors = []; for (var i = 0; i < context.values.length; i++) { var result = JSON.parse(context.values[i]); if (context.key === 'success') { successCount++; successes.push(result); } else if (context.key === 'error') { errorCount++; errors.push(result); } } log.audit('Reduce Results - ' + context.key, { count: context.key === 'success' ? successCount : errorCount, key: context.key }); if (context.key === 'success') { context.write({ key: 'final_success', value: JSON.stringify({ total: successCount, records: successes }) }); } else if (context.key === 'error') { context.write({ key: 'final_error', value: JSON.stringify({ total: errorCount, errors: errors }) }); } } catch (e) { log.error('Reduce Error', e.toString()); } } function summarize(context) { var currentScript = runtime.getCurrentScript(); // Get taskId directly from the MapReduce context var taskId = null; try { // The task ID is available in the summarize context var contextMatch = context.toString().match(/MAPREDUCETASK[^\s]*/); taskId = contextMatch && contextMatch[0] ? contextMatch[0] : null; } catch (e) { log.debug('Could not extract taskId from context', e); } // Fallback: try to get from inputContext if available if (!taskId) { try { var inputData = currentScript.getParameter({name: 'custscript_mr_data'}); if (inputData) { var dataPayload = JSON.parse(inputData); taskId = dataPayload.taskId; log.audit('TaskId extracted from payload in summarize', taskId); } } catch (e) { log.debug('Could not extract taskId from payload', e); } } var jobId = currentScript.getParameter({name: 'custscript_mr_jobid'}); if (!jobId) { try { var inputData = currentScript.getParameter({name: 'custscript_mr_data'}); if (inputData) { var dataPayload = JSON.parse(inputData); jobId = dataPayload.jobId; log.audit('JobId extracted from payload in summarize', jobId); } } catch (e) { log.error('Could not extract jobId from payload', e); } } log.audit('Summarize Started', { scriptId: currentScript.id, deploymentId: currentScript.deploymentId, jobId: jobId, taskId: taskId }); var myCache = cache.getCache({ name: 'mapReduceResults', scope: cache.Scope.PUBLIC }); // If we still don't have taskId, try cache as last resort if (!taskId && jobId) { taskId = myCache.get({key: jobId + '_taskid'}); if (taskId) { log.audit('TaskId retrieved from cache', taskId); } } if (!taskId) { log.error('No task ID found', { jobId: jobId }); // Continue anyway - we can still process results, just won't cache them } var successes = []; var errors = []; var totalSuccess = 0; var totalErrors = 0; try { context.output.iterator().each(function (key, value) { var result = JSON.parse(value); if (key === 'final_success') { totalSuccess = result.total; successes = result.records; } else if (key === 'final_error') { totalErrors = result.total; errors = result.errors; } return true; }); } catch (err) { log.error('Summarize Error Parsing Output', err); } var totalProcessed = totalSuccess + totalErrors; log.audit('Summarize Totals', { jobId: jobId, taskId: taskId, totalProcessed: totalProcessed, successful: totalSuccess, failed: totalErrors }); var finalResults = { status: totalErrors > 0 ? 'COMPLETED_WITH_ERRORS' : 'COMPLETED', jobId: jobId, taskId: taskId, totalProcessed: totalProcessed, successful: totalSuccess, failed: totalErrors, successes: successes, errors: errors, timestamp: new Date().toISOString() }; try { // Cache by jobId (which we always have) if (jobId) { myCache.put({ key: jobId, value: JSON.stringify(finalResults), ttl: 3600 }); log.audit('Results cached by jobId', { key: jobId }); } // Also cache by taskId if available if (taskId) { myCache.put({ key: taskId, value: JSON.stringify(finalResults), ttl: 3600 }); log.audit('Results cached by taskId', { key: taskId }); } log.audit('Results cached successfully', { jobId: jobId, taskId: taskId, size: JSON.stringify(finalResults).length }); // Clean up temporary cache entries if (jobId) { myCache.remove({key: jobId + '_taskid'}); } if (taskId) { myCache.remove({key: taskId + '_jobid'}); } } catch (cacheError) { log.error('Failed to cache final results', { taskId: taskId, error: cacheError.message }); } log.audit('Summarize Complete', { jobId: jobId, taskId: taskId }); } return { getInputData: getInputData, map: map, reduce: reduce, summarize: summarize }; });