-
Notifications
You must be signed in to change notification settings - Fork 6
/
usps.js
186 lines (147 loc) · 7.66 KB
/
usps.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
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', '10', '14', '30', '81', '82', 'AD', '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;