Maik Skoddow
Tera Patron
Tera Patron
find_real_file.png
This article is not an introduction to Script Includes, as there are a lot of excellent webpages explaining that script type. I personally like this tutorial: https://codecreative.io/blog/glideajax-troubleshooting-guide/

One of the most important script type at ServiceNow is the "Script Include". It is like a library where you can put all the business logic of your application in to have one single place for maintenance. An experienced developer first designs a Script Include and then tests it extensively before using its methods in other types of scripts like Business Rules, Reference Qualifiers, Scheduled Jobs, etc. An interesting aspect of Script Includes is that they can be called not only on the server side, but also on the client side via an Ajax-based request.

Due to their immense importance, this article presents a generally applicable implementation pattern for Script Includes that covers all invocation variants and also has a standard structure to support clean and maintainable code.

 

        
Table of Contents

 

Challenge

 

In one of my projects, a new Catalog Item was required where the customer wanted to enter an amount of money and select the related currency. That amount then should be converted to the currency "Euro" on the basis of a daily updated exchange rate. And the conversion should be performed on client-side because of other reasons that do not play a role here for the time being.

 

Considering these constraints, I was faced with two problems:

  1. ServiceNow does not offer a numeric data type for catalog variables.
  2. On the one hand, the currency API accepts only the English notation for numbers - with a dot as decimal separator and commas as grouping separator (1,000.00). On the other hand, we mainly had users who wanted to enter the numbers in the European variant, i.e. with a comma as decimal separator and dots as grouping separator (1.000,00).

 

 

Solution

 

Fortunately the conversion is no problem as ServiceNow provides the proper APIs. And also conversions of number formats based on a locale can be done via that API. Due to the fact that these APIs are offered only on the server side, a Script Include is needed which can be called from the client side. But during the implementation and for testing purposes, I wanted to be able of reusing the business logic on the server side too. In such scenarios, a common approach is creating a "proxy" Script Include which is client callable and redirects all invocations to another Script Include holding the business logic:

find_real_file.png

 

 

You can identify these Script Includes by their name, as they often end in "Ajax":

find_real_file.png

 

I am not a fan of this kind of code redundancy because the maintenance efforts are correspondingly high and also the defect potential increases accordingly.

 

For this reason, I prefer the approach with only one script include, which can not only be invoked from client side but also contains all the business logic.

 

 

Usage

 

On Server Side

 

In the following example, all possible parameters are passed to the convert() method. With the explicitly given locale de_DE  the number format can be interpreted accordingly:

find_real_file.png 

 

On Client Side

 

For testing purposes I recommend installing my GlideAjax Test Client. With the help of this Catalog Item you can simply click together the invocation of the method CurrencyUtils.convert() and get the corresponding source code at the end.

 

But you also can copy the following code block:

 

 

var gaRequest = new GlideAjax('CurrencyUtils');

gaRequest.addParam('sysparm_name', 'convert');
gaRequest.addParam('sysparm_source_currency', 'USD');
gaRequest.addParam('sysparm_target_currency', 'EUR');
gaRequest.addParam('sysparm_original_amount', '112,45');
gaRequest.addParam('sysparm_locale', 'de_DE');

gaRequest.getXMLAnswer(function(strAnswer) {
  var jsonAnswer = JSON.parse(strAnswer);

  if (jsonAnswer.error_message.length > 0) {
    alert(jsonAnswer.error_message);
  }
  else {
    alert(
      jsonAnswer.original_amount + ' ' + jsonAnswer.source_currency + ' make ' +
      jsonAnswer.converted_amount + ' ' + jsonAnswer.target_currency + 
      ' at an exchange rate of ' + jsonAnswer.exchange_rate
    );
  }
});

 

 



Source Code explained

 

You can download the complete source code of the Script Include right from my public GitHub repo: CurrencyUtils.txt

 

The referenced LogHelper class is described in detail in my article Just another Log Helper.

 

 

(1) Constant Definitions

 

At the beginning of a Script Include you should define some constants, which will be used later. Constants help to make the code more maintainable and can tell you what the required parameter names should be when you first look at the source code. Constant names should be all uppercase, with words separated by underscores ("_"). In ServiceNow, they also should be prepended with the class name and separated with a dot in order to indicate which class the constants belong to. 

 

 

var CurrencyUtils = Class.create();

CurrencyUtils.METHOD_CONVERT = 'CurrencyUtils.convert';

CurrencyUtils.METHOD_PARAMETER_SOURCECURRENCY = 'strSourceCurrency';
CurrencyUtils.METHOD_PARAMETER_TARGETCURRENCY = 'strTargetCurrency';
CurrencyUtils.METHOD_PARAMETER_ORIGINALAMOUNT = 'strOriginalAmount';
CurrencyUtils.METHOD_PARAMETER_LOCALE         = 'strLocale';

CurrencyUtils.AJAX_PARAMETER_SOURCECURRENCY   = 'sysparm_source_currency';
CurrencyUtils.AJAX_PARAMETER_TARGETCURRENCY   = 'sysparm_target_currency';
CurrencyUtils.AJAX_PARAMETER_ORIGINALAMOUNT   = 'sysparm_original_amount';
CurrencyUtils.AJAX_PARAMETER_LOCALE           = 'sysparm_locale';

 

 

Note:
In larger projects with many artifacts, it may make sense to create a Script Include that exists exclusively for the purpose of specifying constants. The names of such Script Includes often end in "Const". If you want to learn more regarding that topic, please watch the following video: https://www.youtube.com/watch?v=B94UUQPDyDg

 

(2) Extending AbstractAjaxProcessor

 

This definition enables your Script Include to handle client-side calls:

 

 

CurrencyUtils.prototype = Object.extendsObject(global.AbstractAjaxProcessor, {

 

 

Note:

  • Please be aware that you can't define an initialize() method here! As a consequence, when instantiating via the new operator, you have no constructor via which initialization values can be passed. However, you can initialize values as usual via setter methods
  • Make sure that also the checkbox "Client callable" in the Script Include record is set:

    find_real_file.png

 

(3) Public Methods

 

In this part of the Script Include you define all public methods, i.e. those that can be invoked from outside the class by other scripts.

 

(3.1) Method definition

 

According to the "camel case" naming conventions, methods should be verbs, in mixed case with the first letter lowercase and with the first letter of each internal word capitalized. Inside the round brackets the parameter names are listed for which corresponding values must be passed to the method. The parameter names also follow the "camel case" notation. A prefix, which represents the expected data type ("str"), is not mandatory, but helps tremendously when reading the source code.

 

Note:
The parameter names only matter when calling the method from another server-side script. For a client-side invocation, the parameter passing is done differently, as we will see later.

 

 

/**
 * Converts a number for a given currency to the value of a target currency with the help of an OOTB 
 * ServiceNow API. The conversion rates are determined automatically on a daily basis by ServiceNow.
 *
 *  {String} strSourceCurrency 
 *        Original currency of the given amount to be converted as a 3-letter code
 *        defined by the <a href="https://en.wikipedia.org/wiki/ISO_4217">ISO 4217</a>.
 *  {String} strTargetCurrency 
 *        Target currency the given amount should be converted to as a 3-letter code
 *        defined by the <a href="https://en.wikipedia.org/wiki/ISO_4217">ISO 4217</a>.
 * @return {JSON} 
 *         Value object containing all information. In casde of errors the property `error_message` 
 *         contains a respective message.
 */
convert: function(strSourceCurrency, strTargetCurrency, strOriginalAmount, strLocale) {​

 

 

Above the single line of code you can see a comments block which follows a certain notation. This form is based on the standards for commenting Java code. At first glance, this may seem unreadable, but there are ways to have HTML-based documentation generated from these code comments (see https://jsdoc.app/). 

 

Code documentation is extremely important, because it helps the developer to understand how the method is used, which method parameters are passed and which return value is to be expected, and you should follow these conventions.

 

(3.2) Local constants

 

Right at the beginning of the method body and outside the following try-catch block, I define the local constants. Of central importance here is the constant IS_CLIENT_CALL, which indicates whether the method invocation was made from the client side or from the server side.

 

 

var IS_CLIENT_CALL = JSUtil.notNil(this.getName());
var SESSION_LOCALE = String(GlideLocale.get().getCurrent());

var PARAMETER_SOURCECURRENCY = 
            IS_CLIENT_CALL ? 
            CurrencyUtils.AJAX_PARAMETER_SOURCECURRENCY : 
            CurrencyUtils.METHOD_PARAMETER_SOURCECURRENCY;

var PARAMETER_TARGETCURRENCY = 
            IS_CLIENT_CALL ? 
            CurrencyUtils.AJAX_PARAMETER_TARGETCURRENCY : 
            CurrencyUtils.METHOD_PARAMETER_TARGETCURRENCY;

var PARAMETER_ORGINALAMOUNT  = 
            IS_CLIENT_CALL ? 
            CurrencyUtils.AJAX_PARAMETER_ORIGINALAMOUNT : 
            CurrencyUtils.METHOD_PARAMETER_ORIGINALAMOUNT;

var PARAMETER_LOCALE = 
            IS_CLIENT_CALL ? 
            CurrencyUtils.AJAX_PARAMETER_LOCALE : 
            CurrencyUtils.METHOD_PARAMETER_LOCALE;

 

 

(3.3) Helper for Building the Return Values

 

For public methods I always return an object with several values and the local method _getReturnObject() helps to build that object. Depending on whether a client-side or server-side invocation was performed the stringified version or the native object is returned.

 

Note:

  • The LogHelper referenced here is described in detail in my article Just another Log Helper.
  • arguments[0], arguments[1] and so on is another way to grab the method parameters.
  • With commands like String(arguments[0] || '') it is ensured that a defined String value is returned.
  • For local variables and methods I prefer a notation with a leading underscore. That way I can better distinguish between global and local variables.

 

 

var _getReturnObject = function() {
    var _objReturn = {
        error_message:     String(arguments[0] || ''),
        original_currency: String(arguments[1] || ''),
        target_currency:   String(arguments[2] || ''),
        original_amount:   String(arguments[3] || ''),
        converted_amount:  String(arguments[4] || ''),
        exchange_rate:     String(arguments[5] || ''),
        used_locale:       String(arguments[6] || ''),
    };

    var _strReturn = JSON.stringify(_objReturn);

    LogHelper.debug(CurrencyUtils.METHOD_CONVERT, 'Return Values: {0}', _strReturn);

    return IS_CLIENT_CALL ? _strReturn : _objReturn;
};

 

 

 

(3.4) Determination of the parameter values

 

This is the most critical part, because here the parameter values are pulled from the respective source.

 

Note:
The forced conversion to a string using String() is very important: When passed as a method parameter, in contrast to real high-level languages, no type can be enforced and in case of a client-side call, the invocation of this.getParameter() always returns an object and not a string value as expected.

 

 

try {
    var _strOriginalCurrency = 
        String(strOriginalCurrency || 
        this.getParameter(CurrencyUtils.AJAX_PARAMETER_ORIGINALCURRENCY)).trim();

    var _strTargetCurrency = 
        String(strTargetCurrency || 
        this.getParameter(CurrencyUtils.AJAX_PARAMETER_TARGETCURRENCY)).trim();
    
    var _strOriginalAmount = 
        String(strOriginalAmount || 
        this.getParameter(CurrencyUtils.AJAX_PARAMETER_ORIGINALAMOUNT)).trim();   

    var _strLocale = 
        String(strLocale || 
        this.getParameter(CurrencyUtils.AJAX_PARAMETER_LOCALE) || _strSessionLocale).trim();

 



(3.5) Debug logging

 

Before further processing, it can be helpful to generate a log output in order to better understand which parameter values were passed in case of an error.

 

 

LogHelper.debug(
    CurrencyUtils.METHOD_CONVERT,
    'Entering method with:\n<{0}> = >{1}<\n<{2}> = >{3}<\n<{4}> = >{5}<\n<{6}> = >{7}<',
    PARAMETER_SOURCECURRENCY, _strSourceCurrency, 
    PARAMETER_TARGETCURRENCY, _strTargetCurrency, 
    PARAMETER_ORGINALAMOUNT, _strOriginalAmount,
    PARAMETER_LOCALE, _strLocale
);

 

 

 

(3.6) Plausibility checks

 

The most extensive part is checking for valid parameter values. The purpose is to cover as many cases as possible that could prevent further processing. This includes not only the check for missing values, but also whether the parameter values are technically correct. In the case of invalid parameter values, it is critical to return clear error messages that can be processed further on the consumer side accordingly.

 

 

// source currencey code is not valid
if (!this._isValidCurrencyCode(_strSourceCurrency)) {
    return _getReturnObject(
        LogHelper.error(
            CurrencyUtils.METHOD_CONVERT, 
            '>{0}< is not a valid currency code at <{1}>!',
            _strSourceCurrency, PARAMETER_SOURCECURRENCY
        )
    );
}

// target currencey code is not valid
if (!this._isValidCurrencyCode(_strTargetCurrency)) {
    return _getReturnObject(
        LogHelper.error(
            CurrencyUtils.METHOD_CONVERT, 
            '>{0}< is not a valid currency code at <{1}>!',
            _strTargetCurrency, PARAMETER_TARGETCURRENCY
        )
    );
}

// locale has not the required format
if (!new RegExp('^[a-z]{2}_[A-Z]{2}$').test(_strLocale)) {
    return _getReturnObject(
        LogHelper.error(
            CurrencyUtils.METHOD_CONVERT, 
            'Value >{0}< at <{1}> does not represent a valid format for a locale identifier!',
            _strLocale, PARAMETER_LOCALE)
    );                
}

// normalize amount to english format
var _strNormalizedOriginalAmount = this._getNormalizedNumber(_strOriginalAmount, _strLocale);

// amount is not a valid number for the given locale
if (_strNormalizedOriginalAmount == null) {
    return _getReturnObject(
        LogHelper.error(
            CurrencyUtils.METHOD_CONVERT, 
            'Value >{0}< at <{1}> does not represent a valid number for the locale >{2}<!',
            _strOriginalAmount, PARAMETER_ORGINALAMOUNT, _strLocale)
    );
}

 

 

 

The following example of a wrong currency code demonstrates how the return values look like:

find_real_file.png

 

(3.7) Currency Conversion

 

After a long way, the conversion of the given amount into the target currency can finally be performed. And even here, problems can occur and must be handled accordingly.

 

 

// preconfigure the currency converter
var gcvConverter = new sn_currency.GlideCurrencyConverter(_strSourceCurrency, _strTargetCurrency);

// set the value to be converted
gcvConverter.setAmount(_strNormalizedOriginalAmount);

// perform currency conversion
var _gcevConversionResult = gcvConverter.convert();

// something went wrong
if (_gcevConversionResult == null) {
    return _getReturnObject(
        LogHelper.warn(
            CurrencyUtils.METHOD_CONVERT, 
            'Value >{0}< for orignal currency >{1}< could not be converted to target currency >{2}<',
            _strOriginalAmount, _strSourceCurrency, _strTargetCurrency
        )
    );
}

 

 

(3.8) Return all values

 

In case of a successful conversion, all values are bundled by _getReturnObject() and returned to the caller.

 

 

return _getReturnObject(
    '',
    _strSourceCurrency, 
    _strTargetCurrency, 
    _strOriginalAmount, 
    this._getDenormalizedNumber(_gcevConversionResult.getAmount(), _strLocale),
    this._getDenormalizedNumber(_gcevConversionResult.getRate(), _strLocale, 6),
    _strLocale
);

 


 

(3.9) Handling of unexpected errors

 

The try-catch block is your safety net that catches all the errors you didn't think of during development. And also a proper error handling is necessary to complete the method call successfully.

 

 

}
catch (e) {
    return _getReturnObject(
        LogHelper.fatal(CurrencyUtils.METHOD_CONVERT, 'Unexpected error!', e)
    );
}

 



(4) Private Helper Methods

 

At the end of a class I always place all methods which are only used internally. Because of the leading underscore "_" these methods cannot be invoked from outside the class and thus are defined as "private".

For this reason, the validation of the method parameters can be omitted, since it is ensured from the internal caller side that the values passed are valid.

 

 

_isValidCurrencyCode: function(strCurrencyCode) {
    try {
        var _gcp = new sn_currency.GlideCurrencyParser(); 
        
        _gcp.setDefaultCurrencyCode(strCurrencyCode);
        _gcp.parse("1");

        return true;
    }
    catch (e) {
        return false;
    }
},

_getNormalizedNumber: function(strNumber, strLocale) { 
    var _gcpParser = new sn_currency.GlideCurrencyParser();
    var _arrLocale = strLocale.split('_');

    try {
        _gcpParser.setLocale(_arrLocale[0], _arrLocale[1]); 
        _gcpParser.setDefaultCurrencyCode("USD");
    
        var _gcvParsedValue = _gcpParser.parse(strNumber); 
    
        return _gcvParsedValue.getAmount();
    }
    catch (e) {
        return null;
    }
},

_getDenormalizedNumber: function(strNumber, strLocale, numFractionDigits) {
    try {
        var _gcfCurrencyFormatter = new sn_currency.GlideCurrencyFormatter("%v");
        var _arrLocale            = strLocale.split('_');
        var _parsedInt            = parseInt(numFractionDigits, 10);
        var _numFractionDigits    = isNaN(_parsedInt) ? 2 : Math.abs(_parsedInt);

        _gcfCurrencyFormatter.setLocale(_arrLocale[0], _arrLocale[1]); 

        return _gcfCurrencyFormatter.setMaxFractionDigits(_numFractionDigits).format(strNumber, 'USD');
    }
    catch (e) {
        return null;
    }
},

 

 

 

Comments
vkachineni
Kilo Sage
Kilo Sage

Maik,

In the source code CurrencyUtils, we have the variable "LogHelper". Where is it declared? or is it an SN object?

Thank you.

Maik Skoddow
Tera Patron
Tera Patron

Hi

thanks for your question. 

I have mentioned in the article that this class is described in my article Just another Log Helper, but it seems I have to promote that fact a bit more. I will check how to modify my article.

Kind regards
Maik

vkachineni
Kilo Sage
Kilo Sage

Thanks Maik. You can put a comment in the CurrencyUtils with a link to the LogHelper.

I like the concept of having single file to handle both ajax and non-ajax calls.

 

 

Version history
Last update:
‎12-24-2022 10:38 PM
Updated by:
Contributors