COA Validator

COA validation happens both on the client and server-side.  Basic flow is:

  • Check on the client-side and return errors to the end user
  • If client-side succeeds send it to the COA validation web service
  • If the web service returns errors display them to the user
  • If the web service returns a success set the validated variable to true
  • Display feedback via a loading spinner to the user throughout when we're waiting on communication

How

Client Script

Looking at the common Validate COA as part of the RITM_COA variable set you can see how the validation works.  We do some client-side validation first to save us trips to the validation service.  Here it is together, I would recommend looking at the live instance in case anything has changed.

function onChange(control, oldValue, newValue, isLoading) {
	if (isLoading || newValue == '') {
		return;
	}
	
	var validationField = 'coa_validated';
	var validatingField = 'coa_validating';
	var coaTypeField = 'coa_type';
	
	// Reset the messages on every change
	g_form.hideFieldMsg('coa',true);
	// Send the value to the script
	g_form.setValue(validationField,'false');
		
	var ga = new GlideAjax('YaleAjax');
	ga.addParam('sysparm_name', 'clientCOAValidation');
	// Funky things happen if we don't stringify these
	ga.addParam('sysparm_type',JSON.stringify(g_form.getValue(coaTypeField)));
	ga.addParam('sysparm_coa',JSON.stringify(newValue));
	ga.getXML(clientCOAValidator);
	
	function clientCOAValidator(response) {
		g_form.setDisplay(validatingField,false);
		var answer = response.responseXML.documentElement.getAttribute("answer");
		var resp = JSON.parse(answer);
		if(!resp.success)
			g_form.showErrorBox('coa',resp.message);
		
		if(resp.success){
			// Continue on to actually validate
			var ga = new GlideAjax('YaleAjax');
			ga.addParam('sysparm_name', 'validateCOAExpressed');
			ga.addParam('sysparm_coa',JSON.stringify(resp.coaObject));
			
			ga.getXML(COAValidator);
			g_form.setDisplay(validatingField,true);
		}
	}
	
	function COAValidator(response) {
		g_form.setDisplay(validatingField,false);
		var answer = response.responseXML.documentElement.getAttribute("answer");
		coaws = JSON.parse(answer);
		for(i=0;i< coaws.message.length;i++)
			g_form.showErrorBox('coa',coaws.message[i]);
		
		if(coaws.success)
			g_form.setValue(validationField,'true');
	}
}



/*
var company = coa[0];
var grant = coa[1];
var gift = coa[2];
var yaleDesignated = coa[3];
var costCenter = coa[4];
var program = coa[5];
var project = coa[6];
var assignee = coa[7];
var fund = coa[8];
var location = coa[9];
var spendCategory = coa[10];
var revenueCategory = coa[11];
var payComponent = coa[12];
var ledgerAccount = coa[13];
var debtLine = coa[14];
 */

Script Include

YaleAjax contains the methods used to make this possible.

clientCOAValidation

	clientCOAValidation: function (coaType,coa){
		if(this.getParameter('sysparm_type'))
			coaType = JSON.parse(this.getParameter('sysparm_type'));
		if(this.getParameter('sysparm_coa'))
			coa = JSON.parse(this.getParameter('sysparm_coa'));
		
		coa = coa.split('.');
		
		var message = '';
		var multiMessage = [];
		var coaObject = {};
		var r = {};
		r.success = true;
		
		if(coa[0] == ''){
			r.success = false;
			message = 'Company is required!';
		}else{
			coaObject.Company_code = coa[0];
		}
		
		//Company is always required
		switch(coaType){
			case 'grant':
			
			if(coa[1] == ''){
				r.success = false;
				message = 'Grant is required!';
			}else{
				coaObject.Grant_code = coa[1];
			}
			
			// Location is optional
			if(coa[9] === undefined && coa[9] != '')
				coaObject.Location = coa[9];
			
			break;
			
			case 'grant_with_cost_share':
			if(coa[1] == '' || coa[3] == ''){
				r.success = false;
				message = 'Grant and Yale Designated is required!';
			}
			else{
				coaObject.Grant_code =  coa[1];
				coaObject.Yale_Designated_code =  coa[3];
			}
			
			// Location is optional
			if(coa[9] === undefined && coa[9] != '')
				coaObject.Location = coa[9];
			
			break;
			
			case 'gift':
			//gift,cost center, program,project required
			
			if(coa.length <6){
				r.success = false;
				message = "Required segments aren't supplied!  Gift, Cost Center Program, and Project are required segments";
				break;
			}
			
			if(coa[2] == '')
				multiMessage.push('Gift');
			
			if(coa[4] == '')
				multiMessage.push('Cost Center');
			
			if(coa[5] == '')
				multiMessage.push('Program');
			
			if(coa[6] == '')
				multiMessage.push('Project');
			
			
			if(multiMessage.length > 0){
				r.success = false;
				message = multiMessage.join(", ") + ' is required!';
			}
			
			coaObject.Gift_code = coa[2];
			coaObject.Cost_Center_code = coa[4];
			coaObject.Program_code = coa[5];
			coaObject.Project_code = coa[6];
			
			//assignee,location optional
			if(coa[7] === undefined && coa[7] != '')
				coaObject.Assignee = coa[7];
			if(coa[9] === undefined && coa[9] != '')
				coaObject.Location = coa[9];
			break;
			
			case 'designated':
			//designated,cost center,program,project required
			if(coa.length <7){
				r.success = false;
				message = "Required segments aren't supplied!  Designated, Cost Center, Program, and Project are required segments";
				break;
			}
			
			if(coa[3] == '')
				multiMessage.push('Designated');
			
			if(coa[4] == '')
				multiMessage.push('Cost Center');
			
			if(coa[5] == '')
				multiMessage.push('Program');
			
			if(coa[6] == '')
				multiMessage.push('Project');
			
			if(multiMessage.length > 0){
				r.success = false;
				message = multiMessage.join(", ") + ' is required!';
			}
			
			coaObject.Yale_Designated_code = coa[3];
			coaObject.Cost_Center_code = coa[4];
			coaObject.Program_code = coa[5];
			coaObject.Project_code = coa[6];
			
			//assignee,location optional
			if(coa[7] === undefined && coa[7] != '')
				coaObject.Assignee = coa[7];
			if(coa[9] === undefined && coa[9] != '')
				coaObject.Location = coa[9];
			
			break;
		}
		
		r.message = message;
		r.coaObject = coaObject;
		return JSON.stringify(r);
	}


validateCOAExpressed

validateCOAExpressed: function(coa) {
		try {
			if(this.getParameter('sysparm_coa'))
				coa = JSON.parse(this.getParameter('sysparm_coa'));
			
			var returnData = {};
			returnData.message = '';
			returnData.success = true;
			
			var r = new sn_ws.RESTMessageV2('COA Validator', 'get');
			r.setStringParameter('endpoint',gs.getProperty('yale.coa.endpoint'));
			for (var key in coa) {
				r.setQueryParameter(key,coa[key]);
			}
			
			// Assuming always today for now?
			var today = new Date();
			var dd = today.getDate();
			var mm = today.getMonth()+1; //January is 0!
			
			var yyyy = today.getFullYear();
			if(dd<10){
				dd='0'+dd;
			}
			if(mm<10){
				mm='0'+mm;
			}
			
			var transactionDate = mm+'/'+dd+'/'+yyyy;
			r.setQueryParameter('Transaction_Date',transactionDate);
			
			var response = r.execute();
			var responseBody = response.getBody();
			var httpStatus = response.getStatusCode();

			// If something isn't right, return a message to try again later
			if(httpStatus != 200){
				returnData.message = ['The COA validation service is unavailable. Please try your request later.'];
				returnData.success = false;
				return JSON.stringify(returnData);
			}
			
			var data = gs.xmlToJSON(responseBody);
			
			var message = data['soapenv:Envelope']['soapenv:Body']['ser-root:validateCOAResponse']['ErrorMessage'];
			returnMessage = [];
			Object.keys(message).forEach(function(element) {
				ignoreList = ['Revenue_Category_Message','Spend_Category_Message','Ledger_Account_Message'];
				// Make sure that the element has an error and it's not something we should skip.
				
				var arrayUtil = new ArrayUtil();
				// If the message is empty don't do anything
				if(message[element] != true && arrayUtil.indexOf(ignoreList,element) == -1){
					var ccc = new GlideRecord('u_cost_center_configuration');
					ccc.get('u_validator_error_tag',element);
					returnMessage.push(message[element] + " (" + ccc.u_segment + " is located in the " + ccc.u_expressed_position_label + " segment)");
				}
			});
			
			
			if(returnMessage.length > 0){
				returnData.message = returnMessage;
				returnData.success = false;
			}
			
			return JSON.stringify(returnData);
		}
		catch(ex) {
			var message = ex.getMessage();
			return message;
		}
	}


Catalog Items

Adding COA to a catalog item

To add a single COA to a catalog item, simply add the RITM_COA variable set to an item.  All logic for validating the COA is encapsulated here.  Easy, right?

Workflow

For an item with a single COA it should be as easy as adding the 'Cost Center Approvers' sub workflow and handling the output.  See RITM_YALE_Ethernet_Request as an example.  There are properties in place for each item to control what type of approvals will be used, whether they be based on cost center of the COA provided or supervisory org.  Minds have changed about this throughout this process which is why there's a switch.

Adding multiple COAs to a catalog item 

  • Remove the variable set COA if it's already present, you'll want to be sure this doesn't impact existing ordered items however.
  • Add the following fields for each COA, where x is a unique name for the COA.  Sections help you more easily hide the COA using UI policies.
    • x_section
    • x_coa_type
    • x_coa_help
    • x_coa
    • x_coa_validated
    • x_coa_validating
    • x_coa_corrected
    • x_corrected_coa
    • x_end_section
  • Client Scripts needed
    • One for showing the message that shows how to format the values and clears out the existing COA if provided on change of type.  See here for an example.
    • One for validating the COA.  Below is a template in which you'll need to set the actual field names. 

      function onChange(control, oldValue, newValue, isLoading) {
         if (isLoading || newValue == '') {
            return;
         }
      
      	var validationField = 'x_coa_validated';
      	var validatingField = 'x_coa_validating';
      	var coaTypeField = 'x_coa_type';
      	var coaField = 'x_recurring_charge'
      	
      	// Reset the messages on every change
      	g_form.hideFieldMsg(coaField,true);
      	// Send the value to the script
      	g_form.setValue(validationField,'false');
      		
      	var ga = new GlideAjax('YaleAjax');
      	ga.addParam('sysparm_name', 'clientCOAValidation');
      	// Funky things happen if we don't stringify these
      	ga.addParam('sysparm_type',JSON.stringify(g_form.getValue(coaTypeField)));
      	ga.addParam('sysparm_coa',JSON.stringify(newValue));
      	ga.getXML(clientCOAValidator);
      	
      	function clientCOAValidator(response) {
      		g_form.setDisplay(validatingField,false);
      		var answer = response.responseXML.documentElement.getAttribute("answer");
      		var resp = JSON.parse(answer);
      		if(!resp.success)
      			g_form.showErrorBox(coaField,resp.message);
      		
      		if(resp.success){
      			// Continue on to actually validate
      			var ga = new GlideAjax('YaleAjax');
      			ga.addParam('sysparm_name', 'validateCOAExpressed');
      			ga.addParam('sysparm_coa',JSON.stringify(resp.coaObject));
      			
      			ga.getXML(COAValidator);
      			g_form.setDisplay(validatingField,true);
      		}
      	}
      	
      	function COAValidator(response) {
      		g_form.setDisplay(validatingField,false);
      		var answer = response.responseXML.documentElement.getAttribute("answer");
      		coaws = JSON.parse(answer);
      		
      		for(i=0;i< coaws.message.length;i++)
      			g_form.showErrorBox(coaField,coaws.message[i]);
      		
      		if(coaws.success)
      			g_form.setValue(validationField,'true');
      	}   
      }
      • Making sure the COAs are validated on submit.  You can use something like this.  The conditions and messages will vary depending on the requirements.

        function onSubmit() {
        	if(g_form.isMandatory('monthly_recurring_charge') && g_form.getValue('mrc_coa_validated') == 'false'){
        		alert('Sorry. The monthly recurring charge COA has not been validated.');
        		return false;
        	}
        	if(g_form.isMandatory('pc_charge') && g_form.getValue('pc_coa_validated') == 'false'){
        		alert('Sorry. The one-time project charge COA has not been validated.');
        		return false;
        	}
        }
  • Workflow considerations
    • This is somewhat dependent on the catalog item, but the current process for current items with COAs as of 8/11/17 is that approvals will only go out for one COA and if approved the cost center managers on the other COA will only be notified.
    • The Cost Center Approvers Sub-Workflow takes a couple arguments.  You need to calculate the cost center in the activity before to pass it to the workflow.  In the case of a single approval above we have a predictable field so we don't need to pass it.

  • Configuring the Request Item Multiple Configs Table
    • The COA correction widget for Service Portal and UI Macro for vanilla need to be able to handle when there are multiple COAs.
    • Add entries to  Request Item Multiple Configs Table for each COA you've added.