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);
}
});
});