Skip to content

Commit

Permalink
Merge pull request #54 from mediocre/dhl-utapi
Browse files Browse the repository at this point in the history
Dhl utapi
  • Loading branch information
freshlogic committed Jun 8, 2022
2 parents 43bb2eb + 2eb60b8 commit bb2bac3
Show file tree
Hide file tree
Showing 7 changed files with 107 additions and 93 deletions.
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [1.9.0] - 2021-06-08
### Changed
- Updated the DHL API to use https://developer.dhl.com/api-reference/shipment-tracking.

## [1.8.1] - 2021-07-31
### Changed
- Updated the pitney-bowes module to ~0.3.0.
Expand All @@ -16,4 +20,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [1.7.5] - 2021-03-17
### Changed
- Changed the default URL for USPS to use https (instead of http).
- Changed the default URL for USPS to use https (instead of http).
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ By default, when Bloodhound encounters a timestamp without a UTC offset it will
const Bloodhound = require('@mediocre/bloodhound');

const bloodhound = new Bloodhound({
dhl: {
apiKey: 'DHL API key from https://developer.dhl.com'
},
fedEx: {
account_number: '123456789',
environment: 'live',
Expand Down Expand Up @@ -100,6 +103,8 @@ bloodhound.track('tracking number', 'FedEx', function(err, data) {
console.log(data);
});
```
**dhl**
The DHL API requires an API key: https://developer.dhl.com.

**fedEx**

Expand Down
147 changes: 83 additions & 64 deletions carriers/dhl.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const async = require('async');
const moment = require('moment-timezone');
const request = require('request');

const checkDigit = require('../util/checkDigit');
Expand All @@ -10,26 +11,24 @@ const DELIVERED_TRACKING_DESCRIPTIONS = ['DELIVERED'];
// These tracking descriptions should indicate the shipment was shipped (shows movement beyond a shipping label being created)
const SHIPPED_TRACKING_DESCRIPTIONS = ['ARRIVAL DESTINATION DHL ECOMMERCE FACILITY', 'DEPARTURE ORIGIN DHL ECOMMERCE FACILITY', 'ARRIVED USPS SORT FACILITY', 'ARRIVAL AT POST OFFICE', 'OUT FOR DELIVERY', 'PROCESSED THROUGH SORT FACILITY'];

// In an IMpb number, an initial '420' followed by ZIP or ZIP+4 is part of the barcode but is not supposed to be printed. If the tracking number comes from a barcode scanner, it will have that info.
// 109124 is a Mailer ID provided by DHL. See https://postalpro.usps.com/shipping/impb/BarcodePackageIMSpec for full IMpb specs.
const DHL_IMPB_REGEX = new RegExp(/^(?:420(?:\d{9}|\d{5}))?(93\d{3}109124(?:\d{14}|\d{10})\d)$/);

const geography = require('../util/geography');

function DHL(options) {
const usps = new USPS(options && options.usps);

this.isTrackingNumberValid = function(trackingNumber) {
// Remove spaces and uppercase
trackingNumber = trackingNumber.replace(/\s/g, '').toUpperCase();

if ([/^93612\d{17}$/, /^92612\d{17}$/, /^94748\d{17}$/, /^93748\d{17}$/, /^92748\d{17}$/].some(regex => regex.test(trackingNumber))) {
return checkDigit(trackingNumber, [3, 1], 10);
}
if (DHL_IMPB_REGEX.test(trackingNumber)) {
// Strip off the IMpb routing code and ZIP
trackingNumber = trackingNumber.replace(DHL_IMPB_REGEX, '$1');

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 checkDigit(trackingNumber, [3, 1], 10);
}

return false;
Expand All @@ -46,17 +45,17 @@ function DHL(options) {
_options.minDate = new Date(0);
}

// This is the API being used from: https://www.dhl.com/global-en/home/tracking/tracking-ecommerce.html
// This is the API being used from: https://developer.dhl.com/api-reference/shipment-tracking
const req = {
forever: true,
gzip: true,
headers: {
referer: `https://www.dhl.com/global-en/home/tracking/tracking-ecommerce.html?tracking-id=${trackingNumber}`
'DHL-API-Key': options.apiKey
},
json: true,
method: 'GET',
timeout: 5000,
url: `https://www.dhl.com/utapi?trackingNumber=${trackingNumber}`
url: `https://api-eu.dhl.com/track/shipments?trackingNumber=${trackingNumber}`
};

async.retry(function(callback) {
Expand Down Expand Up @@ -98,68 +97,88 @@ function DHL(options) {
const events = shipment.events.reverse();

// Used when there is no address data present
var previousAddress = shipment.origin && shipment.origin.address;
var previousAddress = shipment.origin?.address;

// Set address and locationString of each event
events.forEach(event => {
// If the event doesn't have a location, make one up using the previousAddress
if (!event.location) {
event.location = {
address: previousAddress
}
}

// If the event's location contains an address without a comma (UNITED STATES), use the previousAddress instead
if (event.location && event.location.address && event.location.address.addressLocality && !event.location.address.addressLocality.includes(',')) {
if (!event.location?.address) {
event.location = event.location || {};
event.location.address = previousAddress;
}
event.locationString = `${event.location.address?.addressLocality} ${event.location.address?.postalCode} ${event.location.address?.countryCode}`.trim();
});

// Save the current address as the previousAddress
previousAddress = event.location.address;

const addressTokens = event.location.address.addressLocality.split(',').map(t => t.trim());

const _event = {
address: {
city: addressTokens[0],
country: event.location.address.countryCode,
state: addressTokens[1],
zip: event.location.address.postalCode
},
date: new Date(event.timestamp),
description: event.status
};

// Ensure event is after minDate (used to prevent data from reused tracking numbers)
if (_event.date < _options.minDate) {
return;
}
// Get unique array of locations (remove falsy values)
const locations = Array.from(new Set(events.map(event => event.locationString))).filter(l => l);

if (event.description) {
_event.details = event.description;
}
// Lookup each location
async.mapLimit(locations, 10, function(location, callback) {
geography.parseLocation(location, options, function(err, address) {
if (err || !address) {
return callback(err, address);
}

if (!results.deliveredAt && _event.description && DELIVERED_TRACKING_DESCRIPTIONS.includes(_event.description.toUpperCase())) {
results.deliveredAt = _event.date;
}
address.location = location;

if (!results.shippedAt && _event.description && SHIPPED_TRACKING_DESCRIPTIONS.includes(_event.description.toUpperCase())) {
results.shippedAt = _event.date;
callback(null, address);
});
}, function(err, addresses) {
if (err) {
return callback(err);
}

results.events.push(_event);
});
events.forEach(event => {
const address = addresses.find(a => a && a.location === event.locationString);

// Use the geolocated timezone, or ET as a default
let timezone = 'America/New_York';
if (address && address.timezone) {
timezone = address.timezone;
}

// Add url to carrier tracking page
results.url = `http://webtrack.dhlglobalmail.com/?trackingnumber=${encodeURIComponent(trackingNumber)}`;
const _event = {
address: {
city: address.city,
country: event.location.address.countryCode,
state: address.state,
zip: event.location.address.postalCode
},
date: moment.tz(event.timestamp, 'YYYY-MM-DDTHH:mm:ss', timezone).toDate(),
description: event.status
};

// Ensure event is after minDate (used to prevent data from reused tracking numbers)
if (_event.date < _options.minDate) {
return;
}

// Reverse results again to get events in order Most Recent - Least Recent
results.events.reverse();
if (event.description) {
_event.details = event.description;
}

if (!results.shippedAt && results.deliveredAt) {
results.shippedAt = results.deliveredAt;
}
if (!results.deliveredAt && _event.description && DELIVERED_TRACKING_DESCRIPTIONS.includes(_event.description.toUpperCase())) {
results.deliveredAt = _event.date;
}

callback(null, results);
if (!results.shippedAt && _event.description && SHIPPED_TRACKING_DESCRIPTIONS.includes(_event.description.toUpperCase())) {
results.shippedAt = _event.date;
}

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

// Add URL to carrier tracking page
results.url = `https://www.dhl.com/us-en/home/tracking.html?tracking-id=${encodeURIComponent(trackingNumber)}&submit=1`;

// Reverse results again to get events in order Most Recent - Least Recent
results.events.reverse();

if (!results.shippedAt && results.deliveredAt) {
results.shippedAt = results.deliveredAt;
}

callback(null, results);
});
});
}
}
Expand Down
6 changes: 5 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,12 @@ function Bloodhound(options) {
geography.geocoder = NodeGeocoder(options.geocoder);
}

// Allow PitneyBowes to cache geocode results in Redis (via petty-cache)
if (options.pettyCache) {
// Allow DHL to cache geocode results in Redis (via petty-cache)
if (options.dhl) {
options.dhl.pettyCache = options.pettyCache;
}

// Allow PitneyBowes to cache geocode results in Redis (via petty-cache)
if (options.pitneyBowes) {
options.pitneyBowes.pettyCache = options.pettyCache;
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,5 @@
"type": "git",
"url": "https://github.com/mediocre/bloodhound.git"
},
"version": "1.8.2"
"version": "1.9.0"
}
33 changes: 7 additions & 26 deletions test/carriers/dhl.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,33 +8,13 @@ describe('DHL', function() {
const dhl = new DHL();

const validTrackingNumbers = [
'9361269903500576940071',
'9361269903503907020237',
'9361 2699 0350 3907 2657 75',
'9261293148703201610999',
'9261293148703201610975',
'9261 2931 4870 3201 6109 82',
'9474812901015476250258',
'9374869903503907077381',
'9374869903503907060802',
'9374 8699 0350 3906 9887 18',
'9274893148703201609685',
'9274893148703201609715',
'9274 8931 4870 3201 6096 92',
'420941339405511899223428669715',
'420206039405511899223428471196',
'4203 7398 9405 5118 9922 3427 4906 00',
'4204923092748927005269000022418209',
'4209081092748927005269000022418407',
'4200681292612927005269000029934812',
'4209 2155 1234 9505 5000 2071 4300 0001 28'
'420726449361210912400330222910'
];

const invalidTrackingNumbers = [
'9970 4895 0367 429',
'DT771613423732',
'9400 1118 9922 3818 2184 07'

];

it('should detect valid DHL tracking numbers', function() {
Expand All @@ -55,17 +35,18 @@ describe('DHL', function() {
});
});

describe('dhl.track', function() {
describe.skip('dhl.track', function() {
this.timeout(10000);

const bloodhound = new Bloodhound({
usps: {
userId: process.env.USPS_USERID
dhl: {
apiKey: process.env.DHL_API_KEY
}
});

it.skip('should return a valid response with no errors', function(done) {
bloodhound.track('9361269903505749570437', 'dhl', function(err, actual) {
it('should return a valid response with no errors', function(done) {
bloodhound.track('420726449361210912400330222910', 'dhl', function(err, actual) {
console.log(actual);
assert.ifError(err);
done();
});
Expand Down
1 change: 1 addition & 0 deletions test/carriers/usps.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ describe('USPS', function() {
];

const invalidTrackingNumbers = [
// incorrect check digit (last digit)
'4206810692612927005269000028978090'
];

Expand Down

0 comments on commit bb2bac3

Please sign in to comment.