Name
sn_nb_action.NextBestActionServiceImpl
Description
This is the main class of Recommended action, This is our interface to the rest of the layers/applications.
Script
var NextBestActionServiceImpl = Class.create();
NextBestActionServiceImpl.prototype = {
initialize: function() {
this._nextBestActionDAO = sn_nb_action.NextBestActionUtil.getNextBestActionDAO();
this.ruleService = sn_nb_action.NextBestActionUtil.getRuleService();
this.actionService = sn_nb_action.NextBestActionUtil.getActionService();
this._consolidateActions = sn_nb_action.NextBestActionUtil.getConsolidateActions();
this._log = new global.GSLog(sn_nb_action.Constants.PROP_LOG_LEVEL, this.type);
},
/*
* Returns a JSON Object representing the existing context of the current record
*/
getContext: function(currentRecord) {
var contextRecord = this._nextBestActionDAO.getContext(currentRecord);
if (contextRecord) {
var context = {};
context.number = contextRecord.getValue(sn_nb_action.Constants.COL_NUMBER);
context.state = contextRecord.getValue(sn_nb_action.Constants.COL_STATE);
return context;
}
return false;
},
_findOrCreateContext: function(currentRecord) {
var response = {
contextRecord: null,
isNewContext: false
};
var contextRecord = this._nextBestActionDAO.getContext(currentRecord);
if (!contextRecord ||
contextRecord.getValue(sn_nb_action.Constants.COL_STATE) == sn_nb_action.Constants.STATE_CANCELLED ||
contextRecord.getValue(sn_nb_action.Constants.COL_STATE) == sn_nb_action.Constants.STATE_COMPLETED) {
contextRecord = this._nextBestActionDAO.createContext(currentRecord);
response.isNewContext = true;
/**
* This checks if the customer already have RA plugin installed and RAContext records exists before upgrade.
* If yes, copies the recommendations from old RAContext to new RAContext.
**/
if (gs.nil(gs.getProperty('sn_nb_action.new_installation'))) {
var oldContextRecord = this._nextBestActionDAO.getOldContext(currentRecord);
if (oldContextRecord) {
this._nextBestActionDAO.copyOldActionsToNewContext(oldContextRecord, contextRecord);
response.isNewContext = false;
}
}
}
if (!contextRecord)
return false;
response.contextRecord = contextRecord;
return response;
},
getFieldsToMonitor: function(currentRecordTable) {
var ruleContext = this._nextBestActionDAO.getRuleContext(currentRecordTable);
var fieldsToMonitor = [];
if (ruleContext) {
var gr = this._nextBestActionDAO.getRulesFromContext(ruleContext);
var seen = {};
while (gr.next()) {
var ruleRoles = gr.getValue(sn_nb_action.Constants.COL_ROLES);
if (gs.nil(ruleRoles) || gs.hasRole(ruleRoles)) {
var fieldsEffectingThisRule = gr.getValue(sn_nb_action.Constants.COL_FIELD_AFF_RULE);
if (fieldsEffectingThisRule) {
var fieldNames = fieldsEffectingThisRule.split(",");
for (var index in fieldNames) {
if (!seen[fieldNames[index]]) {
seen[fieldNames[index]] = true;
fieldsToMonitor.push(fieldNames[index]);
}
}
}
}
}
}
return fieldsToMonitor;
},
/**
* This will generate/refresh Recommended actions in sync fashion based on rules for the current record
* @param {String} currentRecordTable - Current record table name.
* @param {String} currentRecordSysId - Current record sys id.
* @param {String} recommendationType - Recommendations needed for field level or side panel.
* @param {Boolean} forceRefresh - If true, forcefully generate recommendations
* @param {Object} fields - key value pair of unsaved changes for record fields
* @param {Boolean} groupRecommendations - If true, group recommendations based on the configuration per context
*
**/
getRecommendations: function(currentRecordTable, currentRecordSysId, recommendationType, forceRefresh, fields, groupRecommendations) {
var ruleContext = this._nextBestActionDAO.getRuleContext(currentRecordTable);
if (!ruleContext)
return false;
if (groupRecommendations) {
if (ruleContext && sn_nb_action.NextBestActionUtil.isValidRecord(ruleContext)){
this.groupingService = sn_nb_action.NextBestActionUtil.getGroupingService(ruleContext);
this.groupingService.initialize(ruleContext);
}
}
var fieldsToMonitor = this.getFieldsToMonitor(currentRecordTable);
if (!sn_nb_action.NextBestActionUtil.isValidString(recommendationType)) {
this._log.error("Invalid parameter - recommendationType");
return false;
}
var isNewRecord = currentRecordSysId == '-1';
var currentRecord = new GlideRecord(currentRecordTable);
if(!currentRecord.canRead()){
return false;
}
if (isNewRecord) {
currentRecord.newRecord();
} else {
if (!currentRecord.get(currentRecordSysId)) {
this._log.error("Invalid parameter - currentRecord");
return false;
}
}
var changedFields = fields ? Object.keys(fields) : [];
var hasUnsavedChanges = isNewRecord || changedFields.length > 0;
for (var i = 0; i < changedFields.length; i++)
currentRecord.setValue(changedFields[i], fields[changedFields[i]]);
fields = isNewRecord ? [] : fields;
var actionTypes = this._nextBestActionDAO.loadActionTypesByTable();
if (!actionTypes) {
this._log.error("There are no action types defined.");
return false;
}
var contextDetails = this._findOrCreateContext(currentRecord);
if (!contextDetails) {
this._log.error("Could not find or create context for " + currentRecord.getUniqueValue());
return false;
}
var contextRecord = contextDetails.contextRecord;
var isNewContext = contextDetails.isNewContext;
var existingActions = {};
var actionsBeforeRegeneration = {};
var needsRegeneration = forceRefresh;
if (!forceRefresh && contextRecord) {
needsRegeneration = this._checkRecommendationStale(contextRecord, currentRecord, recommendationType);
}
var response = {};
response.number = contextRecord.getValue(sn_nb_action.Constants.COL_NUMBER);
if (needsRegeneration || hasUnsavedChanges) {
if (needsRegeneration && !hasUnsavedChanges) {
var actionsRecordsBeforeRegeneration = this._nextBestActionDAO.getAllActionDetails(contextRecord, false, recommendationType);
if (actionsRecordsBeforeRegeneration)
actionsBeforeRegeneration = this._getExistingActionsFromContext(actionsRecordsBeforeRegeneration);
response.deleteCount = this._nextBestActionDAO.deleteRAByContext(contextRecord.getUniqueValue(), recommendationType);
}
var recommendationResult = this.ruleService.getBestActions(currentRecord, actionTypes, recommendationType, fields);
var currentActions = recommendationResult.currentActions;
var currentRules = recommendationResult.currentRules;
var actionDetailsList = [];
if (isNewContext && currentActions) {
actionDetailsList.newActionDetails = sn_nb_action.NextBestActionUtil.getObjectValues(currentActions);
} else if (!(isNewContext && currentActions)) {
var existingActionRecords = this._nextBestActionDAO.getAllActionDetails(contextRecord, false, recommendationType);
if (existingActionRecords)
existingActions = this._getExistingActionsFromContext(existingActionRecords);
//Passing currentActions as empty object to consolidateActions to deactivate existing
//actions if their corresponding rule is inactive in this run.
if (!currentActions)
currentActions = {};
actionDetailsList = this._consolidateActions.consolidateActions(existingActions, currentActions, currentRules, hasUnsavedChanges);
}
if (needsRegeneration && !hasUnsavedChanges) {
this._persistRecommendations(contextRecord, actionDetailsList, response, recommendationType);
if (response.newActions && response.deleteCount && response.newActions > response.deleteCount) {
response.newRARecommended = true;
} else if (this._consolidateActions.checkNewRARecommended(actionsBeforeRegeneration, currentActions)) {
response.newRARecommended = true;
}
}
}
var result;
if (hasUnsavedChanges)
result = this.actionService.getActionsForUnsavedChanges(contextRecord, currentRecord, recommendationType, currentActions);
else
result = this.actionService.getActions(contextRecord, currentRecord, recommendationType, currentActions);
response.actionDetails = result.actionDetails;
if (groupRecommendations && !gs.nil(this.groupingService) && !gs.nil(this.groupingService.groupRecommendations) &&
(typeof this.groupingService.groupRecommendations === "function")) {
response.actionDetails = this.groupingService.groupRecommendations(result.actionDetails);
}
response.contextId = result.contextId;
response.contextState = result.contextState;
response.fieldsToMonitor = fieldsToMonitor;
return response;
},
_persistRecommendations: function(contextRecord, actionDetailsList, response, recommendationType) {
var newActionCount, updatedActionDetailCount = 0;
var newActionOrderCount = 0;
var updateActionOrderCount = 0;
if (actionDetailsList) {
if (actionDetailsList.newActionDetails) {
newActionCount = this._nextBestActionDAO.addActionDetails(contextRecord, actionDetailsList.newActionDetails);
if (newActionCount > 0)
response.newActions = newActionCount;
}
if (actionDetailsList.newActionDetailOrders) {
newActionOrderCount = this._nextBestActionDAO.addActionDetailOrders(actionDetailsList.newActionDetailOrders);
if (newActionOrderCount > 0)
response.newActionOrderCount = newActionOrderCount;
}
if (actionDetailsList.updateActionDetailOrders) {
updateActionOrderCount = this._nextBestActionDAO.updateActionDetailOrders(actionDetailsList.updateActionDetailOrders);
if (updateActionOrderCount > 0)
response.updateActionOrderCount = updateActionOrderCount;
}
}
var currentState = contextRecord.getValue(sn_nb_action.Constants.COL_STATE);
// now allow updating of refreshed time even if no recommendations generated.
if (currentState == sn_nb_action.Constants.STATE_NEW && actionDetailsList) {
this._nextBestActionDAO.setContextState(contextRecord, sn_nb_action.Constants.STATE_READY, true, recommendationType);
} else if ((currentState == sn_nb_action.Constants.STATE_NEW || currentState == sn_nb_action.Constants.STATE_READY ||
currentState == sn_nb_action.Constants.STATE_IN_USE)) {
this._nextBestActionDAO.setContextState(contextRecord, currentState, true, recommendationType);
}
},
_getExistingActionsFromContext: function(existingActionRecords) {
var existingActions = {};
if (gs.nil(existingActionRecords))
return existingActions;
while (existingActionRecords.next()) {
if (!gs.nil(existingActionRecords[sn_nb_action.Constants.COL_RULE])) {
var actionDetailId = this._nextBestActionDAO.removeRuleFromActionDetail(existingActionRecords);
if (!actionDetailId)
this._log.error("Failed to remove rule for existing Action Detail Record " + existingActionRecords.getUniqueValue() + "for upgrade scenario");
}
var actionDetailOrders = this._nextBestActionDAO.getActionDetailOrders(existingActionRecords, true);
var actDetailsObj = sn_nb_action.ActionDetail.createFromRecord(existingActionRecords, actionDetailOrders);
if (Array.isArray(existingActions[actDetailsObj.getAction()]))
existingActions[actDetailsObj.getAction()].push(actDetailsObj);
else
existingActions[actDetailsObj.getAction()] = [actDetailsObj];
}
return existingActions;
},
/*
* Returns an array of JSON Object representing the actions generated for the current record
* Returns empty list of actionDetails if no action or no context record is available
*/
getActions: function(currentRecord) {
var contextRecord = this._nextBestActionDAO.getContext(currentRecord);
if (!contextRecord) {
var result = {};
result.actionDetails = [];
return result;
}
return this.actionService.getActions(contextRecord, currentRecord);
},
hasActiveOrderActions: function(contextRecord) {
var getActionOrderRecord = this._nextBestActionDAO.getActionDetailOrdersFromContext(contextRecord, true, 1);
if (getActionOrderRecord)
return true;
return false;
},
executeAction: function(currentRecord, actionDetailSysId, actionMetadata) {
var executionDetail = this.useAction(currentRecord, actionDetailSysId);
if (executionDetail.actionStatus == sn_nb_action.Constants.STATUS_ERROR) {
return executionDetail;
}
if (executionDetail) {
var contextRecord = this._nextBestActionDAO.getContext(currentRecord);
return this.actionService.executeAction(contextRecord, currentRecord, actionDetailSysId, actionMetadata);
}
},
/*
* This should be called whenever a recommended action is used.
*/
useAction: function(currentRecord, actionDetailSysId) {
if (!sn_nb_action.NextBestActionUtil.isValidRecord(currentRecord) || !sn_nb_action.NextBestActionUtil.isValidString(actionDetailSysId)) {
this._log.error("Invalid parameter");
return false;
}
var contextRecord = this._nextBestActionDAO.getContext(currentRecord);
if (!contextRecord) {
this._log.error("Could not find context for " + [currentRecord, actionDetailSysId]);
return false;
}
var actionAttributes = this.actionService.useAction(contextRecord, currentRecord, actionDetailSysId);
if (actionAttributes) {
if (actionAttributes.actionStatus == sn_nb_action.Constants.STATUS_ERROR) {
return actionAttributes;
}
if (contextRecord.getValue(sn_nb_action.Constants.COL_STATE) == sn_nb_action.Constants.STATE_READY)
this._nextBestActionDAO.setContextState(contextRecord, sn_nb_action.Constants.STATE_IN_USE);
return actionAttributes;
}
return false;
},
dismissAction: function(currentRecord, actionDetailSysId) {
if (!sn_nb_action.NextBestActionUtil.isValidRecord(currentRecord) || !sn_nb_action.NextBestActionUtil.isValidString(actionDetailSysId)) {
this._log.error("Invalid parameter");
return false;
}
var contextRecord = this._nextBestActionDAO.getContext(currentRecord);
if (!contextRecord) {
this._log.error("Could not find context for " + [currentRecord, actionDetailSysId]);
return false;
}
var dismissAction = this.actionService.dismissAction(contextRecord, currentRecord, actionDetailSysId);
return dismissAction;
},
getActionDetail: function(currentRecord, actionDetailSysId) {
if (!sn_nb_action.NextBestActionUtil.isValidRecord(currentRecord) || !sn_nb_action.NextBestActionUtil.isValidString(actionDetailSysId)) {
this._log.error("Invalid parameter");
return false;
}
var contextRecord = this._nextBestActionDAO.getContext(currentRecord);
if (!contextRecord) {
this._log.error("Could not find context for " + [currentRecord, actionDetailSysId]);
return false;
}
return this.actionService.getActionDetail(contextRecord, currentRecord, actionDetailSysId);
},
/*
* Tells If the Recommendation can be acted upon.
*/
isRecommendationAllowed: function(actionDetailSysId) {
var actionDetailRecord = this._nextBestActionDAO.getActionDetailBySysId(actionDetailSysId);
if (actionDetailRecord) {
return !this.actionService.hideRecommendation(actionDetailRecord);
}
return false;
},
/*
* This should be called whenever a recommended action is ingored.
*/
ignoreAction: function(currentRecord, actionSysId) {
if (!sn_nb_action.NextBestActionUtil.isValidRecord(currentRecord) ||
!sn_nb_action.NextBestActionUtil.isValidString(actionSysId)) {
this._log.error("Invalid parameter");
return false;
}
var contextRecord = this._nextBestActionDAO.getContext(currentRecord);
if (!contextRecord) {
this._log.error("Could not find context for " + [currentRecord, actionSysId]);
return false;
}
return this.actionService.ignoreAction(contextRecord, actionSysId);
},
/*
* This should be called whenever a recommended action in use is skipped.
*/
actionSkipped: function(currentRecord, actionSysId, actionDetailSysId, actionTableName, actionInitialAttributes) {
if (!gs.nil(actionDetailSysId)) {
return this.actionService.skipAction('', '', actionDetailSysId);
}
if (!sn_nb_action.NextBestActionUtil.isValidRecord(currentRecord) ||
!sn_nb_action.NextBestActionUtil.isValidString(actionSysId)) {
this._log.error("Invalid parameter");
return false;
}
if (actionTableName && actionTableName == sn_nb_action.Constants.TBL_DECISION_TREE) {
var contextRecords = this._nextBestActionDAO.getAllContexts(currentRecord);
while (contextRecords.next()) {
this.actionService.skipAction(contextRecords, actionSysId, '', actionInitialAttributes);
}
return true;
}
var contextRecord = this._nextBestActionDAO.getContext(currentRecord);
if (!contextRecord) {
this._log.error("Could not find context for " + [currentRecord, actionSysId]);
return false;
}
return this.actionService.skipAction(contextRecord, actionSysId);
},
/*
* This should be called whenever a recommended action is completed.
*/
actionCompleted: function(currentRecord, actionSysId, actionDetailSysId, actionDetails, actionTableName, actionInitialAttributes) {
if (!gs.nil(actionDetailSysId)) {
return this.actionService.completeAction('', '', actionDetailSysId, actionDetails);
}
if (!sn_nb_action.NextBestActionUtil.isValidRecord(currentRecord) ||
!sn_nb_action.NextBestActionUtil.isValidString(actionSysId)) {
this._log.error("Invalid parameter");
return false;
}
if (actionTableName && actionTableName == sn_nb_action.Constants.TBL_DECISION_TREE) {
var contextRecords = this._nextBestActionDAO.getAllContexts(currentRecord);
while (contextRecords.next()) {
this.actionService.completeAction(contextRecords, actionSysId, '', '', actionInitialAttributes);
}
return true;
}
var contextRecord = this._nextBestActionDAO.getContext(currentRecord);
if (!contextRecord) {
this._log.error("Could not find context for " + [currentRecord, actionSysId]);
return false;
}
return this.actionService.completeAction(contextRecord, actionSysId, '', actionDetails);
},
actionProgressed: function(actionDetailSysId) {
if (!gs.nil(actionDetailSysId)) {
return this.actionService.progressAction(actionDetailSysId);
}
},
_checkRecommendationStale: function(contextRecord, currentRecord, recommendationType) {
var lastRefreshTime;
if (recommendationType == sn_nb_action.Constants.VAL_FIELD_RECOMMENDATIONS)
lastRefreshTime = contextRecord.getValue(sn_nb_action.Constants.COL_REFRESHED_ON_FIELD_TYPE);
else
lastRefreshTime = contextRecord.getValue(sn_nb_action.Constants.COL_REFRESHED_ON);
if (!lastRefreshTime)
return true;
if (this._checkRefreshIntervalExpired(contextRecord, lastRefreshTime))
return true;
return this._checkContextUpdated(contextRecord, currentRecord, lastRefreshTime);
},
/**
* Checks if the refresh interval duration of context has passed and if regeneration is needed.
*/
_checkRefreshIntervalExpired: function(contextRecord, lastRefreshTime) {
var glideRefeshTime = new GlideDateTime(lastRefreshTime);
var ruleContextRecord = this._nextBestActionDAO.getRuleContext(contextRecord.getValue(sn_nb_action.Constants.COL_CONTEXT_TABLE));
if (!ruleContextRecord)
return false;
var refreshInterval = ruleContextRecord[sn_nb_action.Constants.COL_REFRESH_INTERVAL].dateNumericValue();
glideRefeshTime.add(refreshInterval);
var currentTime = new GlideDateTime();
if (glideRefeshTime.compareTo(currentTime) == 1)
return false;
return true;
},
/**
* Checks if Current context updated time is later than RAContext refresh time. If yes,
* regeneration is needed.
*/
_checkContextUpdated: function(raContextRecord, currentRecord, raContextRefreshTime) {
var glideRaContextRefreshTime = new GlideDateTime(raContextRefreshTime);
var glideContextUpdateTime = new GlideDateTime(currentRecord.getValue(sn_nb_action.Constants.COL_SYS_UPDATED_ON));
if (glideRaContextRefreshTime.compareTo(glideContextUpdateTime) == 1)
return false;
return true;
},
// Returns true if the rule is valid
isValidRule: function(ruleRecord) {
return this.ruleService.isValidRule(ruleRecord);
},
// Returns true if the action is allowed for table
isActionAllowed: function(tableName, actionRecord) {
if (!sn_nb_action.NextBestActionUtil.isValidString(tableName) ||
!sn_nb_action.NextBestActionUtil.isValidRecord(actionRecord)) {
this._log.error("Invalid parameter");
return false;
}
return this.actionService.isActionAllowed(tableName, actionRecord);
},
generateActions: function(currentRecord) {
// This is just a dummy method, an unused flow triggers this method
},
/*
* This should be called whenever a recommended action is errored.
*/
actionErrored: function(currentRecord, actionSysId, actionDetailSysId, actionTableName, actionInitialAttributes) {
if (!gs.nil(actionDetailSysId)) {
return this.actionService.errorOutAction(currentRecord, actionSysId, actionDetailSysId);
}
if (!sn_nb_action.NextBestActionUtil.isValidRecord(currentRecord) ||
!sn_nb_action.NextBestActionUtil.isValidString(actionSysId)) {
this._log.error("Invalid parameter");
return false;
}
if (actionTableName && actionTableName == sn_nb_action.Constants.TBL_DECISION_TREE) {
var contextRecords = this._nextBestActionDAO.getAllContexts(currentRecord);
while (contextRecords.next()) {
this.actionService.errorOutAction(contextRecords, actionSysId, '', actionInitialAttributes);
}
return true;
}
var contextRecord = this._nextBestActionDAO.getContext(currentRecord);
if (!contextRecord) {
this._log.error("Could not find context for " + [currentRecord, actionSysId]);
return false;
}
return this.actionService.errorOutAction(contextRecord, actionSysId);
},
type: 'NextBestActionServiceImpl'
};
Sys ID
526456bd3b3e1010c24e870044efc41c