Intro
This document describes how DSP Contracts are represented and used to drive decisions for ServiceNow tasks. These are changing on R1138, R1139, R1140, R1141, D936, D937.
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
Currently, DSP group assignment is made reliable through a UI action that implements the business rule.
To deal with billing, a "bill to contract" field was added to task_time_worked. Upon insertion of each task time worked entry, business logic records a DSP contract to bill as appropriate.
Data Source
Since there is no official data ensconced in an existing database, and we didn't feel like adding a role and ACLs at the time, we delegated responsibility for the data source to something outside of ServiceNow. So a formatted spreadsheet was placed in Box.com and paired with a data source transform map in ServiceNow. Multiple people have edit access, and they are responsible for editing workflow. The import is based on the below data contract.
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; }
Implementation
Group Assignment
"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."
This is facilitated via a UI action called "Assign to DSP Group", which is available in Incident and SC Task forms. It chooses a group by the rules described above.
There are a series of possible scenarios, stored in HPALM on R1138. We have copied them here for convenience:
Business Rules
- If a client has no DSP contract the ticket should be assigned to the Service Desk
- If a client has a DSP Contract that maps to an inactive assignment unit (e.g. an IT Partner who has not onboarded yet) the ticket should be assigned to the Service Desk.
- Medical School Faculty are supported by CTS DSP Teams 2 and 3. The query that flags clients as a FASIT person automatically excludes Medical School faculty from being flagged as FASIT.
- If a client is flagged as a FAS-IT person they should be assigned to the appropriate support contract
- If the client is a faculty member supported by an active IT Partner the ticket should be assigned to the IT Partner assignment unit (see above for what happens if an IT Partner assignment unit is inactive)
- Any other faculty members the ticket should be assigned to DSP Team FASIT
Contract Billing Reports
"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."
Each time worked entry is stamped with the appropriate DSP contract if the current assignment group is the one assigned to the appropriate contract for the client. Examples:
Client Contract | Client Contract Assignment Group | Task Assignment Group | Result |
---|---|---|---|
SRV0000150 (FASIT) | CTS DSP Team FASIT | INF Backup and Storage | current DEV: task time worked is billed to no contract should be in DEV: task time worked is billed to no contract |
SRV0000150 (FASIT) | CTS DSP Team FASIT | CTS DSP Team FASIT | current DEV: task time worked is billed to SRV0000150 (FASIT) should be in DEV: task time worked is billed to SRV0000150 (FASIT) |
SRV0000100 (ITS) | CTS DSP Team 1 | CTS DSP Team 3 | current DEV: task time worked is billed to no contract should be in DEV: task time worked is billed to SRV0000100 (ITS) |
SRV0000100 (ITS) | CTS DSP Team 1 | CTS DSP Team 1 | current DEV: task time worked is billed to SRV0000100 (ITS) should be in DEV: task time worked is billed to SRV0000100 (ITS) |
General algorithm is if Task Assignment Group starts with "CTS DSP Team", AND Client Contract is populated, then bill to Client Contract. If not, do no billing to Client Contract.
Over a month, sums are obtained by making a time-constrained list of task_time_worked records, then reporting on the "Bill to Contract" field. Example:
Task Filtering by DSP Contract
"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."
- In the case of FASIT DSPs, list tasks based on the FASIT status of the client. This can be done from the reporting module or a filter. Example.
- In the case of other DSPs, list tasks based on the contract of the org of the client. This can be done from the reporting module or a filter. Example.
Contract User Counts for Staffing
"DSP managers need counts of the users supported by a given contract so they can appropriately staff the contract support groups."
- FASIT counts can be obtained by tallying the number of active users for whom FASIT==true. Example.
- In the case of non-FASIT users, counts can be obtained via a list of active users for whom FASIT==false, grouped by the contract of the users' orgs. Example: