Skip to end of metadata
Go to start of metadata

You are viewing an old version of this page. View the current version.

Compare with Current View Page History

« Previous Version 14 Next »

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 correct assignment, 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, the date in the user table became stale and a way was eventually established to update it regularly (see below)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.

Business Rules

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 we didn't feel like adding a role and ACLs at the time, 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;
}

 

 

  • No labels