Skip to content

Commit

Permalink
Merge df39453 into 7da4752
Browse files Browse the repository at this point in the history
  • Loading branch information
aaronmwhitehead committed Jul 3, 2019
2 parents 7da4752 + df39453 commit e0ef97c
Show file tree
Hide file tree
Showing 4 changed files with 436 additions and 3 deletions.
186 changes: 186 additions & 0 deletions carriers/usps.js
@@ -0,0 +1,186 @@
const async = require('async');
const moment = require('moment-timezone');
const parser = require('xml2js');
const request = require('request');
const checkDigit = require('../util/checkDigit');
const geography = require('../util/geography');

// Remove these words from cities to turn cities like `DISTRIBUTION CENTER INDIANAPOLIS` into `INDIANAPOLIS`
const CITY_BLACKLIST = /DISTRIBUTION CENTER|NETWORK DISTRIBUTION CENTER/ig;

// These tracking status codes indicate the shipment was delivered: https://about.usps.com/publications/pub97/pub97_appi.htm
const DELIVERED_TRACKING_STATUS_CODES = ['01'];

// These tracking status codes indicate the shipment was shipped (shows movement beyond a shipping label being created): https://about.usps.com/publications/pub97/pub97_appi.htm
const SHIPPED_TRACKING_STATUS_CODES = ['02', '07', 'OF', 'PC'];

// The events from these tracking status codes are filtered because they do not provide any useful information: https://about.usps.com/publications/pub97/pub97_appi.htm
const TRACKING_STATUS_CODES_BLACKLIST = ['NT'];

function USPS(options) {
this.isTrackingNumberValid = function(trackingNumber) {
// remove whitespace
trackingNumber = trackingNumber.replace(/\s/g, '');
trackingNumber = trackingNumber.toUpperCase();

if ([/^[A-Z]{2}\d{9}[A-Z]{2}$/, /^926129\d{16}$/, /^927489\d{16}$/].some(regex => regex.test(trackingNumber))) {
return true;
}

if (/^\d{20}$/.test(trackingNumber)) {
return checkDigit(trackingNumber, [3, 1], 10);
}

if (/^(91|92|93|94|95|96)\d{20}$/.test(trackingNumber)) {
return checkDigit(trackingNumber, [3, 1], 10);
}

if (/^\d{26}$/.test(trackingNumber)) {
return checkDigit(trackingNumber, [3, 1], 10);
}

if (/^420\d{27}$/.test(trackingNumber)) {
return checkDigit(trackingNumber.match(/^420\d{5}(\d{22})$/)[1], [3, 1], 10);
}

if (/^420\d{31}$/.test(trackingNumber)) {
if (checkDigit(trackingNumber.match(/^420\d{9}(\d{22})$/)[1], [3, 1], 10)) {
return true;
} else if (checkDigit(trackingNumber.match(/^420\d{5}(\d{26})$/)[1], [3, 1], 10)) {
return true;
}
}

return false;
};

this.track = function(trackingNumber, callback) {
const xml = `<TrackFieldRequest USERID="${options.userId}"><Revision>1</Revision><ClientIp>${options.clientIp || '127.0.0.1'}</ClientIp><SourceId>${options.sourceId || '@mediocre/bloodhound (+https://github.com/mediocre/bloodhound)'}</SourceId><TrackID ID="${trackingNumber}"/></TrackFieldRequest>`;

const req = {
baseUrl: options.baseUrl || 'http://production.shippingapis.com',
method: 'GET',
timeout: 5000,
url: `/ShippingAPI.dll?API=TrackV2&XML=${encodeURIComponent(xml)}`
};

async.retry(function(callback) {
request(req, callback);
}, function(err, res) {
if (err) {
return callback(err);
}

parser.parseString(res.body, function(err, data) {
const results = {
events: []
};

if (err) {
return callback(err);
} else if (data.Error) {
// Invalid credentials or Invalid Tracking Number
return callback(new Error(data.Error.Description[0]));
} else if (data.TrackResponse.TrackInfo[0].Error) {
// No Tracking Information
return callback(null, results);
}

const scanDetailsList = [];

// TrackSummary[0] exists for every item (with valid tracking number)
const summary = data.TrackResponse.TrackInfo[0].TrackSummary[0];
scanDetailsList.push(summary);

const trackDetailList = data.TrackResponse.TrackInfo[0].TrackDetail;

// If we have tracking details, push them into statuses
// Tracking details only exist if the item has more than one status update
if (trackDetailList) {
trackDetailList.forEach(trackDetail => {
if (TRACKING_STATUS_CODES_BLACKLIST.includes(trackDetail.EventCode[0])) {
return;
}

scanDetailsList.push(trackDetail);
});
}

// Set address and location of each scan detail
scanDetailsList.forEach(scanDetail => {
scanDetail.address = {
city: scanDetail.EventCity[0].replace(CITY_BLACKLIST, '').trim(),
country: scanDetail.EventCountry[0],
state: scanDetail.EventState[0],
zip: scanDetail.EventZIPCode[0]
};

scanDetail.location = geography.addressToString(scanDetail.address);
});

// Get unqiue array of locations
const locations = Array.from(new Set(scanDetailsList.map(scanDetail => scanDetail.location)));

// Lookup each location
async.mapLimit(locations, 10, function (location, callback) {
geography.parseLocation(location, function (err, address) {
if (err) {
return callback(err);
}

address.location = location;

callback(null, address);
});
}, function (err, addresses) {
if (err) {
return callback(err);
}

scanDetailsList.forEach(scanDetail => {
const address = addresses.find(a => a.location === scanDetail.location);
let timezone = 'America/New_York';

if (address && address.timezone) {
timezone = address.timezone;
}

const event = {
address: scanDetail.address,
date: moment.tz(`${scanDetail.EventDate[0]} ${scanDetail.EventTime[0]}`, 'MMMM D, YYYY h:mm a', timezone).toDate(),
description: scanDetail.Event[0]
};

if (DELIVERED_TRACKING_STATUS_CODES.includes(scanDetail.EventCode[0])) {
results.deliveredAt = event.date;
}

if (SHIPPED_TRACKING_STATUS_CODES.includes(scanDetail.EventCode[0])) {
results.shippedAt = event.date;
}

// Use the city and state from the parsed address (for scenarios where the city includes the state like "New York, NY")
if (address) {
if (address.city) {
event.address.city = address.city;
}

if (address.state) {
event.address.state = address.state;
}
}

results.events.push(event);
});

// Add details to the most recent event
results.events[0].details = data.TrackResponse.TrackInfo[0].StatusSummary[0];

callback(null, results);
});
});
});
}
}

module.exports = USPS;
12 changes: 9 additions & 3 deletions index.js
@@ -1,16 +1,20 @@
const FedEx = require('./carriers/fedEx');
const PitneyBowes = require('./carriers/pitneyBowes');
const FedEx = require('./carriers/fedEx');
const USPS = require('./carriers/usps');

function Bloodhound(options) {
const fedEx = new FedEx(options && options.fedEx);
const pitneyBowes = new PitneyBowes(options && options.pitneyBowes);
const usps = new USPS(options && options.usps);

this.guessCarrier = function(trackingNumber) {
if (fedEx.isTrackingNumberValid(trackingNumber)) {
return 'FedEx';
} else if (usps.isTrackingNumberValid(trackingNumber)) {
return 'USPS';
} else {
return undefined;
}

return undefined;
};

this.track = function(trackingNumber, carrier, callback) {
Expand Down Expand Up @@ -40,6 +44,8 @@ function Bloodhound(options) {
fedEx.track(trackingNumber, callback);
} else if (carrier === 'newgistics') {
pitneyBowes.track(trackingNumber, callback);
} else if (carrier === 'usps') {
usps.track(trackingNumber, callback);
} else {
return callback(new Error(`Carrier ${carrier} is not supported.`));
}
Expand Down

0 comments on commit e0ef97c

Please sign in to comment.