Customer Sync to Firebase
/**
* @NApiVersion 2.1
* @NScriptType UserEventScript
*/
/**************************************************************************************************************************************
* Madi International Co LLC-UAE-SCA
*
* MICL-774 : E-receipt Management System for Payment Collection- NetSuite
* MICL-873 : Userevent Script to send the customer data to Sales Web App
* ************************************************************************************************************************************
* Author : Jobin and Jismi IT Services
*
* Date Created : 13-February-2024
*
* Description 1.0.0 : This script is for sending customer data to firebase realtime
*
* REVISION HISTORY
* Version 1.0.0 : 13-February-2024 : Created the initial build by JJ0300
**************************************************************************************************************************************/
define(['N/https', 'N/record', '../Model/jj_cm_customer_model_micl_873.js', '../Library/jj_cm_ns_utility.js', '../ConfigManager/jj_cm_api_configurator_micl_774.js', 'N/runtime', 'N/format'],
(https, record, model, jjUtil, config, runtime, format) => {
/**
* Defines the function definition that is executed before record is loaded.
* @param {Object} scriptContext
* @param {Record} scriptContext.newRecord - New record
* @param {string} scriptContext.type - Trigger type; use values from the context.UserEventType enum
* @param {Form} scriptContext.form - Current form
* @param {ServletRequest} scriptContext.request - HTTP request information sent from the browser for a client action only.
* @since 2015.2
*/
const beforeLoad = (scriptContext) => {
if (scriptContext.type == 'copy') {
try {
log.debug('Executed beforeLoad', '');
let newRec = scriptContext.form;
newRec.setValue({
fieldId: 'custentity_jj_firebase_sync_status',
value: false
});
newRec.setValue({
fieldId: 'custentity_jj_last_firebase_sync_date',
value: ''
});
newRec.setValue({
fieldId: 'custentity_jj_fire_base_sync_error',
value: ''
});
} catch (e) {
log.error('error @ beforeLoad', e);
}
}
}
/**
* Sends customer data to Firebase, and based on the response from Firebase,
* updates the custom fields in the customer record to indicate whether the synchronization was successful.
*
* @param {Object} customerData - The customer data object containing the information to be sent.
* @param {String} recordType - The type of NetSuite record being processed (e.g., "Customer").
* @param {String} customerInternalId - The internal ID of the customer in NetSuite.
*/
function sendCustomerData(customerData, recordType, customerInternalId) {
if (customerData) {
const requestBody = createCustomerDocumentRequestBody(customerData);
log.debug("customer requestBody", requestBody);
let headerObj = {
name: 'Accept-Language',
value: 'en-us'
};
let url;
url = config.generateUrl('customer', customerInternalId, 'createEntityCollection');
log.debug("url", url);
https.request.promise({
method: https.Method.POST,
url: url,
body: JSON.stringify(requestBody),
headers: headerObj
})
.then(function (response) {
log.debug({
title: 'Customer Response',
details: response
});
let responseGot = JSON.parse(response.code);
log.debug('responseGot', responseGot);
if (responseGot == '200') {
record.submitFields({
type: 'customer',
id: customerInternalId,
values: {
custentity_jj_firebase_sync_status: true,
custentity_jj_last_firebase_sync_date: new Date(),
custentity_jj_fire_base_sync_error: ''
}
});
sendAddressData(customerData);
}
else {
record.submitFields({
type: 'customer',
id: customerInternalId,
values: {
custentity_jj_firebase_sync_status: false,
custentity_jj_fire_base_sync_error: response
}
});
}
})
.catch(function onRejected(reason) {
log.debug({
title: 'Invalid Request: ',
details: reason
});
})
}
}
/**
* Deletes customer data from Firebase and triggers the address deletion if successful.
*
* @param {String} recordType - The type of NetSuite record being processed (e.g., "Customer").
* @param {String} customerInternalId - The internal ID of the customer in NetSuite.
* @param {Array} addressArray - An array of address internal IDs to be deleted.
* @param {Object} customerData - The customer data object containing address information.
*/
function deleteCustomerData(recordType, customerInternalId, addressArray, customerData) {
let url = config.generateUrl(recordType, customerInternalId, 'deleteEntity');
log.debug("Delete URL", url);
https.request.promise({
method: https.Method.DELETE,
url: url,
})
.then(response => {
log.debug('Delete Customer Response', response);
if (response.code === 200) {
deleteAddressData(addressArray, customerData, customerInternalId);
} else {
log.error('Failed to delete the existing record in Firebase', response.body);
}
})
.catch(reason => {
log.error('Error deleting record from Firebase', reason);
});
};
/**
* Sends address data of the customer to the firebase.
*
* @param {Object} customerData - The customer data object containing address information to be sent.
* @param {String} addressInternalId - The internal ID of the address in NetSuite to be sent.
*/
function sendAddressData(customerData) {
if (customerData.addresses) {
log.debug('customer Data Address', customerData)
for (i = 0; i < customerData.addresses.length; i++) {
log.debug('customerData.addresses[i]', customerData.addresses[i])
const requestBody = createAddressDocumentRequestBody(customerData.addresses[i], customerData.internalId);
log.debug("address requestBody", requestBody);
let headerObj = {
name: 'Accept-Language',
value: 'en-us'
};
let url;
let recordType = 'address';
url = config.generateUrl(recordType, customerData.addresses[i].addressInternalId, 'createEntityCollection');
log.debug("url", url);
https.request.promise({
method: https.Method.POST,
url: url,
body: JSON.stringify(requestBody),
headers: headerObj
})
.then(function (response) {
if (response.code == 200) {
record.submitFields({
type: 'customer',
id: customerData.internalId,
values: {
custentity_jj_firebase_sync_status: true,
custentity_jj_last_firebase_sync_date: new Date(),
custentity_jj_fire_base_sync_error: ''
}
});
log.debug({
title: ' Address Response',
details: response
});
}
})
.catch(function onRejected(reason) {
log.debug({
title: 'Invalid Request: ',
details: reason
});
})
}
}
}
/**
* Deletes address data from Firebase and triggers customer data sync.
*
* @param {Array} addressArray - An array of address internal IDs to be deleted.
* @param {Object} customerData - The customer data object containing address information.
* @param {String} customerInternalId - The internal ID of the customer in NetSuite.
*/
function deleteAddressData(addressArray, customerData, customerInternalId) {
if (customerData.addresses[0]) {
for (i = 0; i < addressArray.length; i++) {
let url;
let recordType = 'address';
log.debug('addressArray[i]', addressArray[i])
log.debug('customerData.addresses.addressInternalId', customerData.addresses[0].addressInternalId)
if (addressArray[i] === customerData.addresses[0].addressInternalId) {
url = config.generateUrl(recordType, addressArray[i], 'deleteEntity');
log.debug("Delete Address url", url);
https.request.promise({
method: https.Method.DELETE,
url: url,
})
.then(function (response) {
log.debug('Delete Address Response', response);
if (response.code === 200) {
record.submitFields({
type: 'customer',
id: customerInternalId,
values: {
custentity_jj_firebase_sync_status: false,
custentity_jj_fire_base_sync_error: response
}
});
recordType = 'customer'
sendCustomerData(customerData, recordType, customerInternalId);
}
})
.catch(function onRejected(reason) {
log.debug({
title: 'Invalid Request: ',
details: reason
});
})
}
}
} else {
log.debug('addresselse')
recordType = 'customer'
sendCustomerData(customerData, recordType, customerInternalId);
}
}
/**
* Creates a request body for sending customer data.
*
* @param {Object} customerData - The customer data to be sent.
* @returns {Object} The request body formatted for Firebase.
*/
const createCustomerDocumentRequestBody = (customerData) => {
log.debug('dateCreated', customerData.dateCreated);
let dateCreate = format.parse({
value: customerData.dateCreated,
type: format.Type.DATE
});
log.debug('dateCreate', dateCreate);
let dateModify = format.parse({
value: customerData.dateModified,
type: format.Type.DATE
});
log.debug('dateModify', dateModify);
// Extracting only the last part of the subsidiary name
// let subsidiaryName = customerData?.subsidiary ? customerData.subsidiary.split(':').pop().trim() : "";
let countryCode = getCountryCode(customerData.country)
let subsidiariesArray = customerData.subsidiaries ? customerData.subsidiaries.split(',').map(subsidiary => subsidiary.trim()) : [];
let subsidiaryStringArray = subsidiariesArray.map(subsidiary => ({ stringValue: subsidiary }))
//let currencyStringArray = customerData.currency.map(item => ({ stringValue: item.currency }));
let currencyStringArray = customerData.currency.map(item => item.currency).join(',');
log.debug("currencyStringArray",currencyStringArray)
currencyStringArray = [...new Set(customerData.currency.map(item => item.currency))].join(',');
log.debug('after format',currencyStringArray);
let requestBody = {
fields: {
date_created: {
timestampValue: dateCreate
},
date_modified: {
timestampValue: dateModify
},
full_name: {
stringValue: customerData?.companyName || ""
},
email: {
stringValue: customerData?.email || ""
},
primary_subsidiary: {
stringValue: customerData?.subsidiary // Set only the last part of the subsidiary
},
subsidiaries: {
arrayValue: {
values: subsidiaryStringArray
}
},
salesrep: {
stringValue: customerData?.salesRep || ""
},
country: {
stringValue: countryCode || ""
},
city: {
stringValue: customerData?.city || ""
},
state: {
stringValue: customerData?.state || ""
},
status: {
stringValue: customerData?.status || ""
},
code: {
stringValue: customerData?.code || ""
},
vat_number: {
stringValue: customerData?.vat || ""
},
currency: {
stringValue: currencyStringArray || ""
}
}
};
return requestBody;
}
/**
* Creates a request body for sending address data of a customer.
*
* @param {Object} addresses - The customer data including address information.
* @param {String} customerInternalId - The internal ID of the customer in NetSuite.
* @returns {Object} The request body formatted for Firebase, focusing on address information.
*/
const createAddressDocumentRequestBody = (addresses, customerInternalId) => {
let requestBody = {
fields: {
country: {
stringValue: addresses?.country || ""
},
attention: {
stringValue: addresses?.attention || ""
},
addressee: {
stringValue: addresses?.addressee || ""
},
phone: {
stringValue: addresses?.addressPhone || ""
},
address_line_1: {
stringValue: addresses?.address1 || ""
},
address_line_2: {
stringValue: addresses?.address2 || ""
},
city: {
stringValue: addresses?.city || ""
},
zip: {
stringValue: addresses?.zipCode || ""
},
state: {
stringValue: addresses?.state || ""
},
cu_id: {
referenceValue: `projects/e-receipts-app-efcfe/databases/(default)/documents/customer/${customerInternalId}` || ""
}
}
};
return requestBody;
}
/**
* Compares two objects or arrays recursively to check if they are deeply equal.
*
* This function checks if two objects (or arrays) have the same structure and content.
* @param {Object|Array} obj1 - The first object or array to compare.
* @param {Object|Array} obj2 - The second object or array to compare.
* @returns {Boolean} - Returns `true` if both objects/arrays are deeply equal, otherwise `false`.
*/
function compareObjects(obj1, obj2) {
// Check if both arguments are objects and not null
if (typeof obj1 === 'object' && obj1 !== null && typeof obj2 === 'object' && obj2 !== null) {
// Iterate through the properties of obj1
for (let key in obj1) {
// Ensure obj2 has the same key
if (!obj2.hasOwnProperty(key)) {
return false;
}
// Recursively compare the values if they are objects or arrays
if (!compareObjects(obj1[key], obj2[key])) {
return false;
}
}
return true;
} else {
// If the values are not objects (i.e., primitives), directly compare them
return obj1 === obj2;
}
}
/**
* Retrieves the country code for a given country name.
*
* This function takes a country name as input and returns the corresponding
* ISO 3166-1 alpha-2 country code. If the country name is not found in the
* predefined list, it returns "Country code not found".
*
* @param {string} countryName - The name of the country for which to retrieve the code.
* @returns {string} The corresponding country code or a message indicating the code was not found.
*
*/
function getCountryCode(countryName) {
const countryCodes = {
"Afghanistan": "AF",
"Albania": "AL",
"Aland Islands": "AX"
};
return countryCodes[countryName] || "Country code not found";
}
/**
* Defines the function definition that is executed after record is submitted.
* @param {Object} scriptContext
* @param {Record} scriptContext.newRecord - New record
* @param {Record} scriptContext.oldRecord - Old record
* @param {string} scriptContext.type - Trigger type; use values from the context.UserEventType enum
* @since 2015.2
*/
const afterSubmit = (scriptContext) => {
try {
log.debug('Script Executed', '');
let customerRecord = scriptContext.newRecord;
let customerInternalId = scriptContext.newRecord.id;
let recordType = scriptContext.newRecord.type;
let contextType = scriptContext.type;
let executionContext = runtime.executionContext;
let subsidiary = customerRecord.getValue('subsidiary')
let entityStatus = customerRecord.getValue({ fieldId: 'entitystatus' });
const CLOSED_WON_ID = '13';
const lookupCurrencies = {
1: "AED",
3: "Canadian Dollar",
4: "Euro",
5: "USD",
6: "OMR",
7: "BHD",
8: "LBP",
9: "CHF",
10: "GBP",
11: "QAR",
12: "ZAR",
13: "SAR",
14: "KWD",
15: "SEK"
};
if (recordType === 'prospect') {
if (contextType !== 'edit') {
log.debug("Skipping: Not an edit operation for Prospect");
return;
}
if (entityStatus !== CLOSED_WON_ID) {
log.debug("Skipping: Prospect is not changed to Closed - Won");
return;
}
}
if (contextType == 'create' || contextType == 'xedit' || contextType == 'edit' || executionContext == runtime.ContextType.CSV_IMPORT) {
log.debug("recordType", recordType)
let syncStatus = customerRecord.getValue({ fieldId: 'custentity_jj_firebase_sync_status' });
let customerData = model.getCustomerData({
filterConfig: {
customerInternalId: customerInternalId
},
searchConfig: {},
contextType
});
if (contextType === 'edit') {
let oldRecord = scriptContext.oldRecord;
let oldEntityStatus = oldRecord.getValue({ fieldId: 'entitystatus' });
if (recordType === 'prospect' && oldEntityStatus === entityStatus) {
log.debug("Skipping: Entity Status was not changed");
return;
}
let oldRecordObj = {}
oldRecordObj.internalId = oldRecord.getValue({ fieldId: 'id' })
oldRecordObj.dateCreated = oldRecord.getText({ fieldId: 'datecreated' }).split(':').slice(0, 2).join(':') + ' ' + oldRecord.getText({ fieldId: 'datecreated' }).split(' ')[2];
// oldRecordObj.dateModified = oldRecord.getText({ fieldId: 'lastmodifieddate' }).split(':').slice(0, 2).join(':') + ' ' + oldRecord.getText({ fieldId: 'lastmodifieddate' }).split(' ')[2];
oldRecordObj.email = oldRecord.getValue({ fieldId: 'email' });
oldRecordObj.salesRep = oldRecord.getText({ fieldId: 'salesrep' });
oldRecordObj.isPerson = oldRecord.getValue({ fieldId: 'isperson' }) == 'F' ? false : true
oldRecordObj.companyName = oldRecord.getText({ fieldId: 'companyname' });
oldRecordObj.subsidiary = oldRecord.getText({ fieldId: 'subsidiary' });
oldRecordObj.phone = oldRecord.getText({ fieldId: 'phone' });
oldRecordObj.status = oldRecord.getValue({ fieldId: 'isinactive' }) ? "INACTIVE" : "ACTIVE"
oldRecordObj.syncStatus = oldRecord.getValue({ fieldId: 'custentity_jj_firebase_sync_status' });
oldRecordObj.city = oldRecord.getValue({ fieldId: 'custentity_city' });
oldRecordObj.state = oldRecord.getText({ fieldId: 'custentity33' });
oldRecordObj.country = oldRecord.getText({ fieldId: 'custentity_country' });
oldRecordObj.code = oldRecord.getValue({ fieldId: 'entityid' }).split(' ')[0];
oldRecordObj.vat = oldRecord.getText({ fieldId: 'vatregnumber' });
let addressArray = []
let addressLineCount = oldRecord.getLineCount('addressbook')
log.debug('addressLineCount', addressLineCount)
for (let i = 0; i < addressLineCount; i++) {
log.debug(oldRecord.getSublistValue({
sublistId: 'addressbook',
fieldId: 'defaultbilling',
line: i
}))
if (oldRecord.getSublistValue({
sublistId: 'addressbook',
fieldId: 'defaultbilling',
line: i
})) {
addressArray.push({
addressInternalId: oldRecord.getSublistValue({
sublistId: 'addressbook',
fieldId: 'id',
line: i
}).toString(),
address1: oldRecord.getSublistValue({
sublistId: 'addressbook',
fieldId: 'addr1_initialvalue',
line: i
}),
address2: oldRecord.getSublistValue({
sublistId: 'addressbook',
fieldId: 'addr2_initialvalue',
line: i
}),
addressee: oldRecord.getSublistValue({
sublistId: 'addressbook',
fieldId: 'addressee_initialvalue',
line: i
}),
addressPhone: oldRecord.getSublistValue({
sublistId: 'addressbook',
fieldId: 'phone_initialvalue',
line: i
}),
attention: oldRecord.getSublistValue({
sublistId: 'addressbook',
fieldId: 'attention_initialvalue',
line: i
}),
city: oldRecord.getSublistValue({
sublistId: 'addressbook',
fieldId: 'city_initialvalue',
line: i
}),
state: oldRecord.getSublistValue({
sublistId: 'addressbook',
fieldId: 'state_initialvalue',
line: i
}),
zipCode: oldRecord.getSublistValue({
sublistId: 'addressbook',
fieldId: 'zip_initialvalue',
line: i
}),
country: oldRecord.getSublistText({
sublistId: 'addressbook',
fieldId: 'addressbookaddress_text',
line: i
}).trim().split(/s+/).pop(),
})
}
}
let currencyArray = []
let currencyLineCount = oldRecord.getLineCount('currency')
log.debug('currencyLineCount', currencyLineCount)
for (let i = currencyLineCount-1; i > -1; i--) {
let curId = oldRecord.getSublistValue({
sublistId: 'currency',
fieldId: 'currency',
line: i
});
currencyArray.push({
currency: lookupCurrencies[curId] || "Unknown Currency"
});
}
oldRecordObj.currency = currencyArray
oldRecordObj.addresses = addressArray
let subsidiaryArray = []
let subsidiarylineCount = oldRecord.getLineCount("submachine");
log.debug('lineCount', subsidiarylineCount)
for (let i = 0; i < subsidiarylineCount; i++) {
let subsidiary = oldRecord.getSublistText({
sublistId: 'submachine',
fieldId: 'subsidiary',
line: i
})
log.debug('subsidiary', subsidiary)
subsidiaryArray.push(subsidiary)
}
oldRecordObj.subsidiaries = subsidiaryArray.join(",");
log.debug('C oldRecordObj', oldRecordObj)
log.debug('C customerData', customerData)
// // Compare fields of interest between old and new records
let compareResult = compareObjects(oldRecordObj, customerData)
log.debug('compareResult', compareResult)
// if (compareResult && syncStatus === true) {
// log.debug('No changes detected. Skipping sync.');
// return; // Exit the script if no changes are detected
// }
}
let addressArray = []
let addressLineCount = customerRecord.getLineCount({
sublistId: 'addressbook'
})
let addressId;
for (let i = 0; i < addressLineCount; i++) {
addressId = customerRecord.getSublistValue({
sublistId: 'addressbook',
fieldId: 'addressid',
line: i
})
addressArray.push(addressId)
}
log.debug('addressArray', addressArray)
log.debug("customerData", customerData);
let length = customerData.addresses.length;
log.debug('length', length);
if (contextType === 'edit' && syncStatus != true) {
log.debug('if1')
sendCustomerData(customerData, recordType, customerInternalId);
} else if (contextType == 'create' && customerData) {
log.debug('if2')
sendCustomerData(customerData, recordType, customerInternalId);
} else if (contextType === 'edit' && syncStatus === true) {
log.debug('if3')
deleteCustomerData(recordType, customerInternalId, addressArray, customerData)
} else {
log.debug('', 'Record is already synced or no data found.');
}
}
} catch (error) {
log.error("Error @afterSubmit", error)
}
}
return { beforeLoad, afterSubmit }
});