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

Offical Documentation

Official Docs: