usps-webtools
Version:
Api wrapper for the USPS Web-Tools
289 lines (250 loc) • 7.53 kB
JavaScript
// external dependencies
const request = require('request');
const builder = require('xmlbuilder');
const xml2js = require('xml2js');
// internal dependencies
const USPSError = require('./error.js');
module.exports = class USPS {
constructor(config) {
if (!(config && config.server && config.userId)) {
throw new USPSError('must pass usps server url and userId');
}
this.config = {
ttl: 100000,
// Until jshint 2.10.0 comes out, we have to explicitly ignore spread operators in objects
// jshint ignore:start
...config
// jshint ignore:end
};
}
/**
Verifies an address
@param {Object} address The address to be verified
@param {String} address.street1 Street
@param {String} [address.street2] Secondary street (apartment, etc)
@param {String} address.city City
@param {String} address.state State (two-letter, capitalized)
@param {String} address.zip Zipcode
@param {Function} callback The callback function
@returns {Object} instance of module
*/
verify(address, callback) {
const obj = {
Revision: 1,
Address: {
FirmName: address.firm_name,
Address1: address.street2 || '',
Address2: address.street1,
City: address.city,
State: address.state,
Zip5: address.zip,
Zip4: address.zip4 || ''
}
};
if (address.urbanization) {
obj.Address.Urbanization = address.urbanization;
}
callUSPS('Verify', 'AddressValidate', 'Address', this.config, obj, (err, address) => {
if (err) {
return callback(err);
}
const result = {
street1: address.Address2,
street2: address.Address1 || '',
city: address.City,
zip: address.Zip5,
state: address.State,
zip4: address.Zip4
};
const optional = {
FirmName: 'firm_name',
Address2Abbreviation: 'address2_abbreviation',
CityAbbreviation: 'city_abbreviation',
Urbanization: 'urbanization',
DeliveryPoint: 'delivery_point',
CarrierRoute: 'carrier_route',
Footnotes: 'footnotes',
DPVConfirmation: 'dpv_confirmation',
DPVCMRA: 'dpvcmra',
DPVFalse: 'dpv_false',
DPVFootnotes: 'dpv_footnotes',
Business: 'business',
CentralDeliveryPoint: 'central_delivery_point',
Vacant: 'vacant'
};
Object.keys(optional).forEach(key => {
const resultKey = optional[key];
if (address[key]) {
result[resultKey] = address[key];
}
});
callback(null, result);
});
}
/**
Looks up a zipcode, given an address
@param {Object} address Address to find zipcode for
@param {String} address.street1 Street
@param {String} [address.street2] Secondary street (apartment, etc)
@param {String} address.city City
@param {String} address.state State (two-letter, capitalized)
@param {String} address.zip Zipcode
@param {Function} callback The callback function
@returns {Object} instance of module
*/
zipCodeLookup(address, callback) {
const obj = {
Address: {
Address1: address.street2 || '',
Address2: address.street1,
City: address.city,
State: address.state
}
};
callUSPS('ZipCodeLookup', 'ZipCodeLookup', 'Address', this.config, obj, (err, address) => {
if (err) {
return callback(err);
}
callback(null, {
street1: address.Address2,
street2: address.Address1 ? address.Address1 : '',
city: address.City,
state: address.State,
zip: `${address.Zip5}-${address.Zip4}`
});
});
}
/**
Pricing Rate Lookup, based on USPS RateV4
@param {Object} information about pricing Rate
@param {Function} callback The callback function
@returns {Object} instance of module
*/
pricingRateV4(pricingRate, callback) {
const obj = {
Package: {
'@ID': '1ST',
Service: pricingRate.Service || 'PRIORITY',
ZipOrigination: pricingRate.ZipOrigination || 55401,
ZipDestination: pricingRate.ZipDestination,
Pounds: pricingRate.Pounds,
Ounces: pricingRate.Ounces,
Container: pricingRate.Container,
Size: pricingRate.Size,
Width: pricingRate.Width,
Length: pricingRate.Length,
Height: pricingRate.Height,
Girth: pricingRate.Girth,
Machinable: pricingRate.Machinable
}
};
callUSPS('RateV4', 'RateV4', 'Package', this.config, obj, (err, result) => {
if (err) {
return callback(err);
}
callback(null, result.Postage);
});
}
/**
City State lookup, based on zip
@param {String} zip Zipcode to retrieve city & state for
@param {Function} callback The callback function
@returns {Object} instance of module
*/
cityStateLookup(zip, callback) {
const obj = {
ZipCode: {
Zip5: zip
}
};
callUSPS('CityStateLookup', 'CityStateLookup', 'ZipCode', this.config, obj, (err, address) => {
if (err) {
return callback(err);
}
callback(err, {
city: address.City,
state: address.State,
zip: address.Zip5
});
});
}
};
/**
Method to call USPS
*/
function callUSPS(api, method, property, config, params, callback) {
const requestName = `${method}Request`;
const responseName = `${method}Response`;
const obj = {
[requestName]: {
// Until jshint 2.10.0 comes out, we have to explicitly ignore spread operators in objects
// jshint ignore:start
...params,
// jshint ignore:end
['@USERID']: config.userId
}
};
const xml = builder.create(obj).end();
const opts = {
url: config.server,
qs: {
API: api,
XML: xml
},
timeout: config.ttl
};
request(opts, (err, res, body) => {
if (err) {
return callback(new USPSError(err.message, err, {
method: api,
during: 'request'
}));
}
const parseOptions = {
explicitArray: false
};
xml2js.parseString(body, parseOptions, (err, result) => {
let errMessage;
if (err) {
return callback(new USPSError(err.message, err, {
method: api,
during: 'xml parse'
}));
}
if (!result) {
return callback(new USPSError("No response after parsing XML", {
body
}));
}
// may have a root-level error
if (result.Error) {
try {
errMessage = result.Error.Description.trim();
} catch(e) {
errMessage = result.Error;
}
return callback(new USPSError(errMessage, result.Error));
}
/**
walking the result, to drill into where we want
resultDotNotation looks like 'key.key'
though it may actually have arrays, so returning first cell
*/
let specificResult = {};
if (result && result[responseName] && result[responseName][property]) {
specificResult = result[responseName][property];
}
// specific error handling
if (specificResult.Error) {
try {
errMessage = specificResult.Error.Description.trim();
} catch(e) {
errMessage = specificResult.Error;
}
return callback(new USPSError(errMessage, specificResult.Error));
}
// just peachy
callback(null, specificResult);
});
});
}