AVATAX.REST(Suitescript file)
define('AvaTaxRest'
  , [
  ]
  , function AvaTaxRest() {
    'use strict';
    var Base64 = {
      _keyStr: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=", encode: function (e) {
        var t = "";
        var n, r, i, s, o, u, a;
        var f = 0;
        e = Base64._utf8_encode(e);
        while (f < e.length) {
          n = e.charCodeAt(f++);
          r = e.charCodeAt(f++);
          i = e.charCodeAt(f++);
          s = n >> 2;
          o = (n & 3) << 4 | r >> 4;
          u = (r & 15) << 2 | i >> 6;
          a = i & 63;
          if (isNaN(r)) {
            u = a = 64
          } else if (isNaN(i)) {
            a = 64
          }
          t = t + this._keyStr.charAt(s) + this._keyStr.charAt(o) + this._keyStr.charAt(u) + this._keyStr.charAt(a)
        }
        return t
      }, decode: function (e) {
        var t = "";
        var n, r, i;
        var s, o, u, a;
        var f = 0;
        e = e.replace(/[^A-Za-z0-9+/=]/g, "");
        while (f < e.length) {
          s = this._keyStr.indexOf(e.charAt(f++));
          o = this._keyStr.indexOf(e.charAt(f++));
          u = this._keyStr.indexOf(e.charAt(f++));
          a = this._keyStr.indexOf(e.charAt(f++));
          n = s << 2 | o >> 4;
          r = (o & 15) << 4 | u >> 2;
          i = (u & 3) << 6 | a;
          t = t + String.fromCharCode(n);
          if (u != 64) {
            t = t + String.fromCharCode(r)
          }
          if (a != 64) {
            t = t + String.fromCharCode(i)
          }
        }
        t = Base64._utf8_decode(t);
        return t
      }, _utf8_encode: function (e) {
        e = e.replace(/rn/g, "n");
        var t = "";
        for (var n = 0; n < e.length; n++) {
          var r = e.charCodeAt(n);
          if (r < 128) {
            t += String.fromCharCode(r)
          } else if (r > 127 && r < 2048) {
            t += String.fromCharCode(r >> 6 | 192);
            t += String.fromCharCode(r & 63 | 128)
          } else {
            t += String.fromCharCode(r >> 12 | 224);
            t += String.fromCharCode(r >> 6 & 63 | 128);
            t += String.fromCharCode(r & 63 | 128)
          }
        }
        return t
      }, _utf8_decode: function (e) {
        var t = "";
        var n = 0;
        var r = c1 = c2 = 0;
        while (n < e.length) {
          r = e.charCodeAt(n);
          if (r < 128) {
            t += String.fromCharCode(r);
            n++
          } else if (r > 191 && r < 224) {
            c2 = e.charCodeAt(n + 1);
            t += String.fromCharCode((r & 31) << 6 | c2 & 63);
            n += 2
          } else {
            c2 = e.charCodeAt(n + 1);
            c3 = e.charCodeAt(n + 2);
            t += String.fromCharCode((r & 15) << 12 | (c2 & 63) << 6 | c3 & 63);
            n += 3
          }
        }
        return t
      }
    }
    function b64EncodeUnicode(str) {
      // first we use encodeURIComponent to get percent-encoded UTF-8,
      // then we convert the percent encodings into raw bytes which
      // can be fed into btoa.
      return Base64.encode(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g,
        function toSolidBytes(match, p1) {
          return String.fromCharCode('0x' + p1);
        }));
    }
    function createBasicAuthHeader(account, licenseKey) {
      var base64Encoded = b64EncodeUnicode(account + ':' + licenseKey);
      return 'Basic ' + base64Encoded;
    }
    function withTimeout(msecs, promise) {
      var timeout = new Promise(function (resolve, reject) {
        setTimeout(function () {
          reject(new Error('timeout'));
        }, msecs);
      });
      return Promise.race([timeout, promise]);
    }
    function AvaTaxClient(config) {
      this.baseUrl = 'https://rest.avatax.com';
      if (config.environment == 'sandbox') {
        this.baseUrl = 'https://sandbox-rest.avatax.com';
      } else if (
        config.environment.substring(0, 8) == 'https://' ||
        config.environment.substring(0, 7) == 'http://'
      ) {
        this.baseUrl = config.environment;
      }
      this.clientId =
        config.appName +
        '; ' +
        config.appVersion +
        '; JavascriptSdk; 20.7.0; ' +
        config.machineName;
      return this;
    }
    /**
     * Configure this client to use the specified username/password security settings
     *
     * @param  string          username        The username for your AvaTax user account
     * @param  string          password        The password for your AvaTax user account
     * @param  int             accountId       The account ID of your avatax account
     * @param  string          licenseKey      The license key of your avatax account
     * @param  string          bearerToken     The OAuth 2.0 token provided by Avalara Identity
     * @return AvaTaxClient
     */
    AvaTaxClient.prototype.withSecurity = function (securityConfig) {
      if (securityConfig.username != null && securityConfig.password != null) {
        this.auth = createBasicAuthHeader(securityConfig.username, securityConfig.password);
      } else if (securityConfig.accountId != null && securityConfig.licenseKey != null) {
        this.auth = createBasicAuthHeader(securityConfig.accountId, securityConfig.licenseKey);
      } else if (securityConfig.bearerToken != null) {
        this.auth = 'Bearer ' + securityConfig.bearerToken;
      }
      return this;
    }
    /**
     * Make a single REST call to the AvaTax v2 API server
     *
     * @param   string  url        The relative path of the API on the server
     * @param   string  verb       The HTTP verb being used in this request
     * @param   string  payload    The request body, if this is being sent to a POST/PUT API call
     */
    AvaTaxClient.prototype.restCall = function (url, verb, payload) {
      return withTimeout(1200000, fetch(url, {
        method: verb,
        headers: {
          Accept: 'application/json',
          'Content-Type': 'application/json',
          Authorization: this.auth,
          'X-Avalara-Client': this.clientId
        },
        body: (payload === null) ? (null) : (JSON.stringify(payload))
      })).then(function (res) {
        var contentType = res.headers._headers['content-type'][0];
        if (contentType === 'application/vnd.ms-excel' || contentType === 'text/csv') {
          return res;
        }
        return res.json();
      }).then(function (json) {
        // handle error
        if (json.error) {
          var ex = new Error(json.error.message);
          ex.code = json.error.code;
          ex.target = json.error.target;
          ex.details = json.error.details;
          throw ex;
        } else {
          return json;
        }
      })
    }
    AvaTaxClient.prototype.restCallNlapi = function (url, verb, payload) {
      var requestheaders = {
        "Accept": "application/json",
        "Content-Type": "application/json",
        "Authorization": this.auth,
        "X-Avalara-Client": this.clientId
      };
      var payloadData = (payload === null) ? (null) : (JSON.stringify(payload));
      var response = nlapiRequestURL(url, null, requestheaders, verb);
      var error = response.getError();
      if (error != null) {
        var headers = response.getAllHeaders();
        var output = 'Code: ' + response.getCode() + '\n';
        output += 'Headers:\n';
        for (var i in headers)
          output += i + ': ' + headers[i] + '\n';
        output += '\n\nBody:\n\n';
        output += response.getBody();
        nlapiLogExecution("DEBUG", "url", url);
        nlapiLogExecution("DEBUG", "response", output);
        if (error instanceof nlobjError)
          nlapiLogExecution('DEBUG', 'system error', error.getCode() + '\n' + error.getDetails())
        else
          nlapiLogExecution('DEBUG', 'unexpected error', error.toString())
      } else {
        return response;
      }
    }
    /**
     * Construct a URL with query string parameters
     *
     * @param   string  url            The root URL of the API being called
     * @param   string  parameters     A list of name-value pairs in a javascript object to create as query string parameters
     */
    AvaTaxClient.prototype.buildUrl = function (urlConfig) {
      var qs = '';
      for (var key in urlConfig.parameters) {
        var value = urlConfig.parameters[key];
        if (value) {
          qs += encodeURIComponent(key) + '=' + encodeURIComponent(value) + '&';
        }
      }
      if (qs.length > 0) {
        qs = qs.substring(0, qs.length - 1); //chop off last "&"
        urlConfig.url = urlConfig.url + '?' + qs;
      }
      return this.baseUrl + urlConfig.url;
    }
    /**
       * Retrieve geolocation information for a specified address
       *
       * Resolve an address against Avalara's address-validation system. If the address can be resolved, this API
         * provides the latitude and longitude of the resolved location. The value 'resolutionQuality' can be used
         * to identify how closely this address can be located. If the address cannot be clearly located, use the
         * 'messages' structure to learn more about problems with this address.
         * This is the same API as the POST /api/v2/addresses/resolve endpoint.
         * Both verbs are supported to provide for flexible implementation.
         *  
         * Inorder to get any evaluation for an address please provide atleast one of the following fields/pairs:
         * 1. postal code
         * 2. line1 + city + region
         * 3. line1 + postal code
         * 
         * ### Security Policies
         * 
         * * This API requires one of the following user roles: AccountAdmin, AccountOperator, AccountUser, CompanyAdmin, CompanyUser, CSPTester, SSTAdmin, TechnicalSupportAdmin, TechnicalSupportUser.
         * * This API depends on the following active services<br />*Required* (all): AutoAddress.
       *
       * 
         * @param string line1 Line 1
         * @param string line2 Line 2
         * @param string line3 Line 3
         * @param string city City
         * @param string region State / Province / Region
         * @param string postalCode Postal Code / Zip Code
         * @param string country Two character ISO 3166 Country Code (see /api/v2/definitions/countries for a full list)
         * @param string textCase selectable text case for address validation (See TextCase::* for a list of allowable values)
       * @return object
       */
    AvaTaxClient.prototype.resolveAddress = function (addressObject) {
      var path = this.buildUrl({
        url: '/api/v2/addresses/resolve',
        parameters: {
          line1: addressObject.Line1,
          line2: addressObject.Line2,
          line3: addressObject.Line3,
          city: addressObject.City,
          region: addressObject.Region,
          postalCode: addressObject.PostalCode,
          country: addressObject.Country,
          textCase: addressObject.TextCase
        }
      });
      return this.restCallNlapi(path, 'GET', null);
    }
    /**
     * Retrieve geolocation information for a specified address
     *
     * Resolve an address against Avalara's address-validation system. If the address can be resolved, this API
       * provides the latitude and longitude of the resolved location. The value 'resolutionQuality' can be used
       * to identify how closely this address can be located. If the address cannot be clearly located, use the
       * 'messages' structure to learn more about problems with this address.
       * This is the same API as the GET /api/v2/addresses/resolve endpoint.
       * Both verbs are supported to provide for flexible implementation.
       * 
       * ### Security Policies
       * 
       * * This API requires one of the following user roles: AccountAdmin, AccountOperator, AccountUser, CompanyAdmin, CompanyUser, CSPTester, SSTAdmin, TechnicalSupportAdmin, TechnicalSupportUser.
       * * This API depends on the following active services<br />*Required* (all): AutoAddress.
     *
     * 
       * @param object model The address to resolve
     * @return object
     */
    AvaTaxClient.prototype.resolveAddressPost = function (model) {
      var path = this.buildUrl({
        url: '/api/v2/addresses/resolve',
        parameters: {}
      });
      return this.restCallNlapi(path, 'POST', model);
    }
    /**
     * Tests connectivity and version of the service
     *
     * Check connectivity to AvaTax and return information about the AvaTax API server.
       *  
       * This API is intended to help you verify that your connection is working. This API will always succeed and will
       * never return a error. It provides basic information about the server you connect to:
       *  
       * * `version` - The version number of the AvaTax API server that responded to your request. The AvaTax API version number is updated once per month during Avalara's update process.
       * * `authenticated` - A boolean flag indicating whether or not you sent valid credentials with your API request.
       * * `authenticationType` - If you provided valid credentials to the API, this field will tell you whether you used Bearer, Username, or LicenseKey authentication.
       * * `authenticatedUserName` - If you provided valid credentials to the API, this field will tell you the username of the currently logged in user.
       * * `authenticatedUserId` - If you provided valid credentials to the API, this field will tell you the user ID of the currently logged in user.
       * * `authenticatedAccountId` - If you provided valid credentials to the API, this field will contain the account ID of the currently logged in user.
       *  
       * This API helps diagnose connectivity problems between your application and AvaTax; you may call this API even
       * if you do not have verified connection credentials. If this API fails, either your computer is not connected to
       * the internet, or there is a routing problem between your office and Avalara, or the Avalara server is not available.
       * For more information on the uptime of AvaTax, please see [Avalara's AvaTax Status Page](https://status.avalara.com/).
       * 
       * ### Security Policies
       * 
       * * This API may be called without providing authentication credentials.
     *
     * 
     * @return object
     */
    AvaTaxClient.prototype.ping = function () {
      var path = this.buildUrl({
        url: '/api/v2/utilities/ping',
        parameters: {}
      });
      return this.restCallNlapi(path, 'GET', null);
    }
    return AvaTaxClient;
  });Extend address model and add a validation function(Suitescript)
define('JJ.AvaTax.AvaTax'
    , [
        'JJ.AvaTax.AvaTax.ServiceController',
        'Address.Model',
        'Models.Init',
        'AvaTaxRest',
        'Backbone.Validation',
        'underscore',
        'Configuration'
    ]
    , function (
        AvaTaxServiceController,
        AddressModel,
        ModelsInit,
        AvaTaxRest,
        BackboneValidation,
        _,
        Configuration
    ) {
        'use strict';
        var countries
            , states = {};
        _.extend(AddressModel, {
            name: 'Address',
            // @property validation
            validation: {
                addressee: {
                    required: true,
                    msg: 'Full Name is required'
                },
                addr1: {
                    required: true,
                    msg: 'Address is required'
                },
                country: {
                    required: true,
                    msg: 'Country is required'
                },
                state: function (value, attr, computedState) {
                    var selected_country = computedState.country;
                    if (selected_country) {
                        if (!states[selected_country]) {
                            states[selected_country] = ModelsInit.session.getStates([selected_country]);
                        }
                        if (selected_country && states[selected_country] && !value) {
                            return 'State is required';
                        }
                    } else {
                        return 'Country is required';
                    }
                },
                city: {
                    required: true,
                    msg: 'City is required'
                },
                zip: function (value, attr, computedState) {
                    var selected_country = computedState.country;
                    countries = countries || ModelsInit.session.getCountries();
                    if (
                        (!selected_country && !value) ||
                        (selected_country &&
                            countries[selected_country] &&
                            countries[selected_country].isziprequired === 'T' &&
                            !value)
                    ) {
                        return 'State is required';
                    }
                },
                phone: function (value) {
                    if (value) {
                        var regex = new RegExp('^[+]*[(]{0,1}[0-9]{1,4}[)]{0,1}[-\\s\\./0-9]*$');
                        if (!regex.test(value)) {
                            return 'Phone Number is invalid';
                        }
                    } else if (
                        Configuration.get('addresses') &&
                        Configuration.get('addresses.isPhoneMandatory')
                    ) {
                        return 'Phone Number is required';
                    }
                }
            },
            isValid: function (data) {
                data = this.unwrapAddressee(_.clone(data));
                var validator = _.extend(
                    {
                        validation: this.validation,
                        attributes: data
                    },
                    BackboneValidation.mixin
                );
                validator.validate();
                return validator.isValid();
            },
            // @method wrapAddressee
            // our model has "fullname" and "company" insted of  the fields "addresse" and "attention" used on netsuite.
            // this function prepare the address object for sending it to the frontend
            // @param {Object} address
            // @returns {Object} address
            wrapAddressee: function (address) {
                if (address.attention && address.addressee) {
                    address.fullname = address.attention;
                    address.company = address.addressee;
                } else {
                    address.fullname = address.addressee;
                    address.company = null;
                }
                delete address.attention;
                delete address.addressee;
                return address;
            },
            // @method unwrapAddressee
            // @param {Object} address
            // @returns {Object} address
            unwrapAddressee: function (address) {
                if (address.company && address.company.trim().length > 0) {
                    address.attention = address.fullname;
                    address.addressee = address.company;
                } else {
                    address.addressee = address.fullname;
                    address.attention = null;
                }
                delete address.fullname;
                delete address.company;
                delete address.check;
                return address;
            },
            validateAddress: function (address) {
                try {
                    // resolve configuration and credentials
                    var config = {
                        appName: 'artina-sca',
                        appVersion: '1.0',
                        environment: 'sandbox',
                        machineName: 'artina-netsuite'
                    };
// Avatax credential of sandbox
                    var sandboxcreds = {
                        username: '200******0',
                        password: '981*****D080D51'
                    };
// Avatax credential of production
                    var productioncreds = {
                        username: '200****',
                        password: '29E9****F89'
                    };
                    var avatax = new AvaTaxRest(config).withSecurity(sandboxcreds);
                    console.log("avatax",avatax);
                    var addressObject = {
                        AddressCode: "destination",
                        Line1: address.addr1,
                        Line2: address.addr2,
                        City: address.city,
                        Region: address.state,
                        PostalCode: address.zip,
                        Country: address.country
                    };
                    console.log("addressObject",addressObject);
                    try {
                        var avaresponse = avatax.resolveAddress(addressObject);
                        console.log("avaresponse",avaresponse);
                        var responseBody = avaresponse.getBody();
                        console.log("responseBody",responseBody);
                        var responseObject = JSON.parse(responseBody);
                        console.log("responseObject",responseObject);
                        console.log("responseObject string",JSON.stringify(responseObject));
                        var returnedZip = responseObject['validatedAddresses'][0]['postalCode'].substring(0, 5);
                        console.log("returnedZip",returnedZip);
                        console.log("responseObject['validatedAddresses']",responseObject['validatedAddresses']);
                        //if the validated zip does not match what the customer entered, update the address to use the validated zip
                        if (address.zip != returnedZip)
                            address.zip = returnedZip;
                    }
                    catch (err) {
                        nlapiLogExecution("DEBUG", "AvaTax Error", err.name + ' ' + err.message);
                    }
                    finally {
                        return address;
                    }
                }
                catch (e) {
                    console.log("Error in Avatax", e)
                }
            },
            // @method get
            // @param {Number} id
            // @returns {Object} address
            get: function (id) {
                // @class Address.Model.Attributes
                // @property {String} company
                // @property {String} fullname
                // @property {String} internalid
                // @property {String} defaultbilling Valid values are 'T' or 'F'
                // @property {String} defaultshipping Valid values are 'T' or 'F'
                // @property {String} isvalid Valid values are 'T' or 'F'
                // @property {String} isresidential Valid values are 'T' or 'F'
                // @property {String?} addr3
                // @property {String} addr2
                // @property {String} addr1
                // @property {String} country
                // @property {String} city
                // @property {String} state
                // @property {String} phone
                // @property {String} zip
                // @class Address.Model
                return this.wrapAddressee(ModelsInit.customer.getAddress(id));
            },
            // @method getDefaultBilling
            // @returns {Object} default billing address
            getDefaultBilling: function () {
                return _.find(ModelsInit.customer.getAddressBook(), function (address) {
                    return address.defaultbilling === 'T';
                });
            },
            // @method getDefaultShipping
            // @returns {Object} default shipping address
            getDefaultShipping: function () {
                return _.find(ModelsInit.customer.getAddressBook(), function (address) {
                    return address.defaultshipping === 'T';
                });
            },
            // @method list
            // @returns {Array<Object>} all user addresses
            list: function () {
                var self = this;
                return _.map(ModelsInit.customer.getAddressBook(), function (address) {
                    return self.wrapAddressee(address);
                });
            },
            // @method update
            // updates a given address
            // @param {String} id
            // @param {String} data
            // @returns undefined
            update: function (id, data) {
                data = this.unwrapAddressee(data);
                // validate the model
                this.validate(data);
                data.internalid = id;
                this.validateAddress(data);
                return ModelsInit.customer.updateAddress(data);
            },
            // @method create
            // creates a new address
            // @param {Address.Data.Model} data
            // @returns {String} key of the new address
            create: function (data) {
                data = this.unwrapAddressee(data);
                // validate the model
                this.validate(data);
                this.validateAddress(data);
                return ModelsInit.customer.addAddress(data);
            },
            // @method remove
            // removes a given address
            // @param {String} id
            // @returns undefined
            remove: function (id) {
                return ModelsInit.customer.removeAddress(id);
            }
        });
    });