Name

global.OnCallRotationSNC

Description

ServiceNow On-Call Rotation implementation

Script

var OnCallRotationSNC = Class.create();

OnCallRotationSNC._getSpanByGdt = function(scheduleSysId, gdt) {
  if (!scheduleSysId)
  	return null; 

  gdt = gdt || new GlideDateTime();

  var schedule = new GlideSchedule(scheduleSysId);
  var scheduleTimeMap = schedule.fetchTimeMapWithExcludes(gdt, gdt, null, false);

  if (!scheduleTimeMap.hasNext())
  	return null;

  // Only interested in span for this gdt (start and end time the same so will only match one span)
  var span = scheduleTimeMap.next();
  return (span) ? span : null;
};

OnCallRotationSNC._getRotaTime = function(scheduleSysId, dateFormat, timeGdt) {
  var rotaTime = {};
  rotaTime.start = "";
  rotaTime.end = "";
  timeGdt = timeGdt || new GlideDateTime();

  var span = this.getSpanByGdt(scheduleSysId, timeGdt);
  if (!span)
  	return rotaTime;

  var startDateTime = span.getActualStart().getDisplayValue().split(" ");
  var endDateTime = span.getActualEnd().getDisplayValue().split(" ");

  // Get time of start date and end date
  var startTime = this._getHoursMins(startDateTime[1]);
  var endTime = this._getHoursMins(endDateTime[1]);

  var ampmStart = startDateTime[2] ? startDateTime[2].toLowerCase() : "";
  var ampmEnd = endDateTime[2] ? endDateTime[2].toLowerCase() : "";

  startTime = gs.getMessage("{0} {1}", [startTime, ampmStart]);
  startTime = (startTime + "").trim();
  endTime = gs.getMessage("{0} {1}", [endTime, ampmEnd]);
  endTime = (endTime + "").trim();

  rotaTime.start = startDateTime.join(" ");
  rotaTime.end = endDateTime.join(" ");
  rotaTime.startTime = startTime;
  rotaTime.endTime = endTime;
  return rotaTime;
};

OnCallRotationSNC.prototype = {
  TABLES: {
  	CMN_ROTA: 'cmn_rota',
  	CMN_ROTA_ESCALATION_SET: 'cmn_rota_escalation_set',
  	CMN_ROTA_ESCALATION_DEFINITION: 'cmn_rota_esc_step_def',
  	CMN_ROTA_ROSTER : 'cmn_rota_roster',
  	CMN_ROTA_MEMBER: 'cmn_rota_member',
  	SYS_USER: 'sys_user',
  	SYS_USER_GROUP: 'sys_user_group',
  	CMN_NOTIF_DEVICE: 'cmn_notif_device',
  	ON_CALL_GROUP_ESCALATION: 'on_call_group_escalation'
  },

  ESCALATION_TYPE: {
  	ROTATE_THROUGH_MEMBER: 'rotate_through_member',
  	ROTATE_THROUGH_ROSTER: 'rotate_through_roster',
  	CUSTOM: 'custom'
  },

  initialize: function (_gs) {
  	this._gs = _gs || gs;
  	this._log = new GSLog("com.snc.on_call_rotation.log.level", this.type);
  	this._onCallCommon = new OnCallCommon();
  	this.arrayUtils = new global.ArrayUtil();

  	if (this._log.atLevel(GSLog.DEBUG))
  		this.timer = new OCTimer(this.type);

  	// Data model used by who() function
  	this._groupSysId = "";
  	this._rotaSysIds = [];
  	this._rotaCustomEscalation = false;
  	this._rotaTimeZone = "";
  	this._firstDelay = 0;

  	// number of entries to return (-1 means all for active roster)
  	this._maxEntries = -1;

  	// _escalations stores the Escalation list for the current rota
  	this._escalationList = new OnCallEscalation();
  	this._currentEscalation = null;

  	// _escalatees stores the on-call members' Escalation plan in order of escalation. Similar to _escalationList but without repeating entries for the same on-call members
  	this._escalatees = this._initEscalationPlan();

  	// fEscalationLevel will keep track on the level on the Escalation Plan as its being populated
  	this._escalationLevel = 0;
  	this._simulateEscalationToAllRotas = false;

  	// Will be used when custom escalation step has RTM marked to true
  	this._delayTillPreviousStep = 0;
  },

  /**
   * Add members to all rosters of a particular rota and stagger the member orders by 1 for each roster
   *
   * rotaSysId [String]
   * memberUserSysIds [string array]
   */
  addRosterMembers: function(rotaSysId, memberUserSysIds) {
  	if (this._log.atLevel(GSLog.DEBUG))
  		this._log.debug("[addRosterMembers] rotaSysId: " + rotaSysId + " memberUserSysIds: " + memberUserSysIds);

  	var rosterGr = new GlideRecord("cmn_rota_roster");
  	rosterGr.addQuery("rota", rotaSysId + "");
  	rosterGr.orderBy("order");
  	rosterGr.query();
  	var rosterCount = 1;
  	while (rosterGr.next()) {
  		GlideSession.get().putProperty(OnCallRotationSNC.PROPERTY_SKIP_COMPUTE_SCHEDULES, "true");
  		var memberOrder = rosterCount;
  		var memberFrom = rosterGr.rotation_start_date + "";
  		var memberUserSysIdsLength = memberUserSysIds.length;

  		if (this._log.atLevel(GSLog.DEBUG))
  			this._log.debug("[addRosterMembers] memberOrder: " + memberOrder + " memberFrom: " + memberFrom + " memberUserSysIdsLength: " + memberUserSysIdsLength);

  		for (var i = 0; i < memberUserSysIdsLength; i++) {
  			var memberUserSysId = memberUserSysIds[i];
  			var onCallMember = new OnCallMember();
  			onCallMember.setRosterId(rosterGr.sys_id + "");
  			onCallMember.setFrom(memberFrom);
  			onCallMember.setMemberId(memberUserSysId);
  			onCallMember.setOrder(memberOrder++);
  			var onCallMemberSysId = onCallMember.create();

  			if (this._log.atLevel(GSLog.DEBUG))
  				this._log.debug("[addRosterMembers] onCallMemberSysId: " + onCallMemberSysId + " memberUserSysId: " + memberUserSysId);

  			// set the count back to 1 when the number of members is exceeded
  			if (memberOrder > memberUserSysIdsLength)
  				memberOrder = 1;
  		}
  		GlideSession.get().putProperty(OnCallRotationSNC.PROPERTY_SKIP_COMPUTE_SCHEDULES, "false");
  		this.computeRotationSchedules(rosterGr);
  		rosterCount++;
  	}
  },

  /**
   * Add the current escalation to the list of those notified so we know
   * who has been contacted and can react as appriopriate.
   */
  addCurrentToNotified: function() {
  	if (this._escalationList)
  		if (this._currentEscalation)
  			this._escalationList.addToNotified(this._currentEscalation);
  		else
  			this._log.error("addCurrentToNotified called without a current escalation");
  	else
  		this._log.error("addCurrentToNotified called without escalations defined");
  },

  /**
   * Cancel any escalations for the GlideRecord that were started with
   * 'startEscalations'.
   */
  cancelEscalations: function(documentGr) {
  	if (!documentGr)
  		return;

  	var sysId = documentGr.sys_id + "";
  	if (!sysId)
  		return;

  	var rotaEscalationGr = new GlideRecord("cmn_rota_escalation");
  	rotaEscalationGr.addQuery("instance", sysId);
  	rotaEscalationGr.query();
  	while (rotaEscalationGr.next()) {
  		var triggerSysId = rotaEscalationGr.trigger + "";
  		if (!triggerSysId)
  			continue;
  		var triggerGr = new GlideRecord("sys_trigger");
  		if (triggerGr.get(triggerSysId))
  			triggerGr.deleteRecord();
  		rotaEscalationGr.deleteRecord();
  	}
  },

  /**
   * Recompute Roster Schedules based on timezones
   * Called from BRs 
   */
  getAffectedRotas: function(scheduleId) {
  	var scheduleGr = new GlideRecord("cmn_schedule");

  	if (scheduleGr.get(scheduleId)) {
  		var scheduleType = scheduleGr.getValue("type");
  		var rotas = [];
  		if (scheduleType === "roster")
  			rotas = this._getRotas(scheduleId);
  		else {
  			var otherScheduleGr = new GlideRecord("cmn_other_schedule");
  			otherScheduleGr.addQuery("child_schedule", scheduleId);
  			otherScheduleGr.addNotNullQuery("schedule.type").addOrCondition("schedule.type", "=", "roster");
  			otherScheduleGr.query();
  			while (otherScheduleGr.next())
  				rotas = rotas.concat(this.getAffectedRotas(otherScheduleGr.getValue("schedule")));
  		}
  		return this.arrayUtils.unique(rotas);
  	}
  },

  _getRotas: function(scheduleId) {
       var rotaGr = new GlideRecord("cmn_rota");
       rotaGr.addQuery("schedule", scheduleId);
       rotaGr.query();
       var rotas = [];
       while (rotaGr.next()) {
           rotas.push(rotaGr.getUniqueValue());
       }
       return rotas;
   },
   computeRotaSchedules: function(rotaSysId) {
       var rosterGR = new GlideRecord("cmn_rota_roster");
       rosterGR.initialize();
       rosterGR.addActiveQuery();
       rosterGR.addQuery("rota", rotaSysId);
       rosterGR.query();
       while (rosterGR.next())
           this.computeRotationSchedules(rosterGR);
   },
  /**
   * Compute the rotation schedules for a roster based on the rotation
   * values specified for the roster.
   */
  computeRotationSchedules: function(rosterGr) {
  	this._getOnCallRosterByGr(rosterGr).computeRotationSchedules();
  },

  _getTaskGrFromEscalation: function (escalationGr) {
  	var taskTable = escalationGr.table;
  	var taskSysId = escalationGr.instance;
  	if (taskTable && taskSysId) {
  		var taskGr = new GlideRecord(taskTable);
  		if (taskGr.get(taskSysId))
  			return taskGr;
  	}
  },

  /**
   * Continue the escalation process for the group (call after a successful
   * call to whoIsNext from within the escalation business rule script)
   */
  continueEscalations: function(escalationGr) {
  	if (!this._groupSysId || !this._rotaSysIds || (this._rotaSysIds && this._rotaSysIds.length == 0)) {
  		// Invalid calling sequence, Ignore this request
  		// Call who or whoIsNext before this is called so that the notifications are properly set up for escalation
  		if (escalationGr)
  			escalationGr.deleteRecord();
  		this._log.error("[continueEscalations] Escalations called out of sequence");
  		return;
  	}

  	// Figure out when the first escalation needs to fire
  	var runAt = this._escalationList.runAt();
  	if (runAt < 0) {
  		// No more escalations - see if there are more rosters to run through
  		var taskGr = this._getTaskGrFromEscalation(escalationGr);
  		this._escalationList.clear();
  		this._currentEscalation = null;
  		this._checkForMoreRotas(taskGr);
  		if (this.isEmptyAnytime()) {
  			escalationGr.deleteRecord();
  			return;
  		}
  		runAt = this._escalationList.runAt();
  	}

  	escalationGr.setValue("escalation_document", this.getFullEscalationDocument());
  	var gr = new GlideRecord(escalationGr.table + "");
  	gr.get(escalationGr.instance + "");
  	var triggerSysId = this._scheduleEscalationTrigger(gr, escalationGr.sys_id + "", runAt);
  	if (triggerSysId)
  		escalationGr.setValue("trigger", triggerSysId);
  	escalationGr.update();
  },

  /**
   * Create rosters for any particular rota
   *
   * rotaSysId [string]
   * numRosters [number]
   * numReminders [number]
   * startGdt [GlideDateTime]
   * timeBtwReminders [GlideDuration]
   *
   */
  createRosters: function(rotaSysId, numRosters, numReminders, startGdt, timeBtwRemindersDur, rotationIntervalType, rotationIntervalCount, dowForRotate, rosterName, rosterOrder) {
  	var rosterSysIds = [];
  	var isAllDay = this._isAllDayRoster;
  	
  	rotationIntervalType = rotationIntervalType ? rotationIntervalType : OnCallRotationSNC.DEFAULT_ROTATION_INTERVAL_TYPE;
  	rotationIntervalCount = rotationIntervalCount ? rotationIntervalCount : OnCallRotationSNC.DEFAULT_ROTATION_INTERVAL_COUNT;

  	if (this._log.atLevel(GSLog.DEBUG))
  			this._log.debug("[createRosters] rotaSysId: " + rotaSysId + " numRosters: " + numRosters + " numReminders: " + numReminders + " startGdt: " + startGdt + " timeBtwRemindersDur: " + timeBtwRemindersDur + " isAllDay: " + isAllDay);

  	var groupDomain = "";
  	for (var i = 0; i < numRosters; i++) {
  		var rosterGr = new GlideRecord("cmn_rota_roster");
  		rosterGr.initialize();
  		rosterGr.setValue("rota", rotaSysId);
  		if (rosterOrder)
  			rosterGr.setValue("order", rosterOrder);
  		else
  			rosterGr.setValue("order", i + 1);
  		rosterGr.setValue("rotation_interval_type", rotationIntervalType);
  		rosterGr.setValue("rotation_interval_count", rotationIntervalCount);
  		rosterGr.setValue("rotation_all_day", isAllDay);
  		rosterGr.setValue("attempts", numReminders);
  		rosterGr.setValue("time_between_reminders", timeBtwRemindersDur.getValue() + "");
  		rosterGr.setDisplayValue("rotation_start_date", startGdt.getDate().getDisplayValue() + "");
  		if (rosterName)
  			rosterGr.setValue("name", rosterName);
  		else
  			rosterGr.setValue("name", this._getSequenceName(i));
  		if(rotationIntervalType == 'weekly' && !isNaN(dowForRotate))
  			rosterGr.setValue("dow_for_rotate", dowForRotate);
  		if (!isAllDay)
  			rosterGr.setValue("rotation_start_time", this._onCallCommon.gitToTime(this._rosterStartGit));

  		if (i === 0) {
  			var rotaGr = new GlideRecord("cmn_rota");
  			if (rotaGr.get(rotaSysId))
  				groupDomain = rotaGr.group.sys_domain + "";
  		}

  		if (groupDomain)
  			rosterGr.setValue("sys_domain", groupDomain);

  		var rosterSysId = rosterGr.insert();
  		rosterSysIds.push(rosterSysId);

  		if (this._log.atLevel(GSLog.DEBUG))
  			this._log.debug("[createRosters] rosterSysId: " + rosterSysId + " start: " + startGdt.getDate().getDisplayValue());
  	}
  	return rosterSysIds;
  },

  createSimpleRotaFromSchedule: function(rotaName, groupSysId, scheduleSysId, numRosters, numReminders, members, rosterStart, timeBtwReminders, rotationIntervalType, rotationIntervalCount, dowForRotate, sendReminders, reminderLeadTime, coverageLeadType, coverageInterval) {
  	
  	GlideSession.get().putProperty(OnCallRotationSNC.PROPERTY_IS_WIZARD, "true");
  	rotaName = rotaName || "";
  	groupSysId = groupSysId || "";
  	scheduleSysId = scheduleSysId || "";
  	numRosters = numRosters ? parseInt(numRosters) : 0;
  	numReminders = numReminders ? parseInt(numReminders) : 0;
  	members = (members + "").split(",");

  	if (this._log.atLevel(GSLog.DEBUG))
  		this._log.debug("[createSimpleRotaFromSchedule] rotaName: " + rotaName + " groupSysId: " + groupSysId + " scheduleSysId: " + scheduleSysId +
  		" numRosters: " + numRosters + " numReminders: " + numReminders + " members: " + members + " rosterStart: " + rosterStart +
  		" timeBtwReminders: " + timeBtwReminders);

  	var rosterStartGdt = new GlideDateTime();
  	if (rosterStart)
  		rosterStartGdt.setValue(rosterStart);
  	var timeBtwRemindersDur = new GlideDuration(parseInt(timeBtwReminders) * 60000);
  	var rotaSysId = this.createNewRota(rotaName, groupSysId, scheduleSysId, rosterStartGdt, null, null, null, null, sendReminders, reminderLeadTime, coverageLeadType, coverageInterval);
  	if (rotaSysId) {
  		this.createRosters(rotaSysId, parseInt(numRosters), parseInt(numReminders), rosterStartGdt, timeBtwRemindersDur, rotationIntervalType, parseInt(rotationIntervalCount), dowForRotate);
  		this.addRosterMembers(rotaSysId, members);
  	}
  	GlideSession.get().putProperty(OnCallRotationSNC.PROPERTY_IS_WIZARD, "false");
  	return rotaSysId;
  },

  createSimpleRotaNewSchedule: function(rotaName, groupSysId, timezone, startGdt, endGdt, repeats, isAllDay, numRosters, numReminders, rosterStart, members, timeBtwReminders, scheduleName, rotationIntervalType, rotationIntervalCount, dowForRotate, sendReminders, reminderLeadTime, coverageLeadType, coverageInterval) {
  	GlideSession.get().putProperty(OnCallRotationSNC.PROPERTY_IS_WIZARD, "true");
  	rotaName = rotaName || "";
  	groupSysId = groupSysId || "";
  	timezone = timezone || "";
  	startGdt = new GlideDateTime(startGdt);
  	endGdt = new GlideDateTime(endGdt);
  	repeats = repeats || "";
  	isAllDay = isAllDay + "" === "true" ? true : false;
  	numRosters = numRosters ? parseInt(numRosters) : 0;
  	numReminders = numReminders ? parseInt(numReminders) : 0;
  	members = (members + "").split(",");
  	scheduleName = scheduleName || "";
  	members = (members + "").split(",");

  	if (isAllDay) {
  		endGdt.setDisplayValueInternal(startGdt.getDisplayValueInternal());
  		endGdt.addSeconds(86399); //(23 * 60 * 60) + (59 * 60) + 59
  	}

  	var rosterStartGdt = new GlideDateTime();
  	if (rosterStart)
  		rosterStartGdt.setValue(rosterStart);
  	var timeBtwRemindersDur = new GlideDuration(parseInt(timeBtwReminders) * 60000);

  	if (this._log.atLevel(GSLog.DEBUG))
  		this._log.debug("[createSimpleRotaNewSchedule] rotaName: " + rotaName + " groupSysId: " + groupSysId + " timezone: " + timezone +
  		" startGdt: " + startGdt + " endGdt: " + endGdt + " repeats: " + repeats + " isAllDay: " + isAllDay + " numRosters: " + numRosters +
  		" numReminders: " + numReminders + " members: " + members + " rosterStart: " + rosterStart + " scheduleName: " + scheduleName +
  		" timeBtwReminders: " + timeBtwReminders);

  	var rotaSysId = this.createNewRota(rotaName, groupSysId, timezone, startGdt, endGdt, repeats, isAllDay, scheduleName, sendReminders, reminderLeadTime, coverageLeadType, coverageInterval);
  	if (rotaSysId) {
  		this.createRosters(rotaSysId, parseInt(numRosters), parseInt(numReminders), rosterStartGdt, timeBtwRemindersDur, rotationIntervalType, parseInt(rotationIntervalCount), dowForRotate);
  		this.addRosterMembers(rotaSysId, members);
  	}
  	GlideSession.get().putProperty(OnCallRotationSNC.PROPERTY_IS_WIZARD, "false");
  	return rotaSysId;
  },

  /**
   * Create a new rota and a new schedule to go with it with the given params
   *
   * rotaName [string]
   * groupSysId [string]
   * scheduleSysId [string] schedule sys_id OR timzone if scheduleName defined (from overloaded java method)
   * startGdt [GlideDateTime]
   * endGdt [GlideDateTime]
   * repeats
   * isAllDay [boolean]
   * scheduleName [string]
   */
  createNewRota: function(rotaName, groupSysId, scheduleSysId, startGdt, endGdt, repeats, isAllDay, scheduleName, sendReminders, reminderLeadTime, coverageLeadType, coverageInterval, days, isDraft) {
  	if (this._log.atLevel(GSLog.DEBUG))
  		this._log.debug("[createNewRota] rotaName: " + rotaName + " groupSysId: " + groupSysId + " scheduleSysId: " + scheduleSysId + " startGdt: " + startGdt + " endGdt: " + endGdt + " repeats: " + repeats + " isAllDay: " + isAllDay + " scheduleName: " + scheduleName);

  	// Overlapping check for existing rota
  	if (!scheduleName && scheduleSysId) {
  		var overlappingRota = this._getExistingSpanOverlappingRota(groupSysId, scheduleSysId, startGdt);
  		if (JSUtil.notNil(overlappingRota)) {
  			gs.addErrorMessage(gs.getMessage("Cannot create the shift, The selected schedule overlaps with the '{0}' rotation schedule.", [overlappingRota]));
  			return "";
  		}
  	}

  	var rotaGr = this._createNewRota(rotaName, groupSysId, isDraft);
  	if (!rotaGr)
  		return "";

  	rotaGr.setValue('send_reminders', sendReminders);
  	rotaGr.setValue('reminder_lead_time', reminderLeadTime);
  	rotaGr.setValue('coverage_lead_type', coverageLeadType);
  	rotaGr.setValue('coverage_interval', coverageInterval);

  	if (scheduleName) {
  		var timezone = scheduleSysId;
  		scheduleSysId = this._createNewRosterSchedule(timezone, scheduleName);
  		rotaGr.setValue("schedule", scheduleSysId);
  		rotaGr.update();
  		var newScheduleSpanSysId = this._createScheduleSpanWithDefaults(startGdt || new GlideDateTime(), endGdt || new GlideDateTime(), repeats + "", isAllDay	+ "" === "true" ? true : false, days);
  		// Overlapping check for new rota, schedule span creation fails, so simply verifying it.
  		if (!newScheduleSpanSysId) {
  			rotaGr.get(rotaGr.sys_id + "");
  			rotaGr.deleteRecord();
  			gs.addErrorMessage(gs.getMessage("Cannot create the shift"));
  			return "";
  		} else {
  			//Overlapping check for new draft shift spans
  			var overlappingDraftRota = this._getExistingSpanOverlappingRota(groupSysId, scheduleSysId, startGdt);
  			if (JSUtil.notNil(overlappingDraftRota)) {
  				rotaGr.get(rotaGr.sys_id + "");
  				rotaGr.deleteRecord();
  				gs.addErrorMessage(gs.getMessage("Cannot create the shift, The selected schedule overlaps with the '{0}' rotation schedule.", [overlappingDraftRota]));
  				return "";
  			}
  		}
  	} else if (scheduleSysId) {
  		rotaGr.setValue("based_on", scheduleSysId);
  		rotaGr.setValue("schedule", this._createRosterScheduleFromExisting(scheduleSysId, startGdt || new GlideDateTime()));
  		rotaGr.update();
  	}

  	return rotaGr.sys_id + "";
  },

  _getExistingSpanOverlappingRota: function(groupSysId, scheduleSysId, startGdt) {
  	if (!this._onCallCommon.isOverlapAllowed(groupSysId)) {
  		var newSpan = new GlideRecord("cmn_schedule_span");
  		newSpan.addQuery("schedule", scheduleSysId);
  		newSpan.addQuery("type", "on_call");
  		newSpan.addQuery("show_as", "on_call");
  		newSpan.query();
  		while (newSpan.next()) {
  			// if we get a rota name back that means this span overlaps an existing rota schedule

  			var overlappingRota = new ValidateSchedule().scheduleOverlaps(newSpan, groupSysId, startGdt);
  			if (JSUtil.notNil(overlappingRota))
  				return overlappingRota;
  		}
  	}
  },

  updateGroupPreferences: function(groupSysId, rotaManagers, ptoApprovalConfig, allowRotaOverlap, escalationRuleRotaOverlap) {
  	var groupPrefGr = this.getGroupPreferenceByGroup(groupSysId);
  	if (groupPrefGr) {
  		this.populateGroupPreferenceRecord(groupPrefGr, groupSysId, rotaManagers, ptoApprovalConfig, allowRotaOverlap, escalationRuleRotaOverlap);
  		groupPrefGr.update();
  	} else {
  		groupPrefGr = new GlideRecord('on_call_group_preference');
  		groupPrefGr.initialize();
  		this.populateGroupPreferenceRecord(groupPrefGr, groupSysId, rotaManagers, ptoApprovalConfig, allowRotaOverlap, escalationRuleRotaOverlap);
  		groupPrefGr.insert();
  	}
  },

  populateGroupPreferenceRecord: function(groupPrefGr, groupSysId, rotaManagers, ptoApprovalConfig, allowRotaOverlap, escalationRuleRotaOverlap) {
  	groupPrefGr.setValue('group', groupSysId);
  	groupPrefGr.setValue('rota_managers', rotaManagers + '');
  	groupPrefGr.setValue('pto_approval_config', ptoApprovalConfig);
  	groupPrefGr.setValue('allow_rota_overlap', allowRotaOverlap);
  	groupPrefGr.setValue('escalation_rule_rota_overlap', escalationRuleRotaOverlap);
  },

  getGroupPreferenceByGroup: function(groupSysId) {
  	var groupPrefGr = new GlideRecord('on_call_group_preference');
  	groupPrefGr.addQuery('group', groupSysId);
  	groupPrefGr.query();
  	if (groupPrefGr.next())
  		return groupPrefGr;
  },

  /**
   *  Get an array of active Rotas (cmn_rota) for a given Group ID (sys_user_group)
   *
   * groupSysIds [string or array] sys_user_group sys_ids
   * return [Array]: An array of rota objects which each contain three keys, 'sys_id', 'name', and 'group_sys_id' of the cmn_rota record
   *
   */
  getRotasByGroup: function(groupSysIds) {
  	var rotas = [];
  	if (!groupSysIds)
  		return rotas;
  	var gr = new GlideRecord("cmn_rota");
  	gr.addQuery("group", "IN", groupSysIds);
  	gr.addActiveQuery();
  	gr.query();
  	while (gr.next())
  		rotas.push({
  			name: gr.name + "",
  			sys_id: gr.sys_id + "",
  			group_sys_id: gr.group + ""
  		});
  	return rotas;
  },

  gotoTop: function() {
  	this._escalationList.gotoTop();
  },

  getEscalationPlan: function(groupSysId, gdt, rotaSysIds, taskGr) {
  	groupSysId = groupSysId || this._escalationList.getGroupID();
  	gdt = gdt || new GlideDateTime();
  	if (!groupSysId) {
  		this._log.error("Must have exactly two arguments (String groupID, GlideDateTime time) or have who(String groupSysId) called with valid groupSysId");
  		return null;
  	}
  	this.whoAt(groupSysId, gdt, rotaSysIds, taskGr);
  	return this._escalatees;
  },

  getEscalationPlanByEscalationSet: function (escalationSetSysId, groupSysId, rotaSysIds, gdt) {
  	gdt = gdt || new GlideDateTime();

  	if (!groupSysId || !rotaSysIds) {
  		var escalationSetGr = this._getEscalationSetById(escalationSetSysId);
  		if (escalationSetGr) {
  			groupSysId = escalationSetGr.cmn_rota.group + '';
  			rotaSysIds = escalationSetGr.cmn_rota + '';
  		}
  		else
  			return [];
  	}
  	this.forcedEscalationSetSysId = escalationSetSysId;
  	this.whoAt(groupSysId, gdt, rotaSysIds);
  	this.forcedEscalationSetSysId = null;
  	return this._escalatees;
  },

  /**
   * Get the escalation document with all of the entries in it
   *
   * return [string] XML document containing all escalations
   */
  getFullEscalationDocument: function() {
  	return GlideXMLUtil.toString(this._escalationList.toXml(true));
  },

  getMaxEntries: function() {
  	return this._maxEntries;
  },

  /**
   * Provide list of escalatees that are On-call for a given time. If no time specified
   * then current time is used. If no groupSysIds are provided return empty list
   *
   * groupIds [String]: comma seperated list of sys_user_group sys_ids to filter by
   * rotaIds [String]: comma seperated list of cmn_rota sys_ids to filter records by
   * rosterIds [String]: comma seperated list of cmn_rota_roster sys_ids to filter records by
   * gdt [GlideDateTime]: date in UTC timezone
   *
   */
  getEscalatees: function(groupIds, rotaIds, rosterIds, gdt, overrideCustomEscalation, taskGr) {
  	var members = [];
  	var escalatees = this._getEscalatees(groupIds, rotaIds, rosterIds, gdt, false, overrideCustomEscalation, taskGr);
  	escalatees.forEach(function(escalatee) {
  		members.push({
  			memberId: escalatee.getMemberId(),
  			memberIds: escalatee.getMemberIds(),
  			userId: escalatee.getUserId(),
  			userIds: escalatee.getUserIds(),
  			roster: escalatee.getRosterId(),
  			rota: escalatee.getRotaId(),
  			group: escalatee.getGroupId(),
  			escalationGroups: escalatee.getEscalationGroups(),
  			deviceId: escalatee.getDeviceId(),
  			deviceIds: escalatee.getDeviceIds(),
  			isDevice: escalatee.getIsDevice(),
  			order: escalatee.getOrder(),
  			isOverride: escalatee.getIsOverride(),
  			rotationScheduleId: escalatee.getRotaScheduleId(),
  			memberScheduleId: escalatee.getMemberScheduleId()
  		});
  	});
  	return members;
  },

  /**
   * Get the escalatee (either a user or device record depending on the notification rules) at a specific time and
   * position.
   *
   * groupSysId [String]
   * gdt [GlideDateTime]
   * position [number]
   * return [GlideRecord] escalateeGr
   */
  getEscalateeAt: function(groupSysId, gdt, position) {

  	// build the escalation document for the specified group and time and extract the escalatee XML nodes
  	this.whoAt(groupSysId, gdt);

  	if (position < 1 || position > this._escalatees.length)
  		return null;

  	var escalatee = this._escalatees[position - 1];
  	var escalateeSysId = escalatee.getUserId();
  	var tableName = "sys_user";

  	if (!escalateeSysId) {
  		escalateeSysId = escalatee.getDeviceId();
  		tableName = "cmn_notif_device";
  	}

  	var escalateeGr = new GlideRecord(tableName);
  	if (escalateeSysId && escalateeGr.get(escalateeSysId))
  		return escalateeGr;
  	return null;
  },

  /**
   * Get the escalatees (either a user or device record depending on the notification rules) at a specific time and position.
   *
   * groupSysId [String]
   * gdt [GlideDateTime]
   * position [number]
   * return [GlideRecord] escalateeGr
   */
  getEscalateesAt: function(groupSysId, gdt, position, taskGr) {

  	// build the escalation document for the specified group and time and extract the escalatee XML nodes
  	this.whoAt(groupSysId, gdt, '', taskGr);

  	if (position < 1 || position > this._escalatees.length)
  		return null;

  	var escalateesGr = [];
  	var escalateeGr;
  	var escalatee = this._escalatees[position - 1];
  	var users = [];
  	var groups = [];
  	var devices = [];
  	if (escalatee.getUserId())
  		users.push(escalatee.getUserId());
  	if (escalatee.getUserIds() && escalatee.getUserIds().length > 0)
  		users = users.concat(escalatee.getUserIds());
  	var i, j;
  	for (i = 0; i < users.length; i++) {
  		escalateeGr = new GlideRecord(this.TABLES.SYS_USER);
  		if (escalateeGr.get(users[i]))
  			escalateesGr.push(escalateeGr);
  	}

  	if (escalatee.getEscalationGroups() && escalatee.getEscalationGroups().length > 0)
  		groups = groups.concat(escalatee.getEscalationGroups());

  	for (i = 0; i < groups.length; i++) {
  		escalateeGr = new GlideRecord(this.TABLES.SYS_USER_GROUP);
  		if (escalateeGr.get(groups[i]))
  			escalateesGr.push(escalateeGr);
  	}

  	if (escalatee.getDeviceId())
  		devices.push(escalatee.getDeviceId());
  	if (escalatee.getDeviceIds() && escalatee.getDeviceIds().length > 0)
  		devices = devices.concat(escalatee.getDeviceIds());

  	for (i = 0; i < devices.length; i++) {
  		escalateeGr = new GlideRecord(this.TABLES.CMN_NOTIF_DEVICE);
  		if (escalateeGr.get(devices[i]))
  			escalateesGr.push(escalateeGr);
  	}

  	if (escalatee.additionalEscalatees) {
  		for (i = 0; i < escalatee.additionalEscalatees.length; i++) {
  			var additionalEscalatee = escalatee.additionalEscalatees[i];

  			users = [];
  			groups = [];
  			devices = [];
  			if (additionalEscalatee.getUserId())
  				users.push(additionalEscalatee.getUserId());
  			if (additionalEscalatee.getUserIds() && additionalEscalatee.getUserIds().length > 0)
  				users = users.concat(additionalEscalatee.getUserIds());

  			for (j = 0; j < users.length; j++) {
  				escalateeGr = new GlideRecord(this.TABLES.SYS_USER);
  				if (escalateeGr.get(users[j]))
  					escalateesGr.push(escalateeGr);
  			}

  			if (additionalEscalatee.getEscalationGroups() && additionalEscalatee.getEscalationGroups().length > 0)
  				groups = groups.concat(additionalEscalatee.getEscalationGroups());

  			for (j = 0; j < groups.length; j++) {
  				escalateeGr = new GlideRecord(this.TABLES.SYS_USER_GROUP);
  				if (escalateeGr.get(groups[j]))
  					escalateesGr.push(escalateeGr);
  			}

  			if (additionalEscalatee.getDeviceId())
  				devices.push(additionalEscalatee.getDeviceId());
  			if (additionalEscalatee.getDeviceIds() && additionalEscalatee.getDeviceIds().length > 0)
  				devices = devices.concat(additionalEscalatee.getDeviceIds());

  			for (j = 0; j < devices.length; j++) {
  				escalateeGr = new GlideRecord(this.TABLES.CMN_NOTIF_DEVICE);
  				if (escalateeGr.get(devices[j]))
  					escalateesGr.push(escalateeGr);
  			}
  		}
  	}

  	return escalateesGr;
  },

  getEscalationType: function(rotaSysId) {
  	return this._escalationList.getEscalationType(rotaSysId);
  },

  /**
   * Get a value from the current notification. The 'get' methods are used to get information about
   * the current notification information that was set up as a result of a 'who' or 'whoAt' call.
   *
   * The current notification is positioned by calling calling the next() method
   *
   * name: [string] key to fetch the value for
   * return: [string] the value of the key/value pair
   */
  getValue: function(name) {
  	if ("rota" === name)
  		return this._escalationList.getRotaID();
  	if ("group" === name)
  		return this._groupSysId;
  	if ("primary_user" === name)
  		return this._escalationList.getPrimaryUserID();
  	if ("primary_users" === name)
  		return this._escalationList.getPrimaryUsers();
  	if ("primary_user_name" === name)
  		return this._escalationList.getPrimaryUserName();
  	if ("first_delay" === name)
  		return this._firstDelay + "";
  	if (this._currentEscalation)
  		return this._currentEscalation.get(name);
  	return null;
  },

  /**
   * Get the type of notification - can be:
   * device - group device such as phone, pager or email
   * rotation - user notification for an roster rotation member
   * user - user notification to a specific member of the group
   */
  getType: function() {
  	return this.getValue("type");
  },

  /**
   * Get the delay time for this notification
   * return [GlideDuration] with the delay set
   */
  getDelay: function() {
  	var dur = new GlideDuration();
  	var delay = parseInt(this.getValue("delay"));
  	if (isNaN(delay))
  		delay = 0;
  	dur.setValue(delay * 1000);
  	return dur;
  },

  /**
  * Get the time of contact for this notification
  *
  * retun [GlideDateTime] run at time set
  */
  getContactAt: function() {
  	var runAt = new GlideDateTime();
  	var runAtTime = parseInt(this.getValue("run_at")) * 1;
  	if (runAtTime > 0) {
  		runAt.setValue(runAtTime);
  	}
  	return runAt;
  },

  /**
   * Get the escalation group id for this notification
   * return [String] escalationGroupId
   */
  getEscalationGroupId: function() {
  	return this.getValue("group_id");
  },

  /**
   * Get the device id for this notification
   * return [String] deviceId
   */
  getDeviceId: function() {
  	return this.getValue("device_id");
  },

  /**
   * Get the sys_id of the catch all record
   *
   * rotaSysId [String] the rota sys_id
   * return [String] the rota catch all sys_id
   */
  getCatchAll: function(rotaSysId) {
  	var rotaGr = new GlideRecord("cmn_rota");
  	if (!rotaGr.get(rotaSysId))
  		return null;

  	if(rotaGr.getValue('use_custom_escalation') == '1')
  		return null;

  	var catchAll = (rotaGr.catch_all + "").toLowerCase();
  	if (catchAll === "group_manager") {
  		var groupGr = new GlideRecord("sys_user_group");
  		if (groupGr.get(rotaGr.group + ""))
  			return groupGr.manager + "";
  		return null;
  	}
  	if (catchAll === "individual") {
  		var catchAllMember = rotaGr.catch_all_member + "";
  		if (catchAllMember)
  			return catchAllMember;
  		return null;
  	}
  	if (catchAll === "all") {
  		var catchAllRoster = rotaGr.catch_all_roster + "";
  		if (catchAllRoster)
  			return catchAllRoster;
  		return null;
  	}
  	return null;
  },

  getRotaIDs: function () {
  	//_escalationList.rotaSysIds is set when who() method is invoked
  	return this._escalationList.getRotaIDs();
  },

  /**
   * Get the catch all type for this rota
   *
   * rotaSysId [String] the rota sys_id
   * return [String] the rota catch all type
   */
  getCatchAllType: function(rotaSysId) {
  	var rotaGr = new GlideRecord("cmn_rota");
  	if (rotaGr.get(rotaSysId)) {
  		if (this.isGroupEscalationApplied(rotaSysId))
  			return null;
  		if(rotaGr.getValue('use_custom_escalation') == '1')
  			return null;
  		return rotaGr.catch_all + "";
  	}
  	return null;
  },

  /**
   * Get the id of the current primary contact
   */
  getPrimaryUser: function() {
  	return this.getValue("primary_user");
  },

  /**
   * Get the list of the primary contacts
   */
  getPrimaryUsers: function() {
  	return this.getValue("primary_users");
  },

  /**
   * Get the id of the current primary contact
   */
  getPrimaryUserByRota: function(rotaSysId) {
  	return this.getValue("primary_users")[rotaSysId] ? this.getValue("primary_users")[rotaSysId].userSysId : null;
  },

  /**
   * Get the name of the current primary contact
   */
  getPrimaryUserName: function() {
  	return this.getValue("primary_user_name");
  },

  /**
   * Get the id of the current primary contact
   */
  getPrimaryUserNameByRota: function(rotaSysId) {
  	return this.getValue("primary_users")[rotaSysId] ? this.getValue("primary_users")[rotaSysId].userName : null;
  },

  /**
   * Get the id of the group this rotation is associated with
   */
  getGroup: function() {
  	return this.getValue("group");
  },

  /**
   * Get the name of the group this rotation is associated with
   */
  getGroupName: function() {
  	return this.getValue("group_name");
  },

  /**
   * Get the id of the currently active rota
   */
  getRota: function() {
  	return this.getValue("rota");
  },

  /**
   * Get the id of the rota from current escalation
   */
  getRotaFromCurrentEscalation: function() {
  	if (this._currentEscalation)
  		return this._currentEscalation.get("rota");
  	return null;
  },

  /**
   * Get the id of the currently active roster
   */
  getRoster: function() {
  	return this.getValue("roster");
  },

  /**
   * Get the name of user to notify
   * (only valid when getType() returns something other than 'device')
   */
  getUserName: function() {
  	return this.getValue("user_name");
  },

  /**
   * Get the id of the user to notify
   * (only valid when getType() returns something other than 'device')
   */
  getUser: function() {
  	return this.getValue("user");
  },

  /**
   * Get the device info (including the email_suffix is this is an sms message with a phone number)
   * (only valid when getType() returns 'device')
   */
  getDevice: function() {
  	return this.getValue("device");
  },

  getDeviceName: function() {
  	return this.getValue("device_name");
  },

  getDeviceEmail: function() {
  	return this.getValue("device_email");
  },

  getDeviceSMSAddress: function() {
  	return this.getValue("device_sms_address");
  },

  getDevicePhoneNumber: function() {
  	return this.getValue("device_phone_number");
  },

  /**
   * Get a flag indicating whether this should be an SMS message
   * (only valid when getType() returns 'device')
   * @Invalid method
   * @deprecated
   */
  getSMS: function() {
  	throw 'deprecated';
  },

  /**
   * Get the device service provider name
   * (only valid when getType() returns 'device')
   */
  getProvider: function() {
  	return this.getValue("device_provider");
  },

  /**
   * Get the base device contact info (should really use getDevice() method to get this information
   * as that method handles appending the SMS email suffix if required)
   * (only valid when getType() returns 'device')
   */
  getDeviceContact: function() {
  	return this.getValue("device_contact");
  },

  /**
   * Get the type of device to notify - can be:
   * phone
   * pager
   * email
   *
   * (only valid when getType() returns 'device')
   */
  getDeviceType: function() {
  	return this.getValue("device_type");
  },

  /**
   * Returns true if there are no notifications available now
   *
   * return: [boolean] are there any more notifications
   */
  isEmpty: function() {
  	this._escalationList.gotoTop();
  	var found = this.next();
  	this._escalationList.gotoTop();
  	return !found;
  },

  /**
   * True if there are no notifications available at any time
   *
   * return [boolean] whether there are any notifications at any given time
   */
  isEmptyAnytime: function() {
  	this._escalationList.gotoTop();
  	var found = this.nextAnytime();
  	this._escalationList.gotoTop();

  	if (this._log.atLevel(GSLog.DEBUG))
  		this._log.debug("[isEmptyAnytime] no notifications available at any time: " + !found);

  	return !found;
  },

  /**
   * Move to the next notification entry so that calls to get...() will
   * return information about the next entry.
   *
   * Returns true if there is another entry that is ready to be notified
   * or false if there are no more entries available for notification.
   */
  next: function() {
  	this._currentEscalation = this._escalationList.next();
  	if (this._currentEscalation != null)
  		if (this._currentEscalation.getRunAt() <= new GlideDateTime().getNumericValue())
  			return true;
  	this._currentEscalation = null;
  	return false;
  },

  /**
   * Move to the next notification entry so that calls to get...() will
   * return information about the next entry.
   *
   * This is different from 'next' in that 'next' only returns the next
   * entry if it is ready to be notified.  This function returns the
   * next notification entry even if it is to be notified in the future.
   *
   * See next: function() comments for more details
   *
   * return [boolean] true if there is another entry or false if there are no more
   * entries available for notification.
   */
  nextAnytime: function() {
  	this._currentEscalation = this._escalationList.next();
  	return this._currentEscalation ? true : false;
  },

  /**
   * Returns data used by the OnCall Groups landing page.
   *      {
   *              onCallData: [],
   *              groups: {},
   *              users: {},
   *              rotas: {},
   *              rosters: {},
   *              members: {}
   *      }
   *
   * dateTime [String]: "yyyy-MM-dd HH:mm:ss" format in UTC timezone. Default is now
   * offset [integer]: number of records to skip at begining of dataset. Default is 0 (do not skip any records)
   * groupsLimit [integer]: number of records to return, but cannot exceed com.snc.on_call_rotation.landing_page.group_limit which is also the default.
   * textSearch [String]: a string to search to by
   * excludeGroupSysIds [String]: comma separated list of group sys_ids to exclude from results
   */
  onCallGroups: function(dateTime, offset, groupsLimit, filter, textSearch, excludeGroupSysIds, textSearchType) {
  	if (this._log.atLevel(GSLog.DEBUG))
  		this.timer.start("[onCallGroups]");

  	offset = offset || 0;
  	dateTime = dateTime + "";
  	var gdt = new GlideDateTime();
  	if (dateTime)
  		gdt.setValue(dateTime);

  	groupsLimit = parseInt(groupsLimit);

  	if (!groupsLimit || isNaN(groupsLimit) || groupsLimit > this.groupsLimit)
  		groupsLimit = parseInt(this._gs.getProperty("com.snc.on_call_rotation.landing_page.group_limit", "20"));

  	textSearch = textSearch || "";
  	excludeGroupSysIds = excludeGroupSysIds || "";

  	var groups = new OCRotationV2().getGroups(groupsLimit, offset, filter, textSearch, excludeGroupSysIds, textSearchType);
  	return this._getOnCallByGroups(groups, gdt);
  },

  /**
   * Returns flatOnCallDataModel as shown below
   *	{
   *		onCallData: [],
   *		groups: {},
   *		users: {},
   *		rotas: {},
   *		rosters: {},
   *		members: {},
   * 		_metaData: {
   *			offset: integer,
   *			groupsLimit: integer,
   *			hasMoreGroups: boolean
   *		}
   *	}
   *
   * dateTime [String]: "yyyy-MM-dd HH:mm:ss" format in UTC timezone. Default is now
   * offset [integer]: number of records to skip at begining of dataset. Default is 0 (do not skip any records)
   * groupsLimit [integer]: number of records to return, but cannot exceed com.snc.on_call_rotation.landing_page.group_limit which is also the default.
   * textSearch [String]: a string to search to by
   * excludeGroupSysIds [String]: comma separated list of group sys_ids to exclude from results
   * showOnlyActiveGroups[Boolean]: filter to retrieve only the active data 
   */
  getOnCallGroupData: function(dateTime, offset, groupsLimit, textSearch, excludeGroupSysIds, showOnlyActiveGroups) {
  	dateTime = dateTime || '';
  	var gdt = new GlideDateTime();
  	if (dateTime)
  		gdt.setValue(dateTime);

  	offset = offset || 0;
  	if (!groupsLimit || isNaN(groupsLimit) || groupsLimit > this.groupsLimit)
  		groupsLimit = parseInt(this._gs.getProperty('com.snc.on_call_rotation.getOnCallGroupData.group_limit', 10));

  	textSearch = textSearch || '';
  	excludeGroupSysIds = excludeGroupSysIds || '';

  	var groupResultsData = this._getOnCallGroups(offset, groupsLimit, textSearch, excludeGroupSysIds, gdt, showOnlyActiveGroups);
  	var results = this._getOnCallByGroups(groupResultsData.groups, gdt, showOnlyActiveGroups);
  	results._metaData = {
  		offset: offset,
  		groupsLimit: groupsLimit,
  		hasMoreGroups: groupResultsData.hasMoreGroups,
  		groupsCount: groupResultsData.groupsCount,
  		activeGroupsCount: groupResultsData.activeGroupsCount
  	};

  	return results;
  },

  /**
   * Returns flatOnCallDataModel as shown below
   *	{
   *		onCallData: [],
   *		groups: {},
   *		users: {},
   *		rotas: {},
   *		rosters: {},
   *		members: {}
   *	}
   *
   * dateTime [String]: "yyyy-MM-dd HH:mm:ss" format in UTC timezone. Default is now
   * groupSysIds [String]: comma separated list of group sys_ids
   */
  getOnCallGroupDataByGroupSysId: function(dateTime, groupSysIds) {
  	dateTime = dateTime || '';
  	var gdt = new GlideDateTime();
  	if (dateTime)
  		gdt.setValue(dateTime);

  	groupSysIds = groupSysIds || '';

  	if (!groupSysIds)
  		return {
  			onCallData: [],
  			groups: {},
  			users: {},
  			rotas: {},
  			rosters: {},
  			members: {}
  		};

  	var groups = this._getOnCallGroupsBySysId(groupSysIds);
  	return this._getOnCallByGroups(groups, gdt);
  },

  _getOnCallGroupsBySysId: function(groupSysIds) {
  	var groups = [];

  	groupSysIds = groupSysIds || '';

  	var gr = new GlideRecord('sys_user_group');
  	gr.addActiveQuery();

  	if (groupSysIds)
  		gr.addQuery('sys_id', 'IN', groupSysIds);

  	gr.orderBy('name');
  	gr.query();

  	if (this._log.atLevel(GSLog.DEBUG))
  		this._log.logDebug('[_getOnCallGroupsBySysId] encodedQuery: ' + gr.getEncodedQuery());

  	while (gr.next()) {
  		if (!gr.canRead())
  			continue;

  		groups = this._addGroup(gr, groups);
  	}

  	return groups;
  },

  _getOnCallGroups: function(offset, limit, textSearch, excludeGroupSysIds, gdt, showOnlyActiveGroups) {
  	gdt = gdt || new GlideDateTime();
  	var groupResults = {
  		groups: [],
  		hasMoreGroups: false,
  		groupsCount: 0,
  		activeGroupsCount: 0
  	};

  	var groupSysIds = this._getGroupsWithRotasByName(textSearch);
  	if (!groupSysIds)
  		return groupResults;

  	var gr = new GlideRecord('sys_user_group');
  	var encodedQuery = 'active=true^ORDERBYname^sys_idIN' + groupSysIds;

  	excludeGroupSysIds = excludeGroupSysIds || '';
  	if (excludeGroupSysIds)
  		encodedQuery += '^sys_idNOT IN' + excludeGroupSysIds;

  	if (textSearch)
  		encodedQuery += '^nameLIKE' + textSearch;

  	gr.addEncodedQuery(encodedQuery);
  	
  	var ga = new GlideAggregate('sys_user_group');
  	ga.addEncodedQuery(encodedQuery);
  	
  	if (showOnlyActiveGroups) {
  		var onCallData = this.whoIsOnCall(groupSysIds, "", "", gdt);
  	
  		var activeGroupsArr = [];
  		var count = 0;

  		onCallData.forEach(function (ele) {
  			if (activeGroupsArr.indexOf(ele.group) === -1) {
  				activeGroupsArr.push(ele.group);
  				count++;
  			}
  		});
  		
  		groupResults.activeGroupsCount = count;
  	
  		var groupsEncodedQuery = 'sys_idIN' + activeGroupsArr;
  		
  		gr.addEncodedQuery(groupsEncodedQuery);
  		
  		ga.addEncodedQuery(groupsEncodedQuery);
  	}
  	
  	gr.query();
  	
  	ga.addAggregate('COUNT');
  	
  	ga.query();
  	
  	if (ga.next()) groupResults.groupsCount = ga.getAggregate('COUNT');

  	if (this._log.atLevel(GSLog.DEBUG))
  		this._log.logDebug('[_getOnCallGroups] encodedQuery: ' + gr.getEncodedQuery());

  	offset = (!offset || isNaN(offset) || offset < 0) ? 0 : parseInt(offset);
  	limit = (!limit || isNaN(limit) || limit < 0) ? 10 : parseInt(limit);

  	var currentOffset = 0;
  	while (gr.next() && limit > 0) {
  		currentOffset++;
  		if (!gr.canRead())
  			continue;

  		if (offset < currentOffset) {
  			groupResults.groups = this._addGroup(gr, groupResults.groups);
  			limit--;
  		}
  	}

  	groupResults.hasMoreGroups = gr.hasNext();

  	return groupResults;
  },

  _addGroup: function(gr, groups) {
  	if (!gr)
  		return [];

  	groups = groups || [];

  	var groupSysId = gr.getUniqueValue();
  	var group = {
  		sys_id: groupSysId,
  		id: groupSysId,
  		name: gr.getDisplayValue(),
  		text: gr.getDisplayValue(),
  		manager: gr.manager + '',
  		users: this._getGroupMembers(groupSysId),
  		editable: new global.OCCalendarUtils().canWriteByGroupSysId(groupSysId) + ''
  	};

  	groups.push(this.prepareOnCallGroupsDataGroup(gr, group));
  	return groups;
  },

  _getGroupsWithRotasByName: function(textSearch) {
  	var gr = new GlideRecord('cmn_rota');
  	var encodedQuery = 'active=true^group.active=true^ORDERBYgroup.name';
  	if (textSearch)
  		encodedQuery += '^group.nameLIKE' + textSearch;

  	gr.addEncodedQuery(encodedQuery);
  	gr.query();

  	var groupKeys = {};
  	while (gr.next()) {
  		if (!gr.canRead())
  			continue;

  		groupKeys[gr.group.getRefRecord().getUniqueValue()] = true;
  	}

  	return Object.keys(groupKeys);
  },

  prepareOnCallGroupsDataGroup: function(groupGr, group) {
  	return group || {};
  },

  prepareOnCallGroupsDataRota: function(rotaGr, rota) {
  	return rota || {};
  },

  prepareOnCallGroupsDataRoster: function(rosterGr, roster) {
  	return roster || {};
  },

  prepareOnCallGroupsDataMember: function(memberGr, member) {
  	return member || {};
  },

  prepareOnCallGroupsDataUser: function(userGr, user) {
  	return user || {};
  },

  _getGroupMembers: function(groupSysId) {
  	var members = [];
  	if (!groupSysId)
  		return members;

  	var gr = new GlideRecord('sys_user_grmember');
  	gr.addQuery('group', groupSysId);
  	gr.query();
  	while (gr.next())
  		members.push(gr.user + '');

  	return members;
  },

  _getOnCallByGroups: function(groups, gdt, showOnlyActiveGroups) {
  	gdt = gdt || new GlideDateTime();

  	var showPendingActions = gs.getProperty('com.snc.on_call_rotation.landing_page.show_pending_actions', true) + '' === 'true';
  	if (showPendingActions)
  		this._addPendingActionsCount(groups);
  	return this._processOnCallGroups(groups, gdt, showOnlyActiveGroups);
  },

  _addPendingActionsCount: function (groups) {
  	var days = gs.getProperty("com.snc.on_call_rotation.upcoming_rota_days", 30);
  	var ocSecurityNg = new OnCallSecurityNG();
  	var oc = new OCOddityChecker();
  	var context = this;
  	
  	var todayDate = new GlideDate();
  	var from = todayDate.getDisplayValueInternal();
  	todayDate.addDays(days);
  	var to = todayDate.getDisplayValueInternal();
  	
  	var dateDiff = gs.dateDiff(from, to, true);
  	var dateDiffInDays = parseInt(dateDiff / (24 * 60 * 60));
  	
  	var groupSysIds = [];
  	for (var i = 0; i < groups.length; i++) {
  		groupSysIds.push(groups[i].sys_id);
  	}
  	var groupManagerAccessMap = ocSecurityNg.getManagerAccessForGroups(groupSysIds);
  	
  	groups = groups.map(function(group) {
  		if (!groupManagerAccessMap[group.sys_id])
  			return group;
  		
  		var rotaIds = [];
  		if (group.sys_id) {
  			var rotaGr = new GlideRecord(context.TABLES.CMN_ROTA);
  			rotaGr.addQuery("group", group.sys_id);
  			rotaGr.query();
  			while (rotaGr.next()) {
  				rotaIds.push(rotaGr.getUniqueValue());
  			}
  		}
  		
  		var formatterClass = OCFormatterMapping.formatters["dhtmlx"];
  		var formatter = new formatterClass();

  		var ocrRotaV2 = new OCRotationV2(null, formatter);
  		var spans = ocrRotaV2
  			.setStartDate(from)
  			.setEndDate(to, false)
  			.setGroupIds(group.sys_id)
  			.setRotaIds(rotaIds.join(','))
  			.getSpans();
  		
  		var actionsCount = {};
  		var gaps = oc.getGaps(from, to, group.sys_id, dateDiffInDays, spans);
  		actionsCount.gaps = parseInt(gaps.total_count) || 0;
  		
  		var oddities = oc.getConflictsInGroup(from, to, group.sys_id, dateDiffInDays, spans);
  		actionsCount.oddities = oddities.length || 0;
  		
  		var timeOffRequests = oc.getPendingTimeOffRequests();
  		actionsCount.timeOffRequests = parseInt(timeOffRequests) || 0;
  		
  		if ((actionsCount.gaps + actionsCount.oddities  + actionsCount.timeOffRequests) > 0)
  			group.actionsCount = actionsCount;
  		return group;
  	});
  },

  _processOnCallGroups: function(groups, gdt, showOnlyActiveGroups) {
  	if (this._log.atLevel(GSLog.DEBUG))
  		this.timer.start("[_processOnCallGroups]");

  	var groupSysIds = [];
  	for (var i = 0; i < groups.length; i++) {
  		groupSysIds.push(groups[i].sys_id);
  	}
  	var onCallSecurityNG = new OnCallSecurityNG();
  	var rotaMgrAccessMap = onCallSecurityNG.rotaMgrAccessForGroups(groupSysIds);
  	var groupData = {};
  	if (groups) {
  		groups.map(function(group) {
  			var isCurrentUserRotaMgr = rotaMgrAccessMap[group.sys_id];
  			group['isCurrentUserRotaMgr'] = isCurrentUserRotaMgr;
  			
  			var todayDate = new GlideDateTime().getDate().getValue(); // returns internal formatted date
  			var memberGr = new GlideRecord('cmn_rota_member');
  			memberGr.addQuery("member", "=", gs.getUserID());
  			memberGr.addQuery("roster.rota.group", "=", group.sys_id);
  			memberGr.addEncodedQuery("from=NULL^ORfrom<=" + todayDate);
  			memberGr.addEncodedQuery("to=NULL^ORto>=" + todayDate);
  			memberGr.setLimit(1);
  			memberGr.query();
  			group['isCurrentUserGrpMember'] = memberGr.hasNext() && gs.getUser().isMemberOf(group.sys_id + "");

  			groupData[group.sys_id + ""] = group;
  		});
  	}
  	groupSysIds = Object.keys(groupData).join(",");
  	var onCallData = this.whoIsOnCall(groupSysIds, "", "", gdt);
  	
  	if (showOnlyActiveGroups && onCallData.length === 0)
  		return {};
  	
  	var rotaData = this.getRotas(groupSysIds);
  	var rotaSysIds = Object.keys(rotaData).join(",");
  	var rosterData = this.getRostersByRotas(rotaSysIds);
  	var rosterSysIds = Object.keys(rosterData).join(",");

  	var userSysIds = [];
  	onCallData.forEach(function(onCall) {
  		if (onCall.userId)
  			userSysIds.push(onCall.userId);
  	});
  	
  	var result = {
  		onCallData: onCallData,
  		groups: groupData,
  		users: this._getUsers(userSysIds),
  		rotas: rotaData,
  		rosters: rosterData,
  		members: this.getMembers(rosterSysIds)
  	};

  	if (this._log.atLevel(GSLog.DEBUG))
  		this.timer.stop("[_processOnCallGroups]");

  	return result;
  },
  
  processIndividualOCData: function (groups, gdt) {
  	return this._processOnCallGroups(groups, gdt);
  },

  /**
   * Start the escalations for a document and group (call after a successful call to who or whoAt)
   * and specify the event name for the escalation notifications.  In addition,
   * specify the name of the business rule to call when an escalation occurs.
   */
  startEscalations: function(gr, eventName, escalationScriptName) {
  	if (!this._groupSysId || !this._rotaSysIds || (this._rotaSysIds && this._rotaSysIds.length == 0)) {
  		// Invalid calling sequence, Ignore this request
  		// Call who or whoIsNext before this is called so that the notifications are properly set up for escalation
  		this._log.error("[continueEscalations] Escalations called out of sequence");
  		return;
  	}

  	if (this._log.atLevel(GSLog.DEBUG))
  		this._log.debug("[startEscalations] groupSysId: " + this._groupSysId + " rotaSysId: " + this._rotaSysIds[0] + " eventName: " + eventName + " escalationScriptName: " + escalationScriptName);

  	// Figure out when the first escalation needs to fire
  	var runAt = this._escalationList.runAt();

  	if (this._log.atLevel(GSLog.DEBUG))
  		this._log.debug("[startEscalations] runAt: " + runAt);

  	if (runAt < 0) {
  		// No more escalations - see if there are more rotas to run through
  		this._escalationList.clear();
  		this._currentEscalation = null;
  		this._checkForMoreRotas(gr);
  		if (this.isEmptyAnytime())
  			return;
  	}
  	this._saveEscalation(gr, eventName, escalationScriptName, runAt);
  },

  getCurrentRotaID: function() {
  	return this._rotaSysIds[0];
  },

  /*
   * Return array of rota ids in case overlapping schedule
   */
  getCurrentRotaIDs: function() {
  	return this._rotaSysIds;
  },

  setCurrentRotaID: function(rotaSysId) {
  	this._rotaSysIds = [rotaSysId];
  },

  setCurrentRotaIDs: function(rotaSysIds) {
  	this._rotaSysIds = rotaSysIds;
  },

  addCurrentRotaID: function(rotaSysId) {
  	if(!this._rotaSysIds){
  		this._rotaSysIds = [];
  	}
  	this._rotaSysIds.push(rotaSysId);
  },

  /**
   * Set max entries to return for call to who/whoAt (-1 means all for active roster)
   */
  setMaxEntries: function(entries) {
  	this._maxEntries = entries;
  },

  getGroupEscalationGr: function (rotaId) {
  	if (!rotaId) {
  		return;
  	}

  	var rotaGr = new GlideRecord(this.TABLES.CMN_ROTA);
  	if (rotaGr.get(rotaId)) {
  		var groupId = rotaGr.getValue('group');

  		var groupEscalationGr = new GlideRecord(this.TABLES.ON_CALL_GROUP_ESCALATION);
  		groupEscalationGr.addQuery('group', groupId);
  		var gc = groupEscalationGr.addQuery('apply_to_all_rotas', true);
  		gc.addOrCondition('rotas', 'CONTAINS', rotaId);
  		groupEscalationGr.addActiveQuery();
  		groupEscalationGr.query();

  		if (groupEscalationGr.next()) {
  			return groupEscalationGr;
  		}
  	}
  },

  isGroupEscalationApplied: function (rotaId) {
  	var groupEscalationGr = this.getGroupEscalationGr(rotaId);
  	if (groupEscalationGr) {
  		return true;
  	}

  	return false;
  },

  /**
   * Set up the notification list for this group based on the
   * current time.
   *
   * When this method returns, next() should be called until it
   * returns false to indicate no more current notifications.
   * At that point, a call to startEscalations() should be made
   * to set up for the pending notifications set up by the who()
   * call.
   *
   * Usage:
   *    var rota = new OnCallRotation();
   *    rota.who();
   *    while (rota.next()) {
   *    	// generate event based on the properties of the user to be notified
   *    }
   *
   *    // set up to get any future notifications
   *    rota.startEscalations(glide_record);
   *
   *  Notes:
   *    After you call rota.who(), you may retrieve the primary contact that is on-call
   *    right now using:
   *
   *       rota.getPrimaryUser();
   *       rota.getPrimaryUserName();
   *
   * groupSysId [String]: sys_id of the group to check for
   * gdt [GlideDateTime]: "yyyy-MM-dd HH:mm:ss" format in UTC timezone. Default is now
   * nullifyOverrideRoster [boolean]: if set to true it is not for a specific roster
   * return [boolean]: true means found at least one match, false if no matches found
   */
  who: function(groupSysId, gdt, nullifyOverrideRoster, rotaSysIds, overrideCustomEscalation, taskGr) {
  	this._escalationList.clear();
  	this._clearEscalationLineUp();

  	this._groupSysId = groupSysId ? groupSysId + "" : "";
  	this._escalationList.setGroupID(this._groupSysId);
  	this._currentEscalation = null;
  	this._rotaSysIds = [];
  	if (typeof nullifyOverrideRoster === "undefined")
  		nullifyOverrideRoster = false;
  	gdt = gdt || new GlideDateTime();

  	if (this._log.atLevel(GSLog.DEBUG))
  		this._log.debug("[who] groupSysId: " + groupSysId + " gdt: " + gdt + " nullifyOverrideRoster: " + nullifyOverrideRoster);

  	var wrktimeGdt = new GlideDateTime(gdt);
  	var rosterGr;
  	if (this._onCallCommon.isOverlapAllowed(this._groupSysId)) {
  		var rotas = this._getAllRotaWithOverlappingSchedule(wrktimeGdt, rotaSysIds);
  		if (rotas !== -1 && rotas.length > 0) {
  			if(!this._simulateEscalationToAllRotas){
  				var escalationSettings = this._onCallCommon.getEscalationSettings(this._groupSysId);
  				if (escalationSettings == this._onCallCommon.escalation_rule_rota_overlap.START) {
  					rotas = [this._findFirstStartedRota(rotas)];
  				} else if (escalationSettings == this._onCallCommon.escalation_rule_rota_overlap.END) {
  					rotas = [this._findEndingLastRota(rotas)];
  				}
  			}
  			this._rotaSysIds = [];
  			for (var i = 0; i < rotas.length; i++) {
  				this._rotaSysIds.push(rotas[i].rotaSysId);
  				this._rotaTimeZone = rotas[i].rotaTimeZone;
  				var isAdditionalRoster = (i == 0 ? false : true);

  				var isGroupEscalationApplied = this.isGroupEscalationApplied(rotas[i].rotaSysId);

  				if (!overrideCustomEscalation && (rotas[i].rotaCustomEscalation || isGroupEscalationApplied)) {
  					this._getCustomEscalation(rotas[i].rotaSysId, wrktimeGdt, isAdditionalRoster, taskGr);
  				} else {
  					rosterGr = new GlideRecord(this.TABLES.CMN_ROTA_ROSTER);
  					rosterGr.addQuery("rota", this._rotaSysIds[this._rotaSysIds.length - 1]);
  					rosterGr.addActiveQuery();
  					rosterGr.orderBy("order");
  					rosterGr.query();
  					this._getRotaNotifications(rosterGr, wrktimeGdt, nullifyOverrideRoster, isAdditionalRoster);
  				}
  			}
  		}
  	} else {
  		this._firstDelay = this._getFirstDelay(wrktimeGdt, rotaSysIds);

  		if (this._rotaSysIds[this._rotaSysIds.length - 1]) {
  			this._escalationList.setRotaID(this._rotaSysIds[this._rotaSysIds.length - 1]);
  			var isGroupEscalationApplied = this.isGroupEscalationApplied(this._rotaSysIds[this._rotaSysIds.length - 1]);
  			if (!overrideCustomEscalation && (this._rotaCustomEscalation || isGroupEscalationApplied)) {
  				this._getCustomEscalation(this._rotaSysIds[this._rotaSysIds.length - 1], wrktimeGdt, false, taskGr);
  			} else {
  				wrktimeGdt.addSeconds(this._firstDelay);
  				rosterGr = new GlideRecord(this.TABLES.CMN_ROTA_ROSTER);
  				rosterGr.addQuery("rota", this._rotaSysIds[this._rotaSysIds.length - 1]);
  				rosterGr.addActiveQuery();
  				rosterGr.orderBy("order");
  				rosterGr.query();
  				if (!rosterGr.hasNext())
  					this._setPrimaryUser(wrktimeGdt);
  				this._getRotaNotifications(rosterGr, wrktimeGdt, nullifyOverrideRoster);
  			}
  		}
  	}
  	return !this.isEmpty();
  },

  /*
   * Find rota which will end first
   * rotas [array]
   */
  _findEndingLastRota: function(rotas) {
  	if (rotas.length === 1) {
  		return rotas[0];
  	}
  	var selectedRotaIndex = 0;
  	var selectedEndTime = (new GlideDateTime(rotas[0].end)).getNumericValue();

  	var rotaLength = rotas.length;
  	for (var i = 1; i < rotaLength; i++) {
  		var endTime = (new GlideDateTime(rotas[i].end)).getNumericValue();
  		if (endTime > selectedEndTime) {
  			selectedEndTime = endTime;
  			selectedRotaIndex = i;
  		}
  	}
  	return rotas[selectedRotaIndex];
  },

  /*
   * Find rota which has started recently
   * rotas [array]
   */
  _findFirstStartedRota: function(rotas) {
  	if (rotas.length === 1) {
  		return rotas[0];
  	}
  	var selectedRotaIndex = 0;
  	var selectedStartTime = (new GlideDateTime(rotas[0].start)).getNumericValue();

  	var rotaLength = rotas.length;
  	for (var i = 1; i < rotaLength; i++) {
  		var startTime = (new GlideDateTime(rotas[i].start)).getNumericValue();
  		if(startTime < selectedStartTime) {
  			selectedStartTime = startTime;
  			selectedRotaIndex = i;
  		}
  	}
  	return rotas[selectedRotaIndex];
  },

  _getCustomEscalation: function (rotaId, gdt, isAdditionalRota, taskGr) {
  	var isGroupEscalation = false;
  	var groupEscalationGr = this.getGroupEscalationGr(rotaId);
  	if (groupEscalationGr) {
  		isGroupEscalation = true;
  	}

  	if(!isAdditionalRota){
  		this._escalationList.clear();
  		this._clearEscalationLineUp();
  		this._escalationList.setGroupID(this._groupSysId);
  		this._escalationList.setRotaID(this._rotaSysIds[this._rotaSysIds.length - 1]);
  		this._escalationList.setTimeZone(this._rotaTimeZone);
  	} else {
  		this._escalationLevel = 0;
  		this._escalationList.addRotaID(this._rotaSysIds[this._rotaSysIds.length - 1]);
  	}

  	this._addPrimaryMembers(rotaId, gdt, isAdditionalRota);

  	var rotaEscalationSet = this.getRotaEscalationSet(rotaId, taskGr);
  	if(!rotaEscalationSet)
  		return;
  	var rotaEscalationDefinitionGr = this._getRotaEscalationDefinition(rotaEscalationSet.getValue('sys_id'));
  	var delayTillPreviousStep = 0;
  	while (rotaEscalationDefinitionGr.next()) {
  		var rotaEscalationDefinitionSysId = rotaEscalationDefinitionGr.getValue('sys_id');
  		var reminderDuration = rotaEscalationDefinitionGr.time_between_reminders.getGlideObject();
  		var reminderDelay = reminderDuration ? (reminderDuration.getNumericValue() / 1000) : 0;
  		var reminders = parseInt(rotaEscalationDefinitionGr.reminders + "");
  		reminders = isNaN(reminders) ? 1 : reminders + 1;
  		var totalDelay;
  		var remindersLeft;

  		if (rotaEscalationDefinitionGr.rotate_through_member) {
  			this._delayTillPreviousStep = delayTillPreviousStep;
  			var rosterGr = new GlideRecord(this.TABLES.CMN_ROTA_ROSTER);
  			if (rosterGr.get(rotaEscalationDefinitionGr.cmn_rota_roster_rtm)) {
  				this._addRotationEscalations(rosterGr, gdt, false, isAdditionalRota, rotaEscalationDefinitionGr);
  				delayTillPreviousStep = this._delayTillPreviousStep;
  			}
  			continue;
  		}

  		var users = rotaEscalationDefinitionGr.getValue('sys_users');
  		var usersArray = [];
  		if (users) {
  			var usersGr = new GlideRecord(this.TABLES.SYS_USER);
  			usersGr.addQuery('sys_id', "IN", users);
  			usersGr.addActiveQuery();
  			usersGr.query();
  			while (usersGr.next()) {
  				usersArray.push(usersGr.getUniqueValue());
  				totalDelay = delayTillPreviousStep;
  				remindersLeft = reminders - 1;
  				this._addCustomEscalation('user', usersGr.getUniqueValue(), gdt, totalDelay);
  				while (remindersLeft > 0) {
  					totalDelay += reminderDelay;
  					this._addCustomEscalation('user', usersGr.getUniqueValue(), gdt, totalDelay);
  					remindersLeft--;
  				}
  			}
  		}

  		var escalateToGroupManager = rotaEscalationDefinitionGr.getValue('group_manager');
  		if (escalateToGroupManager == '1') {
  			var managerElement = null;
  			var groupManagerField = rotaEscalationDefinitionGr.getValue('group_manager_field');
  			groupManagerField = groupManagerField ? groupManagerField : "manager";

  			if (groupEscalationGr && rotaEscalationSet.group_escalation && rotaEscalationSet.group_escalation.group)
  				managerElement = rotaEscalationSet.getElement("group_escalation.group." + groupManagerField);
  			else if (!groupEscalationGr && rotaEscalationSet.cmn_rota && rotaEscalationSet.cmn_rota.group)
  				managerElement = rotaEscalationSet.getElement("cmn_rota.group." + groupManagerField);

  			if (managerElement && !managerElement.active)
  				managerElement = null;

  			if (managerElement && (managerElement + "")) {
  				usersArray.push(managerElement + "");
  				totalDelay = delayTillPreviousStep;
  				remindersLeft = reminders - 1;
  				this._addCustomEscalation('user', managerElement + "", gdt, totalDelay);
  				while (remindersLeft > 0) {
  					totalDelay += reminderDelay;
  					this._addCustomEscalation('user', managerElement + "", gdt, totalDelay);
  					remindersLeft--;
  				}
  			}
  		}

  		var groups = rotaEscalationDefinitionGr.getValue('sys_user_groups');
  		var groupsArray = [];
  		if (groups) {
  			var groupsGr = new GlideRecord(this.TABLES.SYS_USER_GROUP);
  			groupsGr.addQuery('sys_id', "IN", groups);
  			groupsGr.addActiveQuery();
  			groupsGr.query();
  			while (groupsGr.next()) {
  				groupsArray.push(groupsGr.getUniqueValue());
  				totalDelay = delayTillPreviousStep;
  				remindersLeft = reminders - 1;
  				this._addCustomEscalation('group', groupsGr.getUniqueValue(), gdt, totalDelay);
  				while (remindersLeft > 0) {
  					totalDelay += reminderDelay;
  					this._addCustomEscalation('group', groupsGr.getUniqueValue(), gdt, totalDelay);
  					remindersLeft--;
  				}
  			}
  		}

  		var devices = rotaEscalationDefinitionGr.getValue('cmn_notif_devices');
  		var devicesArray = [];
  		if (devices) {
  			var devicesGr = new GlideRecord(this.TABLES.CMN_NOTIF_DEVICE);
  			devicesGr.addQuery('sys_id', "IN", devices);
  			devicesGr.addActiveQuery();
  			devicesGr.query();
  			while (devicesGr.next()) {
  				devicesArray.push(devicesGr.getUniqueValue());
  				totalDelay = delayTillPreviousStep;
  				remindersLeft = reminders - 1;
  				this._addCustomEscalation('device', devicesGr.getUniqueValue(), gdt, totalDelay);
  				while (remindersLeft > 0) {
  					totalDelay += reminderDelay;
  					this._addCustomEscalation('device', devicesGr.getUniqueValue(), gdt, totalDelay);
  					remindersLeft--;
  				}
  			}
  		}

  		var rosterSysIds = '';
  		if (groupEscalationGr) {
  			var rosterPosition = rotaEscalationDefinitionGr.getValue('roster_position') || '';
  			rosterPosition = rosterPosition.trim()
  				.split(',')
  				.map(function(position) {
  					return parseInt(position);
  				})
  				.filter(function(position) {
  					return !isNaN(position) && position > 0;
  				});
  			if (rosterPosition && rosterPosition.length > 0) {
  				var rosterGr = new GlideRecord(this.TABLES.CMN_ROTA_ROSTER);
  				rosterGr.addQuery('rota', rotaId);
  				rosterGr.orderBy('order');
  				rosterGr.query();
  				var rosterIndex = 0;
  				var rosterIds = [];
  				while (rosterGr.next()) {
  					rosterIndex++;
  					if (rosterPosition.indexOf(rosterIndex) > -1) {
  						rosterIds.push(rosterGr.getUniqueValue() + '');
  					}
  				}
  				rosterSysIds = rosterIds.join(',');
  			}
  		}
  		else {
  			if (rotaEscalationDefinitionGr.isValidField('cmn_rota_rosters')) {
  				var selectedRosterSysIds = rotaEscalationDefinitionGr.getValue('cmn_rota_rosters');
  				if (selectedRosterSysIds) {
  					rosterSysIds = selectedRosterSysIds;
  				}
  			}
  			else {
  				rosterSysIds = rotaEscalationDefinitionGr.getValue('cmn_rota_roster');
  			}
  		}

  		var membersArray = [];
  		if (rosterSysIds) {
  			var rosterGr = new GlideRecord(this.TABLES.CMN_ROTA_ROSTER);
  			rosterGr.addQuery('sys_id', 'IN', rosterSysIds);
  			rosterGr.query();
  			while(rosterGr.next()) {
  				var rosterSysId = rosterGr.getUniqueValue();
  				var memberGr = new GlideRecord(this.TABLES.CMN_ROTA_MEMBER);
  				
  				var userSysId = this._checkForOverrideMemberByRoster(gdt, rosterSysId);
  				
  				if (this._getRotationMember(memberGr, gdt, rosterSysId) || userSysId) {
  					var memberSysId = memberGr.sys_id + "";
  					
  					if (!userSysId)
  						userSysId = this._checkForOverrideMember(gdt);

  					if (!userSysId)
  						userSysId = memberGr.member + "";

  					var isPrimaryMemberTimeOff = this._isTheUserOff(userSysId, gdt);
  					// check if the person is off
  					if (!isPrimaryMemberTimeOff) {
  						totalDelay = delayTillPreviousStep;
  						this._addRotationEscalation(memberGr, userSysId, gdt, totalDelay, rosterSysId);

  						usersArray.unshift(userSysId);
  						membersArray.push(memberGr.getValue('sys_id'));

  						// repeat the first user if multiple attempts are requested
  						remindersLeft = reminders - 1;
  						while (remindersLeft > 0) {
  							totalDelay += reminderDelay;
  							this._addRotationEscalation(memberGr, userSysId, gdt, totalDelay, rosterSysId);
  							remindersLeft--;
  						}
  					}

  					var maxMemberCnt = 0;
  					if (rotaEscalationDefinitionGr.getValue('roster_escalation') == 'all_members')
  						maxMemberCnt = 100;
  					else if(isPrimaryMemberTimeOff)
  						maxMemberCnt = 1;

  					var memberCnt = 1;
  					var done = false;
  					while (!done) {
  						if (memberCnt > maxMemberCnt)
  							done = true;

  						if (!done) {
  							if (!memberGr.next()) {
  								memberGr.setLocation(-1);
  								memberGr.next();
  							}

  							if (memberGr.sys_id + "" === memberSysId)
  								done = true;
  							else {
  								userSysId = memberGr.member + "";

  								if (this._isTheUserOff(userSysId, gdt))
  									continue;

  								totalDelay = delayTillPreviousStep;
  								this._addRotationEscalation(memberGr, userSysId, gdt, totalDelay, rosterSysId);

  								usersArray.push(userSysId);
  								membersArray.push(memberGr.getValue('sys_id'));

  								remindersLeft = reminders - 1;
  								while (remindersLeft > 0) {
  									totalDelay += reminderDelay;
  									this._addRotationEscalation(memberGr, userSysId, gdt, totalDelay, rosterSysId);
  									remindersLeft--;
  								}
  							}
  							memberCnt++;
  						}
  					}
  				}
  			}
  		}

  		usersArray = usersArray.filter(function(val, index, self) {
  			return self.indexOf(val) == index;
  		});
  		var timeToNextStep = rotaEscalationDefinitionGr.time_to_next_step.getGlideObject();
  		var forcedCommunicationChannel = rotaEscalationDefinitionGr.getValue('forced_communication_channel');
  		var overrideUserContactPreference = rotaEscalationDefinitionGr.escalation_set.override_user_contact_preference + "";

  		if (this._log.atLevel(GSLog.DEBUG))
  			this._log.logDebug('[_getCustomEscalation] overrideUserContactPreference: ' + overrideUserContactPreference);

  		rosterSysIds = rosterSysIds.split(',');
  		var rosterId = rosterSysIds.length === 1 ? rosterSysIds[0] : '';
  		if (!isAdditionalRota)
  			this._addCustomEscalateeToEscalationLineUp(usersArray, groupsArray, devicesArray, membersArray, new GlideDuration(reminderDuration), (reminders - 1), new GlideDuration(timeToNextStep), rotaEscalationDefinitionSysId, rosterId, forcedCommunicationChannel, this._escalationLevel + 1, overrideUserContactPreference, rotaId, isGroupEscalation, rosterSysIds);
  		else
  			this._addAdditionalCustomEscalateeToEscalationLineUp(usersArray, groupsArray, devicesArray, membersArray, new GlideDuration(reminderDuration), (reminders - 1), new GlideDuration(timeToNextStep), rotaEscalationDefinitionSysId, rosterId, forcedCommunicationChannel, this._escalationLevel + 1, overrideUserContactPreference, rotaId, isGroupEscalation, rosterSysIds);

  		this._escalationLevel++;
  		delayTillPreviousStep = delayTillPreviousStep + reminderDelay * (reminders - 1) + (timeToNextStep ? (timeToNextStep.getNumericValue()/1000) : 0);
  	}
  },


  _addPrimaryMembers: function(rotaId, gdt, isAdditionalRota) {
  	var workTimeGdt = new GlideDateTime(gdt);
  	var rosterGr = new GlideRecord(this.TABLES.CMN_ROTA_ROSTER);
  	rosterGr.addQuery("rota", rotaId);
  	rosterGr.addActiveQuery();
  	rosterGr.orderBy("order");
  	rosterGr.query();
  	var memberGr;
  	var userSysId;
  	if (rosterGr.getRowCount() === 1) {
  		rosterGr.next();
  		memberGr = new GlideRecord(this.TABLES.CMN_ROTA_MEMBER);
  		if (this._getRotationMember(memberGr, workTimeGdt, rosterGr.sys_id + "")) {
  			userSysId = this._checkForOverrideMemberByRoster(workTimeGdt, rosterGr.sys_id + "");

  			if (!userSysId)
  				userSysId = this._checkForOverrideMember(workTimeGdt);

  			if (!userSysId)
  				userSysId = memberGr.member + "";

  			var isPrimaryMemberTimeOff = this._isTheUserOff(userSysId, workTimeGdt);
  			// check if the person is off
  			if (!isPrimaryMemberTimeOff) {
  				if (!isAdditionalRota) {
  					this._escalationList.setPrimaryUserID(userSysId, rotaId, rosterGr.getUniqueValue());
  				} else if (!this._escalationList.getPrimaryUserIdByRota(rotaId)) {
  					this._escalationList.addPrimaryUserID(userSysId, rotaId, rosterGr.getUniqueValue());
  				}
  			}
  		}
  	} else if (rosterGr.getRowCount() > 1) {
  		while (rosterGr.next()) {
  			var rosterSysId = rosterGr.sys_id + "";
  			memberGr = new GlideRecord(this.TABLES.CMN_ROTA_MEMBER);

  			if (this._getRotationMember(memberGr, workTimeGdt, rosterSysId)) {
  				userSysId = this._checkForOverrideMemberByRoster(workTimeGdt, rosterGr.sys_id + "");

  				if (!userSysId)
  					userSysId = this._checkForOverrideMember(workTimeGdt);

  				if (!userSysId)
  					userSysId = memberGr.member + "";

  				if (this._isTheUserOff(userSysId, workTimeGdt))
  					continue;

  				if (!isAdditionalRota) {
  					if (!this._escalationList.getPrimaryUserID())
  						this._escalationList.setPrimaryUserID(userSysId, rotaId, rosterSysId);
  				} else if (!this._escalationList.getPrimaryUserIdByRota(rotaId)) {
  					this._escalationList.addPrimaryUserID(userSysId, rotaId, rosterSysId);
  				}
  			}
  		}
  	}
  },

  _getRotaDefaultEscalationSet: function (rotaId) {
  	var groupEscalationGr = this.getGroupEscalationGr(rotaId);

  	var escalationSetGr = new GlideRecord(this.TABLES.CMN_ROTA_ESCALATION_SET);
  	if (groupEscalationGr) {
  		escalationSetGr.addQuery('group_escalation', groupEscalationGr.getUniqueValue());
  	}
  	else {
  		escalationSetGr.addQuery('cmn_rota', rotaId);
  	}
  	escalationSetGr.addQuery('default', true);
  	escalationSetGr.addActiveQuery();
  	escalationSetGr.query();
  	if (escalationSetGr.next())
  		return escalationSetGr;
  },

  _getEscalationSetById: function (sysId) {
  	var escalationSetGr = new GlideRecord(this.TABLES.CMN_ROTA_ESCALATION_SET);
  	if (escalationSetGr.get(sysId))
  		return escalationSetGr;
  },

  _getRotaEscalationSetByTask: function (rotaId, taskGr) {
  	var groupEscalationGr = this.getGroupEscalationGr(rotaId);

  	var escalationSetGr = new GlideRecord(this.TABLES.CMN_ROTA_ESCALATION_SET);
  	if (groupEscalationGr) {
  		escalationSetGr.addQuery('group_escalation', groupEscalationGr.getUniqueValue());
  	}
  	else {
  		escalationSetGr.addQuery('cmn_rota', rotaId);
  	}
  	escalationSetGr.addQuery('default', false);
  	escalationSetGr.orderBy('order');
  	escalationSetGr.addActiveQuery();
  	escalationSetGr.query();
  	while (escalationSetGr.next()) {
  		var tableMatched = escalationSetGr.getValue('table') == taskGr.getRecordClassName();
  		if (tableMatched) {
  			var conditionMatched = GlideFilter.checkRecord(taskGr, escalationSetGr.condition);
  			if (conditionMatched)
  				return escalationSetGr;
  		}
  	}
  	return this._getRotaDefaultEscalationSet(rotaId);
  },

  getRotaEscalationSet: function (rotaId, taskGr) {
  	if (this.forcedEscalationSetSysId)
  		return this._getEscalationSetById(this.forcedEscalationSetSysId);
  	if (taskGr)
  		return this._getRotaEscalationSetByTask(rotaId, taskGr);
  	return this._getRotaDefaultEscalationSet(rotaId);
  },

  _getRotaEscalationDefinition: function(escalationDefinitionId) {
  	var cmnRotaEscalationDefinitionGr = new GlideRecord(this.TABLES.CMN_ROTA_ESCALATION_DEFINITION);
  	cmnRotaEscalationDefinitionGr.addQuery('escalation_set', escalationDefinitionId);
  	cmnRotaEscalationDefinitionGr.orderBy('escalation_level');
  	cmnRotaEscalationDefinitionGr.query();
  	return cmnRotaEscalationDefinitionGr;
  },

  /**
   * Set up the notification list for this group based on the
   * specified time.  See who() for more information on the
   * usage of this method.
   *
   * groupSysId [string]
   * gdt [GlideDateTime or string]: in the user's date-time format
   */
  whoAt: function(groupSysId, gdt, rotaSysIds, taskGr) {
  	var _gdt = new GlideDateTime();
  	try {
  		// typeof gdt for GlideDateTime object fails on executing it from a scoped app, so wrapped it in a try catch block and reassign _gdt in catch block
  		if (typeof gdt === 'string')
  			_gdt.setDisplayValue(gdt);
  		else
  			_gdt = gdt;
  	} catch (e) {
  		_gdt = gdt;
  	}
  	return this.who(groupSysId, _gdt, false, rotaSysIds, false, taskGr);
  },

  /**
   * This is used by the escalation business rules to determine who in the rota do we contact next. The escalation
   * record passed to the method tracks the escalation between invocations of the business rule.
   *
   * 1. If we are still within the rotation schedule for the current rota, find the next member in the rota to contact
   *
   * 2. If there are no more members, create an escalation that fires when the next rota begins its schedule, unless
   * this is the first rota that we started with, in which case we have tried everyone, so give up
   *
   * escalation: [GlideRecord] the escalation record
   */
  whoIsNext: function(escalationGr) {
  	this._groupSysId = escalationGr.group + "";

  	// Load the rota notifications from the escalation
  	this._escalationList.fromXml(escalationGr.escalation_document + "");
  	this._currentEscalation = null;
  	this._rotaSysIds = this._escalationList.getRotaIDs();
  	var taskGr = this._getTaskGrFromEscalation(escalationGr);
  	return !this.isEmpty() || this._checkForMoreRotas(taskGr);
  },

  /**
   * Provide list of users that are On-call for a given time. If no time specified
   * then current time is used. If no groupSysIds are provided return empty list
   *
   * groupSysIds [String]: comma seperated list of sys_user_group sys_ids to filter by
   * rotaSysIds [String]: comma seperated list of cmn_rota sys_ids to filter records by
   * rosterSysIds [String]: comma seperated list of cmn_rota_roster sys_ids to filter records by
   * gdt [GlideDateTime]: date in UTC timezone
   *
   */
  whoIsOnCall: function(groupSysIds, rotaSysIds, rosterSysIds, gdt) {
  	groupSysIds = groupSysIds ? groupSysIds + "" : "";
  	rotaSysIds = rotaSysIds ? rotaSysIds + "" : "";
  	rosterSysIds = rosterSysIds ? rosterSysIds + "" : "";

  	if (!groupSysIds)
  		if (rotaSysIds)
  			groupSysIds = this._getGroupIdByRota(rotaSysIds);
  		else if (rosterSysIds)
  			groupSysIds = this._getGroupIdByRoster(rosterSysIds);

  	gdt = gdt || new GlideDateTime();
  	return this.getEscalatees(groupSysIds, rotaSysIds, rosterSysIds, gdt, true);
  },

  isBrowserSupported: function(scopeName) {
  	if (this._isGlobalScope())
  		return !new GlideCollaborationCompatibility().isIncompatible();
  	else
  		return !new GlideUICompatibility(scopeName || OnCallRotationSNC.SCOPE_NAME).isBlocked();
  },

  getBrowserCompatibility: function(scopeName) {
  	if (this._isGlobalScope())
  		return new GlideCollaborationCompatibility().getCompatibility();
  	else
  		return new GlideUICompatibility(scopeName || OnCallRotationSNC.SCOPE_NAME).getCompatibility();
  },

  getRotaMembersByUserAndGroupURL: function(userSysId, groupSysId) {
  	var url = "";
  	url = this._setPageAndPrepare(OnCallRotationSNC.CMN_ROTA_MEMBER_LIST, url);
  	if (!userSysId || !groupSysId)
  		return url;
  	url += "sysparm_query=memberIN" + userSysId + "^roster.rota.groupIN" + groupSysId;
  	return url;
  },

  /**
   * Check if the child span(s) overlap with the parent span(s)
   *
   * parent [array]
   * child [array]
   * timeZone [String]
   * return [boolean] overlap
   */
  spansOverlap: function(parent, child, timeZone) {
  	var parentMap = new GlideScheduleTimeMap();
  	parentMap.addIncludeSpans(parent);
  	parentMap.buildMap(timeZone);

  	var childMap = new GlideScheduleTimeMap();
  	childMap.addIncludeSpans(child);
  	childMap.buildMap(timeZone);

  	var overlap = parentMap.overlapsWith(childMap, timeZone);
  	overlap.buildMap(timeZone);

  	return !overlap.isEmpty();
  },

  isWizardRunning: function() {
  	return GlideSession.get().getBooleanProperty(OnCallRotationSNC.PROPERTY_IS_WIZARD, false);
  },

  toString: function() {
  	return this.type;
  },

  // PRIVATE METHODS

  /**
   * Returns an array with size and get functions to preserve original behaviour
   * of the Java List collection.
   *
   */
  _initEscalationPlan: function() {
  	var escalationPlan = [];
  	escalationPlan.get = function (i) { return this[i]; };
  	escalationPlan.size = function () { return this.length; };
  	return escalationPlan;
  },

  /**
   * Sets a timezone on the sys_choice table to active. Used before adding a timezone to a new schedule
   *
   * timezone [string]
   */
  _activateTimezone: function(timezone) {
  	var timeZoneChoiceGr = new GlideRecord("sys_choice");
  	timeZoneChoiceGr.addQuery("name", "sys_user");
  	timeZoneChoiceGr.addQuery("element", "time_zone");
  	timeZoneChoiceGr.addQuery("value", timezone);
  	timeZoneChoiceGr.query();

  	while (timeZoneChoiceGr.next()) {
  		if (timeZoneChoiceGr.inactive + "" === "true") {
  			timeZoneChoiceGr.setValue("inactive", false);
  			timeZoneChoiceGr.update();
  		}
  	}
  },

  /**
   * Add an Escalatee to the Escalation Lineup.
   *
   * order [number]
   * userId [string]
   * deviceId [string]
   * isDevice [boolean]
   * timeBetweenReminders [GlideDuration]
   * reminderNum [number]
   * rosterId [string]
   * memberId [string]
   * isOverride [boolean]
   */
  _addToEscalationLineUp: function (userId, deviceId, isDevice, timeBetweenReminders, reminderNum, rosterId, memberId, isOverride, forcedCommunicationChannel, overrideUserContactPreference, escalationType) {
  	this._escalationLevel++;
  	this._escalatees.push(new Escalatee({
  		order: this._escalationLevel,
  		userId: userId,
  		deviceId: deviceId,
  		isDevice: isDevice,
  		timeBetweenReminders: timeBetweenReminders,
  		reminderNum: reminderNum,
  		rosterId: rosterId,
  		memberId: memberId,
  		isOverride: isOverride,
  		forcedCommunicationChannel: forcedCommunicationChannel,
  		overrideUserContactPreference: overrideUserContactPreference,
  		escalationType: escalationType
  	}));
  },

  _addCustomEscalateeToEscalationLineUp: function (userIds, groupIds, deviceIds, memberIds, timeBetweenReminders, reminderNum, timeToNextStep, cmnRotaEscStepDefId, rosterId, forcedCommunicationChannel, escalationLevel, overrideUserContactPreference, rotaId, isGroupEscalation, rosterIds) {
  	this._escalatees.push(new Escalatee({
  		order: escalationLevel,
  		userIds: userIds,
  		escalationGroups: groupIds,
  		deviceIds: deviceIds,
  		timeBetweenReminders: timeBetweenReminders,
  		reminderNum: reminderNum,
  		timeToNextStep: timeToNextStep,
  		memberIds: memberIds,
  		cmnRotaEscStepDefId: cmnRotaEscStepDefId,
  		rosterId: rosterId,
  		rosterIds: rosterIds,
  		forcedCommunicationChannel: forcedCommunicationChannel,
  		overrideUserContactPreference: overrideUserContactPreference,
  		escalationType: this.ESCALATION_TYPE.CUSTOM,
  		rotaId: rotaId,
  		isGroupEscalation: isGroupEscalation
  	}));
  },

  _addAdditionalEscalateeToEscalationLineUp: function (userId, deviceId, isDevice, timeBetweenReminders, reminderNum, rosterId, memberId, isOverride, forcedCommunicationChannel, overrideUserContactPreference, escalationType) {
  	this._escalationLevel++;
  	if (this._escalatees.length < this._escalationLevel) {
  		this._escalatees.push(new Escalatee());
  	}
  	this._escalatees[this._escalationLevel-1].addAdditionalEscalatee(new Escalatee({
  		order: this._escalationLevel,
  		userId: userId,
  		deviceId: deviceId,
  		isDevice: isDevice,
  		timeBetweenReminders: timeBetweenReminders,
  		reminderNum: reminderNum,
  		rosterId: rosterId,
  		memberId: memberId,
  		isOverride: isOverride,
  		forcedCommunicationChannel: forcedCommunicationChannel,
  		overrideUserContactPreference: overrideUserContactPreference,
  		escalationType: escalationType
  	}));
  },

  _addAdditionalCustomEscalateeToEscalationLineUp: function (userIds, groupIds, deviceIds, memberIds, timeBetweenReminders, reminderNum, timeToNextStep, cmnRotaEscStepDefId, rosterId, forcedCommunicationChannel, escalationLevel, overrideUserContactPreference, rotaId, isGroupEscalation, rosterIds) {
  	if (this._escalatees.length < escalationLevel) {
  		this._escalatees.push(new Escalatee());
  	}
  	this._escalatees[escalationLevel-1].addAdditionalEscalatee(new Escalatee({
  		order: escalationLevel,
  		userIds: userIds,
  		escalationGroups: groupIds,
  		deviceIds: deviceIds,
  		timeBetweenReminders: timeBetweenReminders,
  		reminderNum: reminderNum,
  		timeToNextStep: timeToNextStep,
  		memberIds: memberIds,
  		cmnRotaEscStepDefId: cmnRotaEscStepDefId,
  		rosterId: rosterId,
  		rosterIds: rosterIds,
  		forcedCommunicationChannel: forcedCommunicationChannel,
  		overrideUserContactPreference: overrideUserContactPreference,
  		escalationType: this.ESCALATION_TYPE.CUSTOM,
  		rotaId: rotaId,
  		isGroupEscalation: isGroupEscalation
  	}));
  },

  simulateEscalationToAllRotas: function(simulateEscalationToAllRotasFlag){
  	this._simulateEscalationToAllRotas = simulateEscalationToAllRotasFlag;
  },
  /**
   * Create an escalation list for escalation of type 'rotate through rosters'
   *
   * roster - GlideRecord of the roster for the specified Rota
   * time - GlideDateTime the time point for which to create the escalation for
   * nullifyOverrideRoster
   */
  _addDutyEscalations: function(rosterGr, gdt, nullifyOverrideRoster, isAdditionalRoster) {
  	gdt = gdt || new GlideDateTime();
  	nullifyOverrideRoster = nullifyOverrideRoster + "" === "true";
  	var totalDelay = 0;

  	// iterate over all active rosters associated to our rota so we can get the on call person from each roster
  	while (rosterGr.next()) {
  		var attempts = parseInt(rosterGr.attempts + "");
  		attempts = isNaN(attempts) ? 1 : attempts + 1;
  		var reminderDuration = rosterGr.time_between_reminders.getGlideObject();
  		var reminderDelay = reminderDuration ? (reminderDuration.getNumericValue() / 1000) : 0;
  		var rosterSysId = rosterGr.sys_id + "";
  		var memberGr = new GlideRecord("cmn_rota_member");
  		var workTimeGdt = new GlideDateTime(gdt);

  		// From and To dates does not have time factor so add +1/-1 day respectively
  		var fromDateLimit = new GlideDateTime(gdt);
  		var toDateLimit = new GlideDateTime(gdt);
  		fromDateLimit.addDaysUTC(1); // Due to the logged in user TZ, provided date may become less than From date
  		toDateLimit.addDaysUTC(-1); // Due to the logged in user TZ, provided date may become greater than To date

  		memberGr.addEncodedQuery("from=NULL^ORfrom<=" + fromDateLimit.getDate().getValue());
  		memberGr.addEncodedQuery("to=NULL^ORto>=" + toDateLimit.getDate().getValue());

  		if (this._log.atLevel(GSLog.DEBUG))
  			this._log.debug("[_addDutyEscalations] reminderDelay: " + reminderDelay + " totalDelay: " + totalDelay + " reminderDuration: " + reminderDuration);

  		// check if the rota member is on call, note that this method will - not return a rotation member, but
  		// a boolean _if_ a member is on call - modify member by reference where the member will be the member
  		// on call - use the rosterID to query the member
  		var isMemberAvailable = this._getRotationMember(memberGr, workTimeGdt, rosterSysId);
  		// member is on call, check if another member has an override schedule for this time making them first
  		// contact for the rotation instead of member within same roster
  		var userSysId = this._checkForOverrideMemberByRoster(workTimeGdt, rosterGr.sys_id + "");

  		if (!userSysId)
  			userSysId = this._checkForOverrideMember(workTimeGdt);

  		var isOverride = false;

  		if (!userSysId && isMemberAvailable)
  			userSysId = memberGr.member + "";
  		else {
  			if (nullifyOverrideRoster)
  				rosterSysId = null;
  			isOverride = true;
  		}

  		if (!userSysId) {
  			this._log.error("No valid sys_user sys_id for cmn_rota_member: " + memberGr.sys_id);
  			continue;
  		}

  		if (this._isTheUserOff(userSysId, workTimeGdt))
  			continue;

  		if (!isAdditionalRoster) {
  			if (!this._escalationList.getPrimaryUserID())
  				this._escalationList.setPrimaryUserID(userSysId, this._rotaSysIds[this._rotaSysIds.length - 1], rosterSysId);
  		} else if (!this._escalationList.getPrimaryUserIdByRota(this._rotaSysIds[this._rotaSysIds.length - 1])) {
  			this._escalationList.addPrimaryUserID(userSysId, this._rotaSysIds[this._rotaSysIds.length - 1], rosterSysId);
  		}

  		this._addRotationEscalation(memberGr, userSysId, gdt, totalDelay, rosterSysId);

  		var forcedCommunicationChannel = rosterGr.getValue('forced_communication_channel');
  		var overrideUserContactPreference = rosterGr.rota.override_user_contact_preference + "";

  		if (this._log.atLevel(GSLog.DEBUG))
  			this._log.logDebug('[_addDutyEscalations] overrideUserContactPreference: ' + overrideUserContactPreference);

  		// Add a new Escalatee to the Lineup for the notification type = 'Rotate through rosters'
  		if (!isAdditionalRoster)
  			this._addToEscalationLineUp(userSysId, null, false, new GlideDuration(reminderDuration), (attempts - 1), rosterSysId, memberGr.sys_id + "", isOverride, forcedCommunicationChannel, overrideUserContactPreference, this.ESCALATION_TYPE.ROTATE_THROUGH_ROSTER);
  		else
  			this._addAdditionalEscalateeToEscalationLineUp(userSysId, null, false, new GlideDuration(reminderDuration), (attempts - 1), rosterSysId, memberGr.sys_id + "", isOverride, forcedCommunicationChannel, overrideUserContactPreference, this.ESCALATION_TYPE.ROTATE_THROUGH_ROSTER);

  		if (this._isMaxEntries())
  			return;

  		// repeat the first user if multiple attempts are requested
  		var attemptsLeft = attempts - 1;

  		while (attemptsLeft > 0) {
  			totalDelay += reminderDelay;

  			// add a delayed escalation record for this member
  			this._addRotationEscalation(memberGr, userSysId, gdt, totalDelay, rosterSysId);
  			attemptsLeft--;
  		}

  		// add delay before next roster is processed
  		totalDelay += reminderDelay;
  	}
  },

  /**
   * add rotation escalation
   *
   * memberGr [GlildeRecord] rota member record
   * userID [string] sys_user id of this rota member (why pass this if it is already in memberGR?)
   * gdt [GlideDateTime] the time point to add an escalation for
   * delay [number] the delay in minutes
   * rosterID [string] the sys_id of the roster
   */
  _addRotationEscalation: function(memberGr, userSysId, gdt, delay, rosterSysId) {
  	var escalationEntry = new OnCallEscalationEntry();
  	escalationEntry.escalationType = "rotation";
  	escalationEntry.rotaSysId = this._rotaSysIds[this._rotaSysIds.length - 1];
  	escalationEntry.rosterSysId = rosterSysId;
  	escalationEntry.memberSysId = memberGr.sys_id + "";
  	escalationEntry.setUserID(userSysId);
  	escalationEntry.setDelay(gdt, delay);
  	this._escalationList.add(escalationEntry);
  },

  /**
   * add custom escalation
   *
   * type escalation type (user, group, device)
   * escalationId [string] id of escalation record. Escalation record could be user, group or device based on type
   * gdt [GlideDateTime] the time point to add an escalation for
   * delay [number] the delay in minutes
   */
  _addCustomEscalation: function(type, escalationId, gdt, delay) {
  	var escalationEntry = new OnCallEscalationEntry();
  	escalationEntry.escalationType = type;
  	escalationEntry.rotaSysId = this._rotaSysIds[this._rotaSysIds.length - 1];
  	escalationEntry.setDelay(gdt, delay);
  	if(type == 'user')
  		escalationEntry.setUserID(escalationId);
  	else if(type == 'group')
  		escalationEntry.setGroupId(escalationId);
  	else if(type == 'device')
  		escalationEntry.setDeviceID(escalationId);
  	else
  		return; //Invalid type
  	this._escalationList.add(escalationEntry);
  },

  _addTime: function(gd, gt) {
  	if (!gd || !gt) {
  		this._log.error("[_addTime] glideDate: " + gd + " glideTime: " + gt);
  		return "";
  	}

  	var gdt = new GlideDateTime(gd);
  	gdt.add(gt);
  	var dateTimeStr = gdt.getValue() + "";
  	dateTimeStr = dateTimeStr.replace(/-/g, "");
  	dateTimeStr = dateTimeStr.replace(/:/g, "");
  	var dateTimeArr = dateTimeStr.split(" ");
  	dateTimeStr = dateTimeArr[0] + "T" + dateTimeArr[1];

  	if (this._log.atLevel(GSLog.DEBUG))
  		this._log.debug("[_addTime] dateTimeStr: " + dateTimeStr);

  	return dateTimeStr;
  },

  /**
   * Create an escalation list for escalation of type 'rotate through members'
   *
   * rosterGr [GlideRecord] of type cmn_rota_roster
   * gdt [GlideDateTime] the time point for which to create the escalation for
   * nullifyOverrideRoster
   * rotaEscalationDefinitionGr - Will be utilized when RTM flag is true for custom escalation
   */
  _addRotationEscalations: function(rosterGr, gdt, nullifyOverrideRoster, isAdditionalRoster, rotaEscalationDefinitionGr) {
  	nullifyOverrideRoster = nullifyOverrideRoster + "" === "true";
  	var escalationType = this.ESCALATION_TYPE.ROTATE_THROUGH_MEMBER;
  	var rosterSysId = rosterGr.sys_id + ""; 
  	var timeToNextStep;
  	var isGroupEscalation = false;
  	var reminderDuration = rosterGr.time_between_reminders.getGlideObject();
  	if (rotaEscalationDefinitionGr) {
  		escalationType = this.ESCALATION_TYPE.CUSTOM;
  		timeToNextStep = rotaEscalationDefinitionGr.time_to_next_step.getGlideObject()
  		isGroupEscalation = rotaEscalationDefinitionGr.escalation_set.group_escalation ? true : false;
  		var CustomStepReminderDuration = rotaEscalationDefinitionGr.time_between_reminders.getGlideObject().getNumericValue();
  		if (CustomStepReminderDuration) {
  			reminderDuration = rotaEscalationDefinitionGr.time_between_reminders.getGlideObject();
  		} else {
  			reminderDuration = timeToNextStep;
  		}
  	}
  	var reminderDelay = reminderDuration ? (reminderDuration.getNumericValue() / 1000) : 0;
  	var totalDelay = this._delayTillPreviousStep ? this._delayTillPreviousStep : 0;
  	var attempts = rotaEscalationDefinitionGr ? parseInt(rotaEscalationDefinitionGr.reminders) : parseInt(rosterGr.attempts + "");
  	attempts = isNaN(attempts) ? 1 : attempts + 1;
  	var attemptsLeft;
  	var wrktimeGdt = new GlideDateTime(gdt);
  	var memberGr = new GlideRecord("cmn_rota_member");

  	// From and To dates does not have time factor so add +1/-1 day respectively
  	var fromDateLimit = new GlideDateTime(gdt);
  	var toDateLimit = new GlideDateTime(gdt);
  	fromDateLimit.addDaysUTC(1); // Due to the logged in user TZ, provided date may become less than From date
  	toDateLimit.addDaysUTC(-1); // Due to the logged in user TZ, provided date may become greater than To date

  	memberGr.addEncodedQuery("from=NULL^ORfrom<=" + fromDateLimit.getDate().getValue());
  	memberGr.addEncodedQuery("to=NULL^ORto>=" + toDateLimit.getDate().getValue());

  	if (this._log.atLevel(GSLog.DEBUG))
  		this._log.debug("[_addRotationEscalations] reminderDelay: " + reminderDelay + " totalDelay: " + totalDelay + " reminderDuration: " + reminderDuration);

  	var isMemberAvailable = this._getRotationMember(memberGr, wrktimeGdt, rosterGr.sys_id + "");
  	var memberSysId = "";
  	if (memberGr) {
  		memberSysId = memberGr.sys_id + "";
  	}
  	var userSysId = this._checkForOverrideMemberByRoster(wrktimeGdt, rosterGr.sys_id + "");
  	var forcedCommunicationChannel = rosterGr.getValue('forced_communication_channel');
  	var overrideUserContactPreference = rosterGr.rota.override_user_contact_preference + "";

  	if (this._log.atLevel(GSLog.DEBUG))
  		this._log.logDebug('[_addRotationEscalations] overrideUserContactPreference: ' + overrideUserContactPreference);

  	if (!userSysId)
  		userSysId = this._checkForOverrideMember(wrktimeGdt);

  	var isOverride = false;

  	if (!userSysId && isMemberAvailable)
  		userSysId = memberGr.member + "";
  	else {
  		if (nullifyOverrideRoster)
  			rosterSysId = null;
  		isOverride = true;
  	}

  	if (!userSysId) {
  		this._log.error("No valid sys_user sys_id for cmn_rota_member: " + memberSysId);
  		return;
  	}

  	var isPrimaryMemberTimeOff = this._isTheUserOff(userSysId, wrktimeGdt);
  	// check if the person is off
  	if (!isPrimaryMemberTimeOff) {
  		if (!isAdditionalRoster) {
  			this._escalationList.setPrimaryUserID(userSysId, this._rotaSysIds[this._rotaSysIds.length - 1], rosterSysId);
  		} else if (!this._escalationList.getPrimaryUserIdByRota(this._rotaSysIds[this._rotaSysIds.length - 1])) {
  			this._escalationList.addPrimaryUserID(userSysId, this._rotaSysIds[this._rotaSysIds.length - 1], rosterSysId);
  		}
  		this._addRotationEscalation(memberGr, userSysId, gdt, totalDelay, rosterSysId);

  		// Add the first Escalatee to the Lineup (for the notification type = 'Rotate through members')
  		if (!isAdditionalRoster) {
  			if (!rotaEscalationDefinitionGr)
  				this._addToEscalationLineUp(userSysId, null, false, new GlideDuration(reminderDuration), (attempts - 1), rosterSysId, memberGr.sys_id + "", isOverride, forcedCommunicationChannel, overrideUserContactPreference, escalationType);
  			else
  				this._addCustomEscalateeToEscalationLineUp([userSysId], null, null, null, new GlideDuration(reminderDuration), (attempts - 1), new GlideDuration(timeToNextStep), rotaEscalationDefinitionGr.getValue('sys_id'), rosterSysId, forcedCommunicationChannel, this._escalationLevel + 1, overrideUserContactPreference, rosterGr.rota + "", isGroupEscalation, [rosterSysId]);
  		} else {
  			if (!rotaEscalationDefinitionGr)
  				this._addAdditionalEscalateeToEscalationLineUp(userSysId, null, false, new GlideDuration(reminderDuration), (attempts - 1), rosterSysId, memberGr.sys_id + "", isOverride, forcedCommunicationChannel, overrideUserContactPreference, escalationType);
  			else
  				this._addAdditionalCustomEscalateeToEscalationLineUp([userSysId], null, null, null, new GlideDuration(reminderDuration), (attempts - 1), new GlideDuration(timeToNextStep), rotaEscalationDefinitionGr.getValue('sys_id'), rosterSysId, forcedCommunicationChannel, this._escalationLevel + 1, overrideUserContactPreference, rosterGr.rota + "", isGroupEscalation, [rosterSysId]);
  		}

  		if (this._isMaxEntries())
  			return;

  		// repeat the first user if multiple attempts are requested
  		attemptsLeft = attempts - 1;
  		while (attemptsLeft > 0) {
  			totalDelay += reminderDelay;
  			this._addRotationEscalation(memberGr, userSysId, gdt, totalDelay, rosterSysId);
  			attemptsLeft--;
  		}
  	}

  	// And add in the rest of the roster rotation members
  	var memberCnt = 1;
  	var done = false;
  	while (!done && isMemberAvailable) {
  		if (memberCnt > 100)
  			// Only put a max of 100 members in the notification list
  			done = true;

  		if (!done) {
  			if (!memberGr.next()) {
  				memberGr.setLocation(-1);
  				memberGr.next();
  			}

  			if (memberGr.sys_id + "" === memberSysId)
  				done = true;
  			else {
  				userSysId = memberGr.member + "";

  				if (this._isTheUserOff(userSysId, wrktimeGdt))
  					continue;

  				// Add more Escalatees to the Lineup for the notification type = 'Rotate through members'
  				if (!isAdditionalRoster) {
  					if (!rotaEscalationDefinitionGr)
  						this._addToEscalationLineUp(userSysId, null, false, new GlideDuration(reminderDuration), (attempts - 1), rosterSysId, memberGr.sys_id + "", isOverride, forcedCommunicationChannel, overrideUserContactPreference, escalationType);
  					else
  						this._addCustomEscalateeToEscalationLineUp([userSysId], null, null, null, new GlideDuration(reminderDuration), (attempts - 1), new GlideDuration(timeToNextStep), rotaEscalationDefinitionGr.getValue('sys_id'), rosterSysId, forcedCommunicationChannel, this._escalationLevel + 1, overrideUserContactPreference, rosterGr.rota + "", isGroupEscalation, [rosterSysId]);
  				}
  				else {
  					if (!rotaEscalationDefinitionGr)
  						this._addAdditionalEscalateeToEscalationLineUp(userSysId, null, false, new GlideDuration(reminderDuration), (attempts - 1), rosterSysId, memberGr.sys_id + "", isOverride, forcedCommunicationChannel, overrideUserContactPreference, escalationType);
  					else
  						this._addAdditionalCustomEscalateeToEscalationLineUp([userSysId], null, null, null, new GlideDuration(reminderDuration), (attempts - 1), new GlideDuration(timeToNextStep), rotaEscalationDefinitionGr.getValue('sys_id'), rosterSysId, forcedCommunicationChannel, this._escalationLevel + 1, overrideUserContactPreference, rosterGr.rota + "", isGroupEscalation, [rosterSysId]);		
  				}

  				if(isPrimaryMemberTimeOff) {
  					totalDelay -= reminderDelay;
  					isPrimaryMemberTimeOff = false;
  				}
  				attemptsLeft = attempts;
  				while (attemptsLeft > 0) {
  					totalDelay += reminderDelay;
  					this._addRotationEscalation(memberGr, userSysId, gdt, totalDelay, rosterGr.sys_id + "");
  					attemptsLeft--;
  				}
  			}
  			memberCnt++;
  		}
  	}
  },

  _checkForMoreRotas: function(taskGr) {
  	var that = this;
  	this._rotaSysIds.forEach(function(rotaSysId) {
  		if (!that._escalationList.isInRotaList(rotaSysId)) {
  			that._escalationList.addRotaToList(rotaSysId);
  		}
  	});
  	var time = new GlideDateTime();
  	if (this._onCallCommon.isOverlapAllowed(this._groupSysId)) {
  		var rotas = this._getAllRotaWithOverlappingSchedule(time);
  		if (rotas !== -1 && rotas.length > 0) {
  			var escalationSettings = this._onCallCommon.getEscalationSettings(this._groupSysId);
  			if (escalationSettings == this._onCallCommon.escalation_rule_rota_overlap.START) {
  				rotas = [this._findFirstStartedRota(rotas)];
  			} else if (escalationSettings == this._onCallCommon.escalation_rule_rota_overlap.END) {
  				rotas = [this._findEndingLastRota(rotas)];
  			}
  			for (var i = 0; i < rotas.length; i++) {
  				if (!this._escalationList.isInRotaList(rotas[i].rotaSysId)) {
  					this._rotaSysIds.push(rotas[i].rotaSysId);
  					if(rotas[i].rotaCustomEscalation) {
  						this._getCustomEscalation(rotas[i].rotaSysId, time, true, taskGr);
  					} else {
  						this._getRotaNotificationsByTimeAndRota(time, rotas[i].rotaSysId, true, true);
  					}
  				}
  			}
  		}
  	} else {
  		this._firstDelay = this._getFirstDelay(time);
  		if (this._rotaSysIds[this._rotaSysIds.length - 1]) {
  			if(this._rotaCustomEscalation) {
  				this._getCustomEscalation(this._rotaSysIds[this._rotaSysIds.length - 1], time, true, taskGr);
  			} else {
  				time.addSeconds(this._firstDelay);
  				this._getRotaNotificationsByTime(time, true);
  			}
  		}
  	}
  	var moreRotas = !this.isEmpty();
  	if (this._log.atLevel(GSLog.DEBUG))
  		this._log.debug("[_checkForMoreRotas] moreRotas: " + moreRotas);

  	return moreRotas;
  },

  _getUserScheduleByGroupId: function(groupId) {
  	var userSchedules = {};
  	var sysUserGr = new GlideRecord("sys_user");
  	sysUserGr.initialize();
  	groupId = groupId || "";
  	if (!groupId)
  		return userSchedules;
  	sysUserGr.addQuery("JOINsys_user.sys_id=sys_user_grmember.user!group=" + groupId);
  	sysUserGr.query();

  	if (this._log.atLevel(GSLog.DEBUG))
  		this._log.debug("[_getUserScheduleByGroupId] table: " + sysUserGr.getTableName() + " encodedQuery: " + sysUserGr.getEncodedQuery());

  	while (sysUserGr.next()) {
  		var userSysId = sysUserGr.sys_id + "";
  		var userScheduleSysId = sysUserGr.schedule + "";
  		if (userSysId && userScheduleSysId)
  			userSchedules[userSysId] = userScheduleSysId;
  	}
  	return userSchedules;
  },

  /**
   * Get the sys_id of a member that has an override specified for the time we are looking for (or null if none)
   *
   * gdt [GlideDateTime] the time point to look for
   * return [string] sys_id of the user (or null)
   */
  _checkForOverrideMember: function(gdt) {
  	gdt = gdt ? gdt : new GlideDateTime();
  	var dateStr = gdt.getDate() + "";
  	var userSchedules = this._getUserScheduleByGroupId(this._groupSysId);
  	var userSysIds = Object.keys(userSchedules);
  	var userSysIdsLength = userSysIds.length;
  	var userScheduleIds = [];
  	for (var i = 0; i < userSysIdsLength; i++) {
  		userScheduleIds.push(userSchedules[userSysIds[i]]);
  	}
  	var cmnScheduleTimezones = {};
  	var cmnScheduleGr = new GlideRecord('cmn_schedule');
  	cmnScheduleGr.addQuery('sys_id', 'IN', userScheduleIds);
  	cmnScheduleGr.query();
  	while (cmnScheduleGr.next()) {
  		cmnScheduleTimezones[cmnScheduleGr.sys_id] = cmnScheduleGr.time_zone + '';
  	}

  	var rosterScheduleSpanGr = new GlideRecord("roster_schedule_span");
  	rosterScheduleSpanGr.initialize();
  	var encQuery = "scheduleIN" + userScheduleIds + "^type=on_call^group=" + this._groupSysId + "^roster=NULL";
  	rosterScheduleSpanGr.addEncodedQuery(this._onCallCommon.getDateLimitedEncQuery(encQuery, dateStr, dateStr));
  	rosterScheduleSpanGr.query();

  	if (this._log.atLevel(GSLog.DEBUG))
  	        this._log.debug("[_checkForOverrideMember] table: " + rosterScheduleSpanGr.getTableName() + " encodedQuery: " + rosterScheduleSpanGr.getEncodedQuery());

  	var rosterScheduleIdToSysIdsMap = {};
  	while (rosterScheduleSpanGr.next()) {
  		var scheduleId = rosterScheduleSpanGr.schedule;
  		if (!rosters[scheduleId]) rosterScheduleIdToSysIdsMap[scheduleId] = [];
  		rosterScheduleIdToSysIdsMap[scheduleId].push(rosterScheduleSpanGr.getValue('sys_id'));
  	}

  	for (var i = 0; i < userSysIdsLength; i++) {
  		var userSysId = userSysIds[i];
  		var userScheduleId = userSchedules[userSysId];
  		if (!userScheduleId || !cmnScheduleTimezones[userScheduleId] || !rosterScheduleIdToSysIdsMap[userScheduleId])
  		     continue;

  		var rosterScheduleSpan = new GlideRecord("roster_schedule_span");
  		rosterScheduleSpan.addQuery("sys_id", "IN", rosterScheduleIdToSysIdsMap[userScheduleId]);
  		rosterScheduleSpan.query();

  		if (this._log.atLevel(GSLog.DEBUG))
  		        this._log.debug("[_checkForOverrideMember] table: " + rosterScheduleSpan.getTableName() + " sys_ids: " + rosterScheduleIdToSysIdsMap[userScheduleId]);

  		var overrideSchedule = new GlideSchedule();
  		overrideSchedule.setTimeZone(cmnScheduleTimezones[userScheduleId] + '');
  		overrideSchedule.addTimeSpans(rosterScheduleSpan);

  		if (overrideSchedule.isValid() && overrideSchedule.isInSchedule(gdt)) {
  			return userSysId;
  		}
  	}
  	return null;
  },

  _clearEscalationLineUp: function() {
  	this._escalatees = this._initEscalationPlan();
  	this._escalationLevel = 0;
  },

  /**
   * Get the sys_id of a member that has an override specified for the time we are looking for (or null if none)
   *
   * gdt [GlideDateTime] the time point to look for
   * rosterSysId [String]: roster to search in
   * return [string] sys_id of the user (or null)
   */
  _checkForOverrideMemberByRoster: function(gdt, rosterSysId) {
              gdt = gdt ? gdt : new GlideDateTime();
              var dateStr = gdt.getDate() + "";
              var userSchedules = this._getUserScheduleByGroupId(this._groupSysId);
              var userSysIds = Object.keys(userSchedules);
  	var userSysIdsLength = userSysIds.length;
  	var userScheduleIds = [];
  	for (var i = 0; i < userSysIdsLength; i++) {
  		userScheduleIds.push(userSchedules[userSysIds[i]]);
  	}
              var cmnScheduleTimezones = {};
              var cmnScheduleGr = new GlideRecord('cmn_schedule');
              cmnScheduleGr.addQuery('sys_id', 'IN', userScheduleIds);
              cmnScheduleGr.query();
              while (cmnScheduleGr.next()) {
                      cmnScheduleTimezones[cmnScheduleGr.sys_id] = cmnScheduleGr.time_zone + '';
              }

              var rosterScheduleSpanGr = new GlideRecord("roster_schedule_span");
              var encQuery = "scheduleIN" + userScheduleIds + "^type=on_call^group=" + this._groupSysId + "^roster=" + rosterSysId;
              rosterScheduleSpanGr.addEncodedQuery(this._onCallCommon.getDateLimitedEncQuery(encQuery, dateStr, dateStr));
              rosterScheduleSpanGr.query();

  	if (this._log.atLevel(GSLog.DEBUG))
  	        this._log.debug("[_checkForOverrideMemberByRoster] table: " + rosterScheduleSpanGr.getTableName() + " encodedQuery: " + rosterScheduleSpanGr.getEncodedQuery());

  	var rosterScheduleIdToSysIdsMap = {};
              while (rosterScheduleSpanGr.next()) {
  		var scheduleId = rosterScheduleSpanGr.schedule;
  		if (!rosterScheduleIdToSysIdsMap[scheduleId]) rosterScheduleIdToSysIdsMap[scheduleId] = [];
  		rosterScheduleIdToSysIdsMap[scheduleId].push(rosterScheduleSpanGr.getValue('sys_id'));
  	}

  	for (var i = 0; i < userSysIdsLength; i++) {
  		var userSysId = userSysIds[i];
  		var userScheduleId = userSchedules[userSysId];
  		if (!userScheduleId || !cmnScheduleTimezones[userScheduleId] || !rosterScheduleIdToSysIdsMap[userScheduleId]) continue;

  		var rosterScheduleSpan = new GlideRecord("roster_schedule_span");
  		rosterScheduleSpan.addQuery("sys_id", "IN", rosterScheduleIdToSysIdsMap[userScheduleId]);
  		rosterScheduleSpan.query();

  		if (this._log.atLevel(GSLog.DEBUG))
  		        this._log.debug("[_checkForOverrideMemberByRoster] table: " + rosterScheduleSpan.getTableName() + " sys_ids: " + rosterScheduleIdToSysIdsMap[userScheduleId]);

                      var overrideSchedule = new GlideSchedule();
                      overrideSchedule.setTimeZone(cmnScheduleTimezones[userScheduleId] + '');
                      overrideSchedule.addTimeSpans(rosterScheduleSpan);
                      if (overrideSchedule.isValid() && overrideSchedule.isInSchedule(gdt)) {
                              return userSysId;
                      }
          }
              return null;
      },

  _createEscalationRecord: function(gr, eventName, escalationScriptName) {
  	var escalationGr = new GlideRecord("cmn_rota_escalation");
  	escalationGr.setValue("table", gr.getTableName());
  	escalationGr.setValue("instance", gr.sys_id + "");
  	escalationGr.setValue("group", this._groupSysId);
  	escalationGr.setValue("name", "Rota-" + gr.getDisplayValue());
  	escalationGr.setValue("event_name", eventName);
  	escalationGr.setValue("escalation_document", this.getFullEscalationDocument());
  	escalationGr.setValue("script_name", escalationScriptName);
  	var escalationSysId = escalationGr.insert();

  	if (this._log.atLevel(GSLog.DEBUG))
  		this._log.debug("[_createEscalationRecord] escalationSysId: " + escalationSysId);

  	return escalationGr;
  },

  _createNewRota: function(rotaName, groupSysId, isDraft) {
  	if (!rotaName || !groupSysId)
  		return null;

  	var rotaGr = new GlideRecord("cmn_rota");
  	rotaGr.setValue("group", groupSysId);
  	rotaGr.setValue("name", rotaName);
  	var groupGr = new GlideRecord("sys_user_group");
  	if (!groupGr.get(groupSysId))
  		return null;

  	var groupDomain = groupGr.sys_domain + "";
  	rotaGr.setValue("sys_domain", groupDomain);
  	if (isDraft) {
  		rotaGr.setValue("state", "draft");
  		rotaGr.setValue("active", false);
  	}
  	rotaGr.insert();
  	this._rotaGr = rotaGr;
  	return rotaGr;
  },

  _createNewRosterSchedule: function(timezone, scheduleName) {
  	var document = this._rotaGr.getTableName();
  	var documentKey = this._rotaGr.sys_id + "";

  	if (this._log.atLevel(GSLog.DEBUG))
  		this._log.debug("[_createNewRosterSchedule] timezone: " + timezone + " scheduleName: " + scheduleName + " document: " + document + " documentKey: " + documentKey);

  	var scheduleGr = new GlideRecord("cmn_schedule");
  	scheduleGr.setValue("name", scheduleName);
  	scheduleGr.setValue("time_zone", timezone);
  	scheduleGr.setValue("type", "roster");
  	scheduleGr.setValue("document", document);
  	scheduleGr.setValue("document_key", documentKey);
  	scheduleGr.setValue("read_only", false);
  	var scheduleSysId = scheduleGr.insert();
  	this._scheduleGr = scheduleGr;

  	if (this._log.atLevel(GSLog.DEBUG))
  		this._log.debug("[_createNewRosterSchedule] scheduleSysId: " + scheduleSysId);

  	return scheduleSysId;
  },

  /**
   * Creates a roster schedule and schedule span(s) by copying data from the given scheduleId
   *
   * existingScheduleSysId [String]
   * groupSysId [String]
   * startGdt [GlideDateTime]
   * return scheduleSysId [String]
   */
  _createRosterScheduleFromExisting: function(existingScheduleSysId, startGdt) {
  	var timezone = this._getTimeZoneBySchedule(existingScheduleSysId);
  	var rotaName = this._rotaGr.name + "";
  	var rosterScheduleGr = new GlideRecord("cmn_schedule");
  	rosterScheduleGr.setValue("name", rotaName);
  	rosterScheduleGr.setValue("type", "roster");
  	rosterScheduleGr.setValue("document", "cmn_rota");
  	rosterScheduleGr.setValue("document_key", this._rotaGr.sys_id + "");
  	rosterScheduleGr.setValue("time_zone", timezone);
  	var scheduleSysId = rosterScheduleGr.insert() + "";

  	if (this._log.atLevel(GSLog.DEBUG))
  		this._log.debug("[_createRosterScheduleFromExisting] scheduleSysId: " + scheduleSysId);

  	this._rosterScheduleGr = rosterScheduleGr;
  	var fromGr = new GlideRecord("cmn_schedule_span");
  	fromGr.addQuery("schedule", existingScheduleSysId);
  	fromGr.orderBy("parent");
  	fromGr.query();
  	var rowCount = parseInt(fromGr.getRowCount());
  	var rosterStartTimeStr;

  	var scheduleTZ = this._parseTZ(timezone);
  	startGdt.setTZ(scheduleTZ); // required, for checking isDST in rota schedule's timezone
  	while (fromGr.next()) {
  		var gdt = fromGr.start_date_time.getGlideObject().getGlideDateTime();
  		if (!fromGr.schedule.time_zone) // floating
  			rosterStartTimeStr = gdt.getTime().getByFormat("HH:mm:ss");
  		else
  			rosterStartTimeStr = gdt.getTime().getDisplayValueInternal();
  		// adjust start and end time based on DST
  		this._adjustStartEndByDST(fromGr, startGdt, scheduleTZ);
  		var rosterDates = this._getStartEndByRosterStart(fromGr.start_date_time + "", fromGr.end_date_time + "", startGdt, this._rosterScheduleGr.time_zone + "", rowCount);
  		var scheduleSpanSysId = this._createScheduleSpan(rotaName, scheduleSysId, fromGr.show_as + "", fromGr.type + "", fromGr.override_start_date + "",
  			rosterDates.start, rosterDates.end, (fromGr.all_day + "" === "true"), fromGr.repeat_until + "", fromGr.repeat_type + "", fromGr.repeat_count + "",
  			fromGr.days_of_week + "", fromGr.monthly_type + "", fromGr.yearly_type + "", fromGr.month + "", fromGr.float_day + "", fromGr.float_week + "");
  		if (this._log.atLevel(GSLog.DEBUG))
  			this._log.debug("[_createRosterScheduleFromExisting] scheduleSpanSysId: " + scheduleSpanSysId);
  	}

  	if (rowCount === 1 && fromGr.all_day + "" !== "true") {
  		this._isAllDayRoster = false;
  		this.setRosterStartTime(rosterStartTimeStr);
  	} else
  		this._isAllDayRoster = true;

  	if (this._log.atLevel(GSLog.DEBUG))
  		this._log.debug("[_createRosterScheduleFromExisting] scheduleSysId: " + scheduleSysId);

  	return scheduleSysId;
  },
  	
  _adjustStartEndByDST: function(fromGr, rotationStartGdt, scheduleTZ) {
  	var fromStartGdt = fromGr.start_date_time.getGlideObject().getGlideDateTime();
  	fromStartGdt.setTZ(scheduleTZ); // required, for checking isDST in rota schedule's timezone
  	if (fromGr.schedule.time_zone && fromStartGdt.isDST() != rotationStartGdt.isDST()) {
  		var dstOffSet = (fromStartGdt.getDSTOffset() - rotationStartGdt.getDSTOffset()) / 1000;
  		fromGr.start_date_time.getGlideObject().addSeconds(dstOffSet);
  		fromGr.end_date_time.getGlideObject().addSeconds(dstOffSet);
  	}
  },
  
  _parseTZ: function(timeZoneStr){
  	var schedule = new GlideSchedule();
  	schedule.setTimeZone(timeZoneStr);
  	return schedule.getTZ();
  },

  /**
   * Creates a schedule span with defaults
   *
   * startGdt [GlideDateTime]
   * endGdt [GlideDateTime]
   * repeats [string]
   * isAllDay [Boolean
   * groupSysId [string
   * return scheduleSpanSysId [string]
   */
  _createScheduleSpanWithDefaults: function(startGdt, endGdt, repeats, isAllDay, days) {
  	if (this._log.atLevel(GSLog.DEBUG))
  		this._log.debug("[_createScheduleSpanWithDefaults] startGdt: " + startGdt + " endGdt: " + endGdt + " repeats: " + repeats + " isAllDay: " + isAllDay);

  	this._isAllDayRoster = isAllDay;
  	var startSdt = new GlideScheduleDateTime(startGdt.getDisplayValueWithoutTZ());
  	var endSdt = new GlideScheduleDateTime(endGdt.getDisplayValueWithoutTZ());
  	var timezone = this._scheduleGr.getValue("time_zone");

  	if (isAllDay) {
  		startSdt.setBeginningOfDay();
  		endSdt.setEndOfDay();
  	} else {
  		var time = new GlideTime();
  		time.setDisplayValue(startSdt.getGlideDateTime().getTime().toString());
  		this.setRosterStartTime(time.getDisplayValueInternal());
  	}

  	var repeatType = "";
  	if (repeats !== "none")
  		repeatType = repeats;
  	if (!days)
  		days = "1234567";

  	var	rotaName = this._rotaGr.name + "";
  	var scheduleSysId = this._scheduleGr.sys_id + "";
  	var startSdtStr = this._applyTimeZone(startSdt.getValue(), timezone).getValue();
  	var endSdtStr = this._applyTimeZone(endSdt.getValue(), timezone).getValue();

  	if (this._log.atLevel(GSLog.DEBUG))
  		this._log.debug("[_createScheduleSpanWithDefaults] startSdtStr: " + startSdtStr + " endSdtStr: " + endSdtStr);

  	var scheduleSpanSysId = this._createScheduleSpan(rotaName, scheduleSysId, "on_call", "on_call", 0, startSdtStr, endSdtStr, this._isAllDayRoster, 0, repeatType, 1, days, "dom", "doy", 1, "1", "1");
  	return scheduleSpanSysId;
  },

  /**
   *
   * name [string]
   * scheduleSysId [string]
   * showAs [string]
   * type [string]
   * overrideStartDate [number]
   * startSchedDateTime [string]
   * endSchedDateTime [string]
   * isAllDay [boolean]
   * repeatUntil [number]
   * repeatType [string]
   * repeatCount [number]
   * daysOfWeek [string]
   * monthType [string]
   * yearlyType [string]
   * month [number]
   * floatDay [string]
   * floatWeek [string]
   * return scheduleSpanSysId [string]
   */
  _createScheduleSpan: function(name, scheduleSysId, showAs, type, overrideStartDate, startSchedDateTime, endSchedDateTime,
  	isAllDay, repeatUntil, repeatType, repeatCount, daysOfWeek, monthType, yearlyType, month, floatDay, floatWeek) {
  		name = name || "";
  		scheduleSysId = scheduleSysId || "";
  		showAs = showAs || "";
  		type = type || "";
  		overrideStartDate = overrideStartDate || 0;
  		startSchedDateTime = startSchedDateTime || "";
  		endSchedDateTime = endSchedDateTime || "";
  		isAllDay = isAllDay + "" === "true" ? true : false;
  		repeatUntil = repeatUntil || 0;
  		repeatType = repeatType || "";
  		repeatCount = repeatCount || 1;
  		daysOfWeek = daysOfWeek || "1234567";
  		monthType = monthType || "dom";
  		yearlyType = yearlyType || "doy";
  		month = month || 1;
  		floatDay = floatDay || "1";
  		floatWeek = floatWeek || "1";

  		if (this._log.atLevel(GSLog.DEBUG))
  			this._log.debug("[_createScheduleSpan] name: " + name + " scheduleSysId: " + scheduleSysId + " showAs: " + showAs + " type: " + type +
  			" overrideStartDate: " + overrideStartDate + " startSchedDateTime: " + startSchedDateTime + " endSchedDateTime: " + endSchedDateTime +
  			" isAllDay: " + isAllDay + " repeatUntil: " + repeatUntil + " repeatType: " + repeatType + " repeatCount: " + repeatCount +
  			" daysOfWeek: " + daysOfWeek + " monthType: " + monthType + " yearlyType: " + yearlyType + " month: " + month + " floatDay: " + floatDay +
  			" floatWeek: " + floatWeek);

  		var spanGr = new GlideRecord("cmn_schedule_span");
  		spanGr.setValue("name", name);
  		spanGr.setValue("schedule", scheduleSysId);
  		spanGr.setValue("show_as", showAs);
  		spanGr.setValue("type", type);
  		spanGr.setValue("start_date_time", startSchedDateTime + "");
  		spanGr.setValue("end_date_time", endSchedDateTime + "");
  		spanGr.setValue("all_day", isAllDay);
  		spanGr.setValue("repeat_until", repeatUntil + "");
  		spanGr.setValue("repeat_type", repeatType);
  		spanGr.setValue("repeat_count", repeatCount);
  		spanGr.setValue("days_of_week", daysOfWeek);
  		spanGr.setValue("monthly_type", monthType);
  		spanGr.setValue("yearly_type", yearlyType);
  		if (month >= 0)
  			spanGr.setValue("month", month);
  		spanGr.setValue("float_day", floatDay);
  		spanGr.setValue("float_week", floatWeek);
  		spanGr.setValue("override_start_date", overrideStartDate + "");
  		var scheduleSpanSysId = spanGr.insert();

  		if (this._log.atLevel(GSLog.DEBUG))
  			this._log.debug("[_createScheduleSpan] scheduleSpanSysId: " + scheduleSpanSysId);

  		return scheduleSpanSysId;
  },

  /**
   * Get the cmn_schedule.time_zone and if this is set to floating get the user time zone,
   * or system time zone if user time zone is not defined. Finally activates the time zone.
   *
   * existingSchedule [string]
   * return timezone [string]
   */
  _getTimeZoneBySchedule: function(scheduleSysId) {
  	var timezone = "";
  	var scheduleGr = new GlideRecord("cmn_schedule");
  	if (scheduleGr.get(scheduleSysId))
  		timezone = scheduleGr.time_zone + "";
  	if (!timezone)
  		timezone = this._gs.getSession().getTimeZoneName();
  	this._activateTimezone(timezone);
  	return timezone;
  },

  /**
   * Determine the roster that:
   * 
   * rotaSysId - Get the roster details based on the rota sysid passed
   *
   * 1. Has not already been processed (is not in the fEscalations roster list)
   * 2. Is the next roster to become active (which might mean it is currently active)
   *
   * gdt: [GlideDateTime] the timepoint to search for
   * return: [number] seconds until the next roster is 'active': 0 -> now, -1 -> no more rosters
   */
  _getFirstDelay: function(gdt, rotaSysId /* Optional */) {
  	if (!this._groupSysId)
  		return -1;

  	var rotaGr = new GlideRecord("cmn_rota");
  	rotaGr.addQuery("group", this._groupSysId);
  	if(!gs.nil(rotaSysId)) {
  		rotaGr.addQuery('sys_id', rotaSysId);
  	}
  	rotaGr.addActiveQuery();
  	rotaGr.query();
  	while (rotaGr.next()) {
  		var rotaSysId = rotaGr.sys_id + "";
  		if (this._escalationList.isInRotaList(rotaSysId))
  			this._rotaSysIds = [];
  		else {
  			var schedule = new GlideSchedule(rotaGr.schedule + "");
  			if (schedule.isValid() && schedule.isInSchedule(gdt)) {
  				this._rotaSysIds = [rotaSysId];
  				this._rotaCustomEscalation = rotaGr.getValue('use_custom_escalation') == '1' ? true : false;
  				this._rotaTimeZone = schedule.getTimeZone();

  				if (this._log.atLevel(GSLog.DEBUG))
  					this._log.debug("[_getFirstDelay] this._groupSysId: " + this._groupSysId + " this._rotaSysId: " + this._rotaSysIds[0] + " gdt: " + gdt);

  				return 0;
  			}
  		}
  	}
  	return -1;
  },

  _sortRotasByDate: function (rotasList) {
  	rotasList.sort(function (rota1, rota2) {
  		var gdt1 = new GlideDateTime();
  		gdt1.setDisplayValueInternal(rota1.start);
  		var gdt2 = new GlideDateTime();
  		gdt2.setDisplayValueInternal(rota2.start);
  		if (gdt1.before(gdt2))
  			return -1;
  		if (gdt1.equals(gdt2))
  			return 0;
  		else
  			return 1;
  	});

  	return rotasList;
  },

  _getAllRotaWithOverlappingSchedule: function(gdt, rotaSysIds) {
  	if (!this._groupSysId)
  		return -1;

  	var rotaGr = new GlideRecord("cmn_rota");
  	rotaGr.addQuery("group", this._groupSysId);
  	rotaGr.addActiveQuery();
  	if(rotaSysIds) {
  		rotaGr.addQuery('sys_id', 'IN', rotaSysIds);
  	}
  	rotaGr.query();
  	var overlappingRotas = [];
  	while (rotaGr.next()) {
  		var rotaSysId = rotaGr.sys_id + "";
  		var schedule = new GlideSchedule(rotaGr.schedule + "");
  		if (schedule.isValid() && schedule.isInSchedule(gdt)) {
  			var scheduleTimeMap = schedule.fetchTimeMapWithExcludes(gdt, gdt, null, false);

  			var rotaInfo = {
  				rotaSysId: rotaSysId,
  				rotaTimeZone: schedule.getTimeZone(),
  				rotaCustomEscalation: rotaGr.getValue('use_custom_escalation') == '1' ? true : false
  			};

  			if (scheduleTimeMap.hasNext()) {
  				var span = scheduleTimeMap.next();
  				rotaInfo.start = span.getActualStart().getDisplayValueInternal();
  				rotaInfo.end = span.getActualEnd().getDisplayValueInternal();
  			}
  			overlappingRotas.push(rotaInfo);
  		}
  	}
  	return this._sortRotasByDate(overlappingRotas);
  },

  getOverlappingRotas: function (groupId, gdt) {
  	this._groupSysId = groupId;
  	var rotas = this._getAllRotaWithOverlappingSchedule(gdt);
  	if (rotas !== -1 && rotas.length > 0) {
  		var escalationSettings = this._onCallCommon.getEscalationSettings(groupId);
  		if (escalationSettings == this._onCallCommon.escalation_rule_rota_overlap.START) {
  			rotas = [this._findFirstStartedRota(rotas)];
  		} else if (escalationSettings == this._onCallCommon.escalation_rule_rota_overlap.END) {
  			rotas = [this._findEndingLastRota(rotas)];
  		}
  	}
  	return rotas;
  },

  _getEscalatees: function(groupSysIds, rotaSysIds, rosterSysIds, gdt, nullifyOverrideRoster, overrideCustomEscalation, taskGr) {
  	var groupSysIdList = groupSysIds ? (groupSysIds + "").split(",") : [];
  	var rotaSysIdList = rotaSysIds ? (rotaSysIds + "").split(",") : [];
  	var rosterSysIdList = rosterSysIds ? (rosterSysIds + "").split(",") : [];
  	gdt = gdt || new GlideDateTime();
  	nullifyOverrideRoster = nullifyOverrideRoster + "" === "true";

  	if (this._log.atLevel(GSLog.DEBUG))
  		this._log.debug("[_getEscalatees] groupSysIds: " + groupSysIds + " rotaSysIds: " + rotaSysIds + " rosterSysIds: " + rosterSysIds + " gdt: " + gdt + " nullifyOverrideRoster: " + nullifyOverrideRoster);

  	var escalatees = [];
  	if (!groupSysIdList || groupSysIdList.length === 0)
  		return escalatees;

  	var groupSysIdListLength = groupSysIdList.length;
  	for (var i =0; i < groupSysIdListLength; i++) {
  		var groupSysId = groupSysIdList[i];
  		var escalationPlan = this._getEscalationsByGroup(groupSysId, gdt, nullifyOverrideRoster, overrideCustomEscalation, taskGr);
  		var escalationPlanLength = escalationPlan.length;
  		for (var j = 0; j < escalationPlanLength; j++) {
  			var escalatee = escalationPlan[j];
  			if (rotaSysIdList && rotaSysIdList.length > 0 && rotaSysIdList.indexOf(escalatee.getRotaId()) === -1)
  				continue;

  			var memberRosterId = escalatee.getRosterId();
  			if (!memberRosterId && escalatee.getMemberId())
  				memberRosterId = this._getRosterByMemberId(escalatee.getMemberId());

  			if (memberRosterId && rosterSysIdList && rosterSysIdList.length > 0 && rosterSysIdList.indexOf(memberRosterId) === -1)
  				continue;

  			if (escalatee.getUserId() || escalatee.getDeviceId() || (escalatee.getUserIds() && escalatee.getUserIds().length > 0) || (escalatee.getDeviceIds() && escalatee.getDeviceIds().length > 0) || (escalatee.getEscalationGroups() && escalatee.getEscalationGroups().length > 0))
  				escalatees.push(escalatee);

  			if (escalatee.additionalEscalatees && escalatee.additionalEscalatees.length > 0) {
  				var additionalEscalateeLength = escalatee.additionalEscalatees.length;
  				for (var k = 0; k < additionalEscalateeLength; k++) {
  					var additionalEscalatee = escalatee.additionalEscalatees[k];
  					if (rotaSysIdList && rotaSysIdList.length > 0 && rotaSysIdList.indexOf(additionalEscalatee.getRotaId()) === -1)
  						continue;

  					memberRosterId = additionalEscalatee.getRosterId();
  					if (!memberRosterId)
  						memberRosterId = this._getRosterByMemberId(additionalEscalatee.getMemberId());

  					if (memberRosterId && rosterSysIdList && rosterSysIdList.length > 0 && rosterSysIdList.indexOf(memberRosterId) === -1)
  						continue;

  					escalatees.push(additionalEscalatee);
  				}
  			}
  		}
  	}

  	if (this._log.atLevel(GSLog.DEBUG))
  		this._log.debug("[_getEscalatees] escalatees: " + escalatees);

  	return escalatees;
  },

  _getStartEndByRosterStart: function(startStr, endStr, rosterStartGdt, timezone, spanCount) {
  	if (this._log.atLevel(GSLog.DEBUG))
  		this._log.debug("[_getStartEndByRosterStart] startStr: " + startStr + " endStr: " + endStr + " rosterStartGdt: " + rosterStartGdt + " timezone: " + timezone);

  	var start;
  	var end;

  	// Check if the schedule is starting in the past and reset the start date based on user input
  	var startSdt = new GlideScheduleDateTime();
  	startSdt.setValue(startStr);

  	var startbeforeToday = startSdt.compareTo(new GlideScheduleDateTime(rosterStartGdt.getDisplayValue())) < 0;
  	if (startbeforeToday && rosterStartGdt && !this._cloneStartEndDate) {
  		start = this._addTime(rosterStartGdt.getDate(), startSdt.getGlideDateTime().getTime());
  		var rotationStartSdt = new GlideScheduleDateTime();
  		rotationStartSdt.setValue(start);
  		var endSdt = new GlideScheduleDateTime();
  		endSdt.setValue(endStr);
  		end = this._getEndDateByDiff(rotationStartSdt, startSdt, endSdt);

  		// Absence of Z means floating
  		if (!startSdt.isFloating()) {
  			start += "Z";
  			end += "Z";
  		}
  	} else {
  		start = startStr;
  		end = endStr;
  	}

  	start = this._applyTimeZone(start, timezone).getValue();
  	end = this._applyTimeZone(end, timezone).getValue();

  	return {start: start, end: end};
  },

  _getSequenceName: function(index) {
  	return index < OnCallRotationSNC.SEQUENCE_NAMES.length ? OnCallRotationSNC.SEQUENCE_NAMES[index] : index + 1;
  },

  _applyTimeZone: function(sdt, timeZone) {
  	if (this._log.atLevel(GSLog.DEBUG))
  		this._log.debug("[_applyTimeZone] sdt: " + sdt + " timeZone: " + timeZone);

  	var sdtTZ = new GlideScheduleDateTime();
  	sdtTZ.setValue(sdt);
  	sdtTZ.setTimeZone(timeZone);
  	sdtTZ.setTimeZone("Etc/UTC");

  	return sdtTZ;
  },

  /**
   * Calculate the difference between start and end and add the result to
   * start of the rotation.
   *
   * rotationStartSdt [ScheduleDateTime] when the rotation will start according to specified start date
   * startSdt [ScheduleDateTime] the date and time specified in the schedule entry definition
   * endSdt [ScheduleDateTime] the date and time specified in the schedule entry definition
   * return rosterEnd
   */
  _getEndDateByDiff: function(rotationStartSdt, startSdt, endSdt) {
  	if (this._log.atLevel(GSLog.DEBUG))
  		this._log.debug("[_getEndDateByDiff] rotationStartSdt: " + rotationStartSdt + " startSdt: " + startSdt + " endSdt: " + endSdt);

  	var diff = endSdt.getMS() - startSdt.getMS();
  	var rotationEndGdt = new GlideDateTime(rotationStartSdt.getGlideDateTime());
  	rotationEndGdt.add(diff);
  	return this._addTime(rotationEndGdt.getDate(), rotationEndGdt.getTime());
  },

  /**
   * Who within a roster is on the rotation schedule for the specified time
   *
   * Note that this method will: - not return a rotation member, but a boolean _if_ a member is on call - modify
   * member by reference where the member will be the member on call - use the rosterID to query the member
   *
   * Example usage: GlideRecord member = new GlideRecord(CMN_ROTA_MEMBER); GlideDateTime time = new GlideDateTime();
   * String rosterID = roster.sys_id + ""; // roster = GlideRecord(CMN_ROTA_ROSTER) if (member, time, rosterID) {
   * // member is now a reference to the member record of the member // that is on call at the specified time }
   *
   * memberGr [GlideRecord] instance of CMN_ROTA_MEMBER
   * gdt [GlideDateTime] the time to check for
   * rosterSysId [String] with the unique ID of a roster record
   * return [boolean] is someone on call in a roster at a certain time
   */
  _getRotationMember: function(memberGr, gdt, rosterSysId) {
  	memberGr.addQuery("roster", rosterSysId);
  	memberGr.orderBy("order");
  	memberGr.query();
  	while (memberGr.next()) {
  		if (memberGr.member.active + '' === 'false')
  			continue;
  		var memberRotaSchedule = new GlideSchedule(memberGr.rotation_schedule + "");
  		var justAfterGdt = new GlideDateTime(gdt);
  		justAfterGdt.addSeconds(1);
  		var scheduleTimeMap = memberRotaSchedule.getTimeMap(new GlideDateTime(gdt), justAfterGdt);

  		// This member is one that is on-call at the specified time
  		if (memberRotaSchedule.isValid() && !scheduleTimeMap.isEmpty())
  			return true;
  	}
  	return false;
  },

  /**
   * Get the set of notifications for the current rota based on the rota's escalation type and rotation members
   * rosterGr [GlideRecord] of the roster for the specified Rota
   * gdt [GlideDateTime] the time point to get notifications for
   * nullifyOverrideRoster [boolean]
   */
  _getRotaNotifications: function(rosterGr, gdt, nullifyOverrideRoster, isAdditionalRota) {
  	nullifyOverrideRoster = nullifyOverrideRoster + "" === "true";
  	if(!isAdditionalRota){
  		this._escalationList.clear();
  		this._clearEscalationLineUp();
  		this._escalationList.setGroupID(this._groupSysId);
  		this._escalationList.setRotaID(this._rotaSysIds[this._rotaSysIds.length - 1]);
  		this._escalationList.setTimeZone(this._rotaTimeZone);
  	} else {
  		this._escalationLevel = 0;
  		this._escalationList.addRotaID(this._rotaSysIds[this._rotaSysIds.length - 1]);
  	}

  	if (rosterGr.getRowCount() === 1) {
  		rosterGr.next();
  		this._addRotationEscalations(rosterGr, gdt, nullifyOverrideRoster, isAdditionalRota);
  	} else if (rosterGr.getRowCount() > 1)
  		this._addDutyEscalations(rosterGr, gdt, nullifyOverrideRoster, isAdditionalRota);
  },

  /**
   * Get the set of notifications for the current rota based on the rota's escalation type and rotation members
   * gdt [GlideDateTime] the time point to get notifications for
   * nullifyOverrideRoster [boolean]
   */
  _getRotaNotificationsByTime: function(gdt, nullifyOverrideRoster, isAdditionalRota) {
  	var gr = new GlideRecord('cmn_rota_roster');
  	gr.addQuery("rota", this._rotaSysIds[this._rotaSysIds.length - 1]);
  	gr.addActiveQuery();
  	gr.query();
  	this._getRotaNotifications(gr, gdt, nullifyOverrideRoster, isAdditionalRota);
  },

  _getRotaNotificationsByTimeAndRota: function(gdt, rotaSysId, nullifyOverrideRoster, isAdditionalRota) {
  	var gr = new GlideRecord('cmn_rota_roster');
  	gr.addQuery("rota", rotaSysId);
  	gr.addActiveQuery();
  	gr.query();
  	this._getRotaNotifications(gr, gdt, nullifyOverrideRoster, isAdditionalRota);
  },

  _getRosterByMemberId: function(memberSysId) {
  	if (!memberSysId)
  		return "";

  	var gr = new GlideRecord("cmn_rota_member");
  	if (!gr.get(memberSysId))
  		return "";

  	return gr.roster + "";
  },

  _getEscalationsByGroup: function(groupSysId, dateTime, nullifyOverrideRoster, overrideCustomEscalation, taskGr) {
  	nullifyOverrideRoster = nullifyOverrideRoster + "" === "true";
  	this.who(groupSysId, dateTime, nullifyOverrideRoster, "", overrideCustomEscalation, taskGr);
  	return this._escalatees;
  },

  /**
   * ical: yyyymmddThhmmss
   * return time hh:mm:ss
   */
  _iCalToTime: function(iCal) {
  		var sdt = new GlideScheduleDateTime();
  		sdt.setValue(iCal);
  		var time = sdt.getDisplayValue().split(" ")[1] + "";
  		if (this._log.atLevel(GSLog.DEBUG))
  			this._log.debug("[_iCalToTime] iCal: " + iCal + " time: " + time);
  		return time;
  },

  /**
   * Checks if the given user is off at the given time period
   *
   * userSysId [String] the sys_user id of this rota member
   * gdt [GlideDateTime] the time point to check the member availability
   **/
  _isTheUserOff: function(userSysId, gdt) {
  	if (!userSysId)
  		return false;

  	gdt = gdt ? gdt : new GlideDateTime();
  	var dateStr = gdt.getDate() + "";

  	// Check the user availability
  	var memberGr = new GlideRecord("sys_user");
  	memberGr.setWorkflow(false);	// inactive users are not visible to non-admins
  	memberGr.addQuery("JOINsys_user.sys_id=sys_user_grmember.user!group=" + this._groupSysId);
  	memberGr.addQuery("sys_id", userSysId);
  	memberGr.query();
  	while (memberGr.next()) {
  		if (!memberGr.active)
  			return true;

  		var userScheduleSysId = memberGr.schedule + "";
  		if (!userScheduleSysId)
  			continue;

  		var rosterScheduleSpanGr = new GlideRecord("roster_schedule_span");
  		var encQuery = "schedule=" + userScheduleSysId + "^group=" + this._groupSysId + "^ORgroup=NULL^type=time_off";
  		rosterScheduleSpanGr.addEncodedQuery(this._onCallCommon.getDateLimitedEncQuery(encQuery, dateStr, dateStr));
  		rosterScheduleSpanGr.query();

  		if (this._log.atLevel(GSLog.DEBUG))
  			this._log.debug("[_isTheUserOff] table: " + rosterScheduleSpanGr.getTableName() + " encodedQuery: " + rosterScheduleSpanGr.getEncodedQuery());

  		var timeOffSchedule = new GlideSchedule();
  		timeOffSchedule.addTimeSpans(rosterScheduleSpanGr);
  		if (timeOffSchedule.isValid() && timeOffSchedule.isInSchedule(gdt))
  			return true;
  	}
  	return false;
  },

  _setPageAndPrepare: function(page, url) {
  	page += "";
  	url += page;
  	if (!page.endsWith(OnCallRotationSNC.DOT_DO_Q)) {
  		if (page.endsWith(OnCallRotationSNC.DOT_DO))
  			url += OnCallRotationSNC.Q;
  		else if (page.includes(OnCallRotationSNC.DOT_DO_Q))
  			url += OnCallRotationSNC.AMP;
  		else
  			url += OnCallRotationSNC.DOT_DO_Q;
  	}
  	return url;
  },

  _getGroupIdByRota: function(rotaId) {
  	rotaId = this._firstValidSysId(rotaId);
  	if (JSUtil.nil(rotaId))
  		return "";
  	var gr = new GlideRecord("cmn_rota");
  	gr.get(rotaId);
  	return gr.group + "";
  },

  _getGroupIdByRoster: function(rosterId) {
  	rosterId = this._firstValidSysId(rosterId);
  	if (JSUtil.nil(rosterId))
  		return "";
  	var gr = new GlideRecord("cmn_rota_roster");
  	if (gr.get(rosterId))
  		return gr.rota.group + "";
  	return "";
  },

  _firstValidSysId: function(sysIds) {
  	sysIds = (sysIds + "").split(",");
  	if (sysIds.length < 1)
  		return "";
  	return sysIds[0];
  },

  _getOnCallRosterByGr: function(rosterGr) {
  	if (rosterGr && !this._onCallRoster || this._onCallRoster.getId !== rosterGr.sys_id + "")
  		this._onCallRoster = new OnCallRoster(rosterGr);

  	if (!this._onCallRoster)
  		this._log.error("[_getOnCallRosterByGr] called without valid cmn_rota_roster GlideRecord");

  	return this._onCallRoster;
  },

  getRotas: function(groupSysIds, fetchPreference) {
  	var rotas = {};
  	if (!groupSysIds)
  		return rotas;

  	var gr = new GlideRecord("cmn_rota");
  	gr.addQuery("group", "IN", groupSysIds);
  	gr.orderBy("name");
  	if (!fetchPreference)
  		gr.addActiveQuery();
  	else if (fetchPreference == 'draft')
  		gr.addQuery("state", "draft");
  	else if (fetchPreference == 'all') {
  		var qc = gr.addQuery("active", true);
  		var qcOR = qc.addOrCondition("active", false);
  		qcOR.addCondition("state", "draft");
  	}
  	
  	gr.query();
  	while (gr.next()) {
  		var sysId = gr.sys_id + "";
  		var rotaTime = this.getRotaTime(gr.schedule + "");
  		var rota = {
  			sys_id: sysId,
  			id: sysId,
  			start: rotaTime.start,
  			end: rotaTime.end,
  			startTime: rotaTime.startTime,
  			endTime: rotaTime.endTime,
  			name: gr.getDisplayValue() + "",
  			group: gr.group + "",
  			state: gr.state + ""
  		};
  		rotas[sysId] = this.prepareOnCallGroupsDataRota(gr, rota);
  	}
  	return rotas;
  },
  
  setCheckAccess: function(checkAccess) {
  	this.checkAccess = checkAccess;
  	return this;
  },

  _getUsers: function(userSysIds) {
  	if (this._log.atLevel(GSLog.DEBUG))
  		this.timer.start("[_getUsers]");

  	var users = {};
  	if (!userSysIds || userSysIds.length == 0)
  		return users;
  	
  	var liveFeedApi;
  	if (typeof SNC.LiveFeedApi !== 'undefined') // in test project this is not initialized, so keeping null check.
  		liveFeedApi = new SNC.LiveFeedApi();

  	var userGr = new GlideRecord('sys_user');
  	userGr.addQuery('sys_id', 'IN', userSysIds);
  	userGr.query();
  	
  	while (userGr.next()) {
  		
  		var sysId = userGr.getUniqueValue();
  		var avatarPath = GlideAvatarFinder.getAvatarPath(sysId);
  		if (avatarPath)
  			avatarPath = '/' + avatarPath;
  		
  		var userContactNumber = '';
  		if (!this.checkAccess) {
  			userContactNumber = (userGr.mobile_phone + '' != '') ? userGr.mobile_phone + '' : userGr.phone + '';
  		} else if ((userGr.mobile_phone + '' != '') && userGr.mobile_phone.canRead()){
  			userContactNumber = userGr.mobile_phone + '';
  		} else if (userGr.phone.canRead()) {
  			userContactNumber = userGr.phone + '';
  		}

  		var userObj = {
  			sys_id: sysId,
  			id: sysId,
  			sysUserId: sysId,
  			userID: sysId,
  			avatar: avatarPath || '',
  			name: userGr.getValue('name'),
  			title: userGr.getValue('title'),
  			email: (!this.checkAccess || userGr.email.canRead()) ? userGr.email + '' : '',
  			contact_number: userContactNumber || "",
  			teams_chat_link: this._onCallCommon.isTeamsPluginInstalled() ? new sn_tcm_collab_hook.MSTeamsChatUtil().getUserChatDeeplink(sysId + '') : ""
  		};
  		
  		if (liveFeedApi) {
  			userObj.initials =  liveFeedApi.getInitials(userGr.name + '');
  		}
  		
  		
  		/**
  		 * To stay backward compatiable to SOW on-call. Remove this once SOW on-call uptakes new changes.
  		 * OnCallRotationSowSNC.prepareOnCallGroupsDataUser
  		 */
  		userGr.getID = function() { 
  			return userGr.getUniqueValue();
  		};
  		users[sysId] = this.prepareOnCallGroupsDataUser(userGr, userObj);
  	}

  	if (this._log.atLevel(GSLog.DEBUG))
  		this.timer.stop("[_getUsers]");

  	return users;
  },

  _getHoursMins: function(time) {
  	time = time + "";
  	var timeSeparater = ":";
  	if (time.indexOf(":") > -1)
  		timeSeparater = ":";
  	else if (time.indexOf(".") > -1)
  		timeSeparater = ".";
  	var timeArr = time.split(timeSeparater);
  	if (time.length < 6)
  		return time;
  	return timeArr.slice(0, 2).join(timeSeparater);
  },

  getRostersByRotas: function(rotaSysIds, toJS, requiredFields) {
  	var rosters = {};
  	if (!rotaSysIds)
  		return rosters;

  	var gr = new GlideRecord("cmn_rota_roster");
  	gr.addQuery("rota", "IN", rotaSysIds);
  	gr.addActiveQuery();
  	gr.query();
  	while (gr.next()) {
  		var sysId = gr.sys_id + "";
  		var roster;
  		if (toJS)
  			roster = this._onCallCommon.toJS(gr, requiredFields);
  		else
  			roster = {
  				sys_id: sysId,
  				id: sysId,
  				name: gr.getDisplayValue() + "",
  				rota: gr.rota + ""
  			};

  		rosters[sysId] = this.prepareOnCallGroupsDataRoster(gr, roster);
  	}
  	return rosters;
  },

  getMembers: function(rosterSysIds, toJS, requiredFields) {
  	var members = {};
  	if (!rosterSysIds)
  		return members;

  	var gr = new GlideRecord("cmn_rota_member");
  	gr.addQuery("roster", "IN", rosterSysIds);
  	gr.addActiveQuery();
  	gr.query();
  	while (gr.next()) {
  		var sysId = gr.sys_id + "";
  		var member;
  		if (toJS)
  			member = this._onCallCommon.toJS(gr, requiredFields);
  		else
  			member = {
  				sys_id: sysId,
  				id: sysId,
  				roster: gr.roster + ""
  			};

  		members[sysId] = this.prepareOnCallGroupsDataMember(gr, member);
  	}
  	return members;
  },

  _getGlideJobContext: function(escalationSysId, gr) {
  	var context = "#" + new GlideDateTime().getDisplayValue() + "\n" + 
  		"fcDocumentClass=" + gr.sys_class_name + "\n" +
  		"fcEscalation=" + escalationSysId +"\n" +
  		"fcDocumentKey=" + gr.sys_id;
  	return context;
  },

  _isGlobalScope: function() {
  	return this._gs.getCurrentScopeName() === "rhino.global";
  },

  _isMaxEntries: function() {
  	return ((this._maxEntries > 0) && (this._escalationList.size() >= this._maxEntries));
  },

  _saveEscalation: function(gr, eventName, escalationScriptName, runAt) {
  	if (this._log.atLevel(GSLog.DEBUG))
  		this._log.debug("[_saveEscalation] gr tablename: " + gr.getTableName() + " escalationScriptName: " + escalationScriptName + " eventName: " + eventName + " runAt: " + runAt);

  	var escalationGr = this._createEscalationRecord(gr, eventName, escalationScriptName);
  	var triggerSysId = this._scheduleEscalationTrigger(gr, escalationGr.sys_id + "", runAt);
  	if (triggerSysId) {
  		escalationGr.setValue("trigger", triggerSysId);
  		escalationGr.update();
  	}
  },

  _scheduleEscalationTrigger: function(gr, escalationSysId, runAt) {
  	var next = new GlideDateTime();
  	next.setNumericValue(runAt);

  	var trigger = new GlideRecord("sys_trigger");
  	trigger.setValue("name", "Rota-" + gr.getDisplayValue());
  	trigger.setValue("document", gr.getRecordClassName());
  	trigger.setValue("document_key", gr.sys_id + "");
  	trigger.setValue("trigger_type", OnCallRotationSNC.TRIGGER_ONCE_ONLY);
  	trigger.setValue("next_action", next);
  	trigger.setValue("trigger_class", "com.glide.job.RotaEscalationJob");
  	trigger.setValue("job_context", this._getGlideJobContext(escalationSysId, gr));
  	trigger.setValue("job_id", "NULL");
  	trigger.setValue("state", OnCallRotationSNC.STATE_READY);
  	var triggerSysId = trigger.insert();

  	if (this._log.atLevel(GSLog.DEBUG))
  		this._log.debug("[_scheduleEscalationTrigger] triggerSysId: " + triggerSysId);

  	return triggerSysId;
  },

  /**
   * time [string] time can be in either of these formats: hh:mm:ss or hhmmss
   */
  setRosterStartTime: function(time) {
  	if (!time)
  		this._log.error("setRosterStartTime called empty parameter: time");

  	var hour;
  	var minute;
  	var second;

  	if (time.indexOf(":") !== -1) {
  		var timeArr = time.split(":");

  		if (timeArr.length !== 3)
  			this._log.error("setRosterStartTime called invalid time parameter. Should be in format 00:00:00");

  		hour = timeArr[0];
  		minute = timeArr[1];
  		second = timeArr[2];
  	} else {
  		hour = time.substring(0, 2);
  		minute = time.substring(2, 4);
  		second = time.substring(4, 6);
  	}

  	if (this._log.atLevel(GSLog.DEBUG))
  		this._log.debug("[setRosterStartTime] time: " + time + " hour: " + hour + " minute: " + minute + " second: " + second);

  	var startTimeGit = new GlideIntegerTime();
  	startTimeGit.setTime(hour, minute, second);
  	this._rosterStartGit = startTimeGit;

  	if (this._log.atLevel(GSLog.DEBUG))
  		this._log.debug("[setRosterStartTime] rosterStartGit hour: " + this._rosterStartGit.getHour() + " min: " + this._rosterStartGit.getMinute() + " sec: " + this._rosterStartGit.getSecond());
  },
  
  setIsAllDayRoster: function(isAllDayRoster) {
  	this._isAllDayRoster = isAllDayRoster;
  },

  /**
   * Find the on call person for a particular point in time for the primary roster and set that user as the primary
   * user for the escalation(s)
   *
   * Used for manually populated rotas, which do not have rosters
   *
   * dateTime: [GlideDateTime] the time point to search for
   */
  _setPrimaryUser: function(dateTime) {
  	var userSysId = this._checkForOverrideMember(dateTime);
  	if (userSysId)
  		this._escalationList.setPrimaryUserID(userSysId);
  },

  /**
   * Used by Escalation Report
   */
  deleteExistingRecords: function() {
  	var gr = new GlideRecord("v_on_call");
  	gr.initialize();
  	gr.addQuery("current_user_id", this._gs.userID());
  	gr.deleteMultiple();
  },

  /**
   * Used by Escalation Report
   */
  populateOnCallList: function(params) {
  	if (!params)
  		params = {};

  	if (this._log.atLevel(GSLog.DEBUG))
  		this._log.debug("[populateOnCallList] params: " + JSON.stringify(params));

  	var dateTime = "";
  	if (params.startDate && typeof params.startDate !== "undefined") {
  		if (typeof params.startDate == 'string') {
  			dateTime = new GlideDateTime();
  			dateTime.setDisplayValueInternal(params.startDate);
  		} else {
  			dateTime = params.startDate; // GlideDateTime
  		}
      }

  	var showTypeOnCallPerson = false;
  	if (params.showType && typeof params.showType !== "undefined" && params.showType == "true")
  		showTypeOnCallPerson = true;

  	var groups = "";
  	if (params.groups && typeof params.groups !== "undefined")
  		groups = params.groups;

  	var groupList = groups.split(",");
  	var groupListLength = groupList.length;
  	if (groupListLength > 0) {
  		var index = groupList.indexOf("--");
  		if (index !== -1)
  			groupList.splice(index, 1);
  	}
  	if (showTypeOnCallPerson) {
  		for (var i = 0; i < groupListLength; i++)
  			this._generatePrimaryMembers(groupList[i], dateTime);
  	} else {
  		for (var j = 0; j < groupListLength; j++)
  			this._generateRota(groupList[j], dateTime);
  	}
  	return dateTime;
  },
  
  _generatePrimaryMembers: function(groupId, dateTime) {
  	this.whoIsOnCall(groupId, dateTime);
  	var primaryMembersData = this.getPrimaryUsers();
  	var gr = new GlideRecord("v_on_call");
  	while (this.nextAnytime()) {
  		var rota = this.getRotaFromCurrentEscalation();
  		if(primaryMembersData.hasOwnProperty(rota) && this.getUser() == primaryMembersData[rota].userSysId){
  			gr.initialize();
  			gr.current_user_id = this._gs.userID();
  			gr.group = groupId;
  			gr.rota = rota;
  			gr.roster = this.getRoster();
  			gr.primary_user = primaryMembersData[rota].userSysId;
  			gr.delay_duration.setDateNumericValue(this.getDelay().getNumericValue());
  			gr.contact_at.setDateNumericValue(this.getContactAt().getNumericValue());
  			gr.user = primaryMembersData[rota].userSysId;
  			gr.type = this.getType();
  			gr.insert();
  			delete primaryMembersData[rota];
  		}
  	}
  },

  _generateRota: function(groupId, dateTime) {
  	if (this._log.atLevel(GSLog.DEBUG))
  		this._log.debug("[_generateRota] groupId: " + groupId + " dateTime: " + dateTime);

  	if (dateTime)
  		this.whoAt(groupId, dateTime);
  	else
  		this.who(groupId);

  	var sysIds = [];
  	var rotaIds = {};
  	var gr = new GlideRecord("v_on_call");
  	while (this.nextAnytime()) {
  		gr.initialize();
  		gr.current_user_id = this._gs.userID();
  		gr.group = groupId;
  		gr.rota = this.getRotaFromCurrentEscalation();
  		gr.roster = this.getRoster();
  		gr.primary_user = this.getPrimaryUserByRota(this.getRotaFromCurrentEscalation());
  		gr.delay_duration.setDateNumericValue(this.getDelay().getNumericValue());
  		gr.contact_at.setDateNumericValue(this.getContactAt().getNumericValue());
  		gr.user = this.getUser();
  		gr.type = this.getType();
  		if (gr.type + "" === "device") {
  			gr.device = this.getDevice();
  			gr.device_type = this.getDeviceType();
  		}
  		gr.escalation_group = this.getEscalationGroupId();
  		gr.cmn_notif_device = this.getDeviceId();
  		if (!rotaIds[gr.rota +""]) {
  			rotaIds[gr.rota + ""] = {rotaId: gr.rota+"", delay_duration: gr.delay_duration, contact_at: gr.contact_at};
  		}
  		sysIds.push(gr.insert());
  	}

  	// if show active roster members is chosen and gr.rota is not empty inserts catch all members to a notification report
  	var that = this;
  	Object.keys(rotaIds).forEach(function(key) {
  		that._insertCatchAllMembers(groupId, rotaIds[key]);
  	});
  },

  _insertCatchAllMembers: function(groupId, rota) {
  	var rotaSysId = rota.rotaId;

  	if (this._log.atLevel(GSLog.DEBUG))
  		this._log.debug("[_insertCatchAllMembers] rotaSysId: " + rotaSysId);

  	var catchAllType = this.getCatchAllType(rotaSysId);
  	if (catchAllType) {
  		var catchAllSysID = this.getCatchAll(rotaSysId);
  		var timeBetweenReminders = this._getTimeBetweenReminders(catchAllType, catchAllSysID);
  		var delayDuration = new GlideDateTime(rota.delay_duration);
  		var contactAt = new GlideDateTime(rota.contact_at);

  		var sysIds = [];
  		var gr = new GlideRecord("v_on_call");
  		gr.initialize();
  		gr.current_user_id = this._gs.userID();
  		gr.group = groupId;
  		gr.rota = rotaSysId;
  		gr.primary_user = this.getPrimaryUserByRota(rotaSysId);
  		gr.type = catchAllType;
  		gr.delay_duration.setDateNumericValue(delayDuration.getNumericValue() + timeBetweenReminders);
  		gr.contact_at.setDateNumericValue(contactAt.getNumericValue() + timeBetweenReminders);
  		if (catchAllType === "all") {
  			var rotaMember = new GlideRecord("cmn_rota_member");
  			rotaMember.addQuery("roster", catchAllSysID);
  			rotaMember.query();
  			while (rotaMember.next()) {
  				gr.user = rotaMember.member;
  				gr.roster = catchAllSysID;
  				sysIds.push(gr.insert());
  			}
  		} else {
  			var userGr = new GlideRecord("sys_user");
  			if (userGr.get(catchAllSysID)) {
  				gr.roster = "";
  				gr.user = userGr.sys_id;
  				sysIds.push(gr.insert());
  			}
  		}

  		if (this._log.atLevel(GSLog.DEBUG))
  			this._log.debug("[_insertCatchAllMembers] catchAllType: " + catchAllType + " sysIds: " + sysIds.join(","));
  	}
  },

  _getTimeBetweenReminders: function(catchAllType, rosterId) {
  	if (this._log.atLevel(GSLog.DEBUG))
  		this._log.debug("[_getTimeBetweenReminders] catchAllType: " + catchAllType + " rosterId: " + rosterId);

  	var gdt = null;
  	// if catchAllType is Notify All then get time_between_reminders from cmn_rota_roster table
  	if (catchAllType === "all") {
  		var rosterGr = new GlideRecord("cmn_rota_roster");
  		if (rosterGr.get(rosterId))
  			gdt = new GlideDateTime(rosterGr.time_between_reminders);
  	} else {
  		var vOnCallGr = new GlideRecord("v_on_call");
  		vOnCallGr.orderByDesc("delay_duration");
  		vOnCallGr.setLimit(2);
  		vOnCallGr.query();
  		// if catchAllType is Manager or Individual then calculate time_between_reminders by substracting delay_duration from last two records from v_on_call
  		if (vOnCallGr.next()) {
  			var lastDelayDuration = new GlideDateTime(vOnCallGr.delay_duration);
  			if (vOnCallGr.next()) {
  				var beforeLastDelayDuration = new GlideDateTime(vOnCallGr.delay_duration);
  				gdt = new GlideDuration(lastDelayDuration.getNumericValue() - beforeLastDelayDuration.getNumericValue());
  			}
  		}
  	}
  	var timeBetweenReminders = gdt ? gdt.getNumericValue() : 0;

  	if (this._log.atLevel(GSLog.DEBUG))
  		this._log.debug("[_getTimeBetweenReminders] timeBetweenReminders: " + timeBetweenReminders);

  	return timeBetweenReminders;
  },

  isRotationCalcRequired: function(gr) {
  	if (gr.last_reminder_time.changes())
  		return false;

  	if (this.isWizardRunning())
  		return false;

  	/* Fields has to be added to this array if changing only these fields will not affect the rotation */
  	var rotationNoImpactFields = ['override_user_contact_preference'];
  	
  	var gru = GlideScriptRecordUtil.get(gr);
  	var changedFields = gru.getChangedFieldNames();
  	changedFields = j2js(changedFields);
  	for (var i = 0; i < changedFields.length; i++) {
  		if (rotationNoImpactFields.indexOf(changedFields[i]) < 0)
  			return true;
  	}
  	return false;
  },

  getRotaTime: OnCallRotationSNC._getRotaTime,

  getSpanByGdt: OnCallRotationSNC._getSpanByGdt,

  type: "OnCallRotationSNC"
};

OnCallRotationSNC.TRIGGER_ONCE_ONLY = 0;
OnCallRotationSNC.STATE_READY = 0;

OnCallRotationSNC.CMN_ROTA_MEMBER_LIST = "cmn_rota_member_list";
OnCallRotationSNC.AMP = "&";
OnCallRotationSNC.DOT_DO = ".do";
OnCallRotationSNC.DOT_DO_Q = ".do?";
OnCallRotationSNC.Q = "?";
OnCallRotationSNC.SCOPE_NAME = "sn.on_call";
OnCallRotationSNC.SEQUENCE_NAMES = ["Primary", "Secondary", "Tertiary", "Quaternary", "Quinary", "Senary", "Septenary", "Octonary", "Nonary"];
OnCallRotationSNC.PROPERTY_SKIP_COMPUTE_SCHEDULES = "com.snc.on_call_rotation.skip_compute_member_rotation_schedules";
OnCallRotationSNC.PROPERTY_IS_WIZARD = "com.snc.on_call_rotation.is_wizard";
OnCallRotationSNC.DEFAULT_ROTATION_INTERVAL_TYPE = 'weekly';
OnCallRotationSNC.DEFAULT_ROTATION_INTERVAL_COUNT = '1';

OnCallRotationSNC.isBrowserSupported = function(scopeName) {
  if (this._isGlobalScope())
  	return !new GlideCollaborationCompatibility().isIncompatible();
  else
  	return !new GlideUICompatibility(scopeName || OnCallRotationSNC.SCOPE_NAME).isBlocked();
};

OnCallRotationSNC.getCompatibility = function(scopeName) {
  if (this._isGlobalScope())
  	return new GlideCollaborationCompatibility().getCompatibility();
  else
  	return new GlideUICompatibility(scopeName || OnCallRotationSNC.SCOPE_NAME).getCompatibility();
};

Sys ID

1d53492757930300532c3da73d94f93a

Offical Documentation

Official Docs: