Intro
This document describes how DSP Contracts are represented and used to drive decisions in ServiceNow's incident module.
Background
DSP Contracts have been implemented as "Service Contracts" table (ast_service} that came with the asset management module. Each contract has a list of organizations which it serves; these lists are maintained in a Box spreadsheet and pulled into ServiceNow via a nightly import set (this could probably be improved by allowing in-tool editing; the trade-off is role/ACL management vs. import set/transform management).
Which DSP contract is used is not a simple matter of lookup; in the case of FASIT-tagged users, a different DSP group is used.
This combination of data and business rules is used to:
- credit time worked to appropriate contracts
- ensure correct dispatch of desktop support
Use Cases
From R1138, R1139, R1140, R1141:
- When processing an incident, an ITIL user should be able to determine which DSP group supports a particular user so that the incident can be routed to the appropriate DSP group.
- DSP managers need to be able to report at the end of the month the time worked for each support contract so they can accurately bill the orgs
- DSPs need to be able to filter tickets assigned to their groups by the contract of each client's org so they can constrain by the contracts they support
- DSP managers need counts of the users supported by a given contract so they can appropriately staff the contract support groups
Fields
To accomplish this, it was initially decided that the user table (sys_user) would have a visible field "DSP Group" (u_dsp_group) which would be populated by a business rule.
The DSP contracts were loaded (one time) into the service contracts table (ast_service). The organization table was given a custom field to point each org back to exactly one contract. These relationships were initially loaded one time. Over time, this data became stale and a way was eventually established to update it regularly (see below).
Business Rule
Fruition originally called the rule "Update org data"... it originally worked something like this:
gs.log(current.department); setOrgData(); function setOrgData() { var org = new GlideRecord('u_cmn_organization'); org.get(current.u_organization); current.department = org.u_department.sys_id; current.u_division = org.u_division.sys_id; current.company = org.u_company.sys_id; current.u_dsp_group = org.u_dsp_group; current.u_service_contract = org.u_service_contract; }
...this rule populates the user's department, division, company, DSP Group, and Service Contract fields with those belonging to the org (instead of dot-walking?). Then, by examining the user field, a tech working an incident can see what the correct group is.
Data Source
Since there is no official data ensconced in an existing database, and there is no general workflow for updating maintenance tables in ServiceNow, it was desirable to delegate responsibility for the data source to something other than the contracts table. So a formatted spreadsheet was placed in Box.com and paired with a data source transform map in ServiceNow. Multiple people have edit access.
Data Contract
- The DSP organization is responsible for the source data content
- spreadsheet containing data will exist at https://yale.box.com/shared/static/k53bk7qymee6t5g7j85u.xls (don't delete the file, or this will break)
- must remain XLS (Excel 97/2000) format
- must remain publicly accessible
- headers must be "Contract Number", "Active?", "Short Description", "Assignment Group", "Organizations"
- Contract Number should be unique (enforced upon import)
- Active? must TRUE or FALSE
- Assignment Group should match an existing group (or it will be rendered as empty)
- Organizations should be a space-delimited list of valid orgs (invalid orgs will not register upon import)
- Each organization should map to one contract only (last match wins)
- The data will be loaded daily at midnight.
Load/Transform
Data Source (DEV) - https://yaledevelopment.service-now.com/nav_to.do?uri=sys_data_source.do?sys_id=deeaf2be800bfc847ac030825c2a51ed
Transform Map (DEV) - https://yaledevelopment.service-now.com/nav_to.do?uri=sys_transform_map.do?sys_id=66569736804bfc847ac030825c2a5196
...the above links to the latest copy in development. Most of the transform is a straight field mapping, coalescing on contract number. Below is the initial version of the org-mapping portion of the transform, for reference only.
// // 2013/01/24 - william.west@yale.edu - Defect 376 - DSP Query not showing contracts for requests. // // The scripted part of this transform parses the Organizations column from the input. // * regex validate, ignore any input that doesn't match. // * foreach org listed, get the gliderecord, set u_service_contract to point to current contract and update. // * ignore no match. // // MDN implementation of Array.indexOf // https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/indexOf if (!Array.prototype.indexOf) { Array.prototype.indexOf = function (searchElement /*, fromIndex */ ) { "use strict"; if (this == null) { throw new TypeError(); } var t = Object(this); var len = t.length >>> 0; if (len === 0) { return -1; } var n = 0; if (arguments.length > 1) { n = Number(arguments[1]); if (n != n) { // shortcut for verifying if it's NaN n = 0; } else if (n != 0 && n != Infinity && n != -Infinity) { n = (n > 0 || -1) * Math.floor(Math.abs(n)); } } if (n >= len) { return -1; } var k = n >= 0 ? n : Math.max(len - Math.abs(n), 0); for (; k < len; k++) { if (k in t && t[k] === searchElement) { return k; } } return -1; } } // // create array of latest list orgs that are supposed to have this contract, trim any spaces from each var newestorgs = source.u_organizations.split(' '); for(var i=0; i<newestorgs.length; i++) { newestorgs[i] = newestorgs[i].replace(/\s+/g, ''); } // // first, get the list of orgs which currently have this contract var contract_id = getContract(source.u_contract_number); var currentorgs = getOrgsWithContract(contract_id); // // next, get a list or orgs which have this contract but shouldn't var deleteorgs = deleteThese(currentorgs, newestorgs); // // finally, get a list of orgs which should have this contract but don't var addorgs = addThese(currentorgs, newestorgs); // // if the contract_id isn't 0, make the prescribed org changes if(contract_id != 0) { deleteOrgs(deleteorgs); addOrgs(addorgs, contract_id); } // // uncomment for debug output gs.log('********* TRANSFORM service_contracts - ' + source.u_contract_number + ', sys_id: ' + contract_id + '; will delete: ' + deleteorgs.toString() + '; will add: ' + addorgs.toString() + '; current orgs: ' + currentorgs.toString() ); //+ '; new org list: ' + newestorgs.toString() //); // // get a contract by number; return sys_id or 0 function getContract(number) { var gr = new GlideRecord('ast_service'); gr.addQuery('number',number); gr.query(); if(gr.next()) { return(gr.sys_id); } else { return 0; } } // // get orgs pointing to contract with sys_id == contract; return array of org sys_ids (could be empty) function getOrgsWithContract(contract) { var orgs = []; var gr = new GlideRecord('u_cmn_organization'); gr.addQuery('u_service_contract', contract); gr.query(); while(gr.next()) { orgs.push(gr.u_id.toString()); } return orgs; } // // return array of orgs in current but not in newest function deleteThese(current, newest) { var orgs = []; var i; for (i=0; i<current.length; i++) { if(newest.indexOf(current[i],0) == -1) { orgs.push(current[i]); } } return orgs; } // // return array of orgs in newest but not in current function addThese(current, newest) { var orgs = []; var i; for (i=0; i<newest.length; i++) { if(current.indexOf(newest[i],0) == -1) { orgs.push(newest[i]); } } return orgs; } // // disassociate service contract from orgs with matching names function deleteOrgs(orgs) { var i; for(i=0; i<orgs.length; i++) { var gr = new GlideRecord('u_cmn_organization'); gr.addQuery('u_id',orgs[i]); gr.query(); if(gr.next()) { gr.u_service_contract = ''; gr.update(); } } } // // associate service contract with orgs matching names function addOrgs(orgs, contract) { var i; for(i=0; i<orgs.length; i++) { var gr = new GlideRecord('u_cmn_organization'); gr.addQuery('u_id',orgs[i]); gr.query(); if(gr.next()) { gr.u_service_contract = contract; gr.update(); } } }
Data Model
Note that the DSP Group of a user is derived from this data via business logic that exists outside of the data model. The pseudocode is as follows:
if User->Fasit "CTS DSP Team FASIT" else User->Org->Contract->DSPGroup.Name
Here is example SNow JS for obtaining the correct DSP Group for a given user in the absence of a denormalized field reference or store proc:
// if user is FASIT, use FASIT group, else use the org's group // gr = user's GlideRecord if(gr.u_facit == true) { gs.addInfoMessage('Assigned to FASIT DSP Group'); current.assignment_group = sys_user_group_by_name('CTS DSP Team FASIT'); } else { current.assignment_group = gr.u_organization.u_service_contract.u_assignment_group.sys_id; }