@mediocre/bloodhound
Advanced tools
Comparing version 1.14.0 to 2.0.0
const async = require('async'); | ||
const FedExClient = require('shipping-fedex'); | ||
const cache = require('memory-cache'); | ||
const createError = require('http-errors'); | ||
const request = require('request'); | ||
@@ -18,5 +20,45 @@ const checkDigit = require('../util/checkDigit'); | ||
function FedEx(options) { | ||
const fedExClient = new FedExClient(options); | ||
function FedEx(args) { | ||
const options = Object.assign({ | ||
url: 'https://apis.fedex.com' | ||
}, args); | ||
this.getAccessToken = function(callback) { | ||
const key = args.api_key; | ||
const accessToken = cache.get(key); | ||
if (accessToken) { | ||
return callback(null, accessToken); | ||
} | ||
const req = { | ||
form: { | ||
grant_type: 'client_credentials', | ||
client_id: args.api_key, | ||
client_secret: args.secret_key | ||
}, | ||
method: 'POST', | ||
url: `${options.url}/oauth/token` | ||
}; | ||
request(req, function(err, response, body) { | ||
if (err) { | ||
return callback(err); | ||
} | ||
if (response.statusCode !== 200) { | ||
const err = createError(response.statusCode); | ||
err.response = response; | ||
return callback(err); | ||
} | ||
const accessToken = JSON.parse(body); | ||
cache.put(key, accessToken, (accessToken.expires_in - 100) * 1000); | ||
return callback(null, accessToken); | ||
}); | ||
}; | ||
this.isTrackingNumberValid = function(trackingNumber) { | ||
@@ -84,16 +126,49 @@ // Remove whitespace | ||
// Create a FedEx track request: https://www.fedex.com/us/developer/webhelp/ws/2018/US/index.htm#t=wsdvg%2FTracking_Shipments.htm%23Tracking_Service_Optionsbc-3&rhtocid=_26_0_2 | ||
const trackRequest = { | ||
SelectionDetails: { | ||
PackageIdentifier: { | ||
Type: 'TRACKING_NUMBER_OR_DOORTAG', | ||
Value: trackingNumber | ||
} | ||
}, | ||
ProcessingOptions: 'INCLUDE_DETAILED_SCANS' | ||
}; | ||
this.getAccessToken(function(err, accessToken) { | ||
if (err) { | ||
return callback(err); | ||
} | ||
// FedEx Web Services requests occasionally fail. Timeout after 5 seconds and retry. | ||
async.retry(function(callback) { | ||
async.timeout(fedExClient.track, 5000)(trackRequest, function(err, trackReply) { | ||
const trackRequestOptions = { | ||
gzip: true, | ||
headers: { | ||
Authorization: `Bearer ${accessToken.access_token}` | ||
}, | ||
json: { | ||
includeDetailedScans: true, | ||
trackingInfo: [ | ||
{ | ||
trackingNumberInfo: { | ||
trackingNumber: trackingNumber | ||
} | ||
} | ||
] | ||
}, | ||
method: 'POST', | ||
url: `${options.url}/track/v1/trackingnumbers` | ||
}; | ||
async.retry(function(callback) { | ||
request(trackRequestOptions, function(err, response, trackResponse) { | ||
if (err) { | ||
return callback(err); | ||
} | ||
if (trackResponse?.output?.alerts?.length) { | ||
let alerts = trackResponse.output.alerts; | ||
let warnings = alerts.filter(alert => alert.alertType === 'WARNING'); | ||
if (warnings.length) { | ||
return callback(new Error(warnings.map(warning => `${warning.code}: ${warning.message}`).join(', '))); | ||
} | ||
} | ||
// Return if only one track detail is returned | ||
if (trackResponse?.output?.completeTrackResults?.trackResults?.scanEvents?.length === 1) { | ||
return callback(null, trackResponse); | ||
} | ||
callback(null, trackResponse); | ||
}); | ||
}, function(err, trackReply) { | ||
if (err) { | ||
@@ -103,119 +178,67 @@ return callback(err); | ||
if (trackReply.HighestSeverity === 'ERROR') { | ||
return callback(new Error(trackReply.Notifications[0].Message)); | ||
} | ||
const results = { | ||
carrier: 'FedEx', | ||
events: [], | ||
raw: trackReply | ||
}; | ||
// Return if only one track detail is returned | ||
if (trackReply?.CompletedTrackDetails?.[0]?.TrackDetails.length === 1) { | ||
return callback(null, trackReply); | ||
// Ensure track reply has events | ||
if (!trackReply?.output?.completeTrackResults?.[0]?.trackResults?.[0]?.scanEvents?.length) { | ||
return callback(null, results); | ||
} | ||
async.mapLimit(trackReply.CompletedTrackDetails[0].TrackDetails, 10, function(trackDetail, callback) { | ||
const trackRequest = { | ||
SelectionDetails: { | ||
PackageIdentifier: { | ||
Type: 'TRACKING_NUMBER_OR_DOORTAG', | ||
Value: trackingNumber | ||
}, | ||
TrackingNumberUniqueIdentifier: trackDetail.TrackingNumberUniqueIdentifier | ||
trackReply?.output?.completeTrackResults?.[0]?.trackResults?.[0]?.scanEvents.forEach(e => { | ||
if (TRACKING_STATUS_CODES_BLACKLIST.includes(e.eventType)) { | ||
return; | ||
} | ||
const event = { | ||
address: { | ||
city: e?.scanLocation?.city, | ||
country: e?.scanLocation?.countryCode, | ||
state: e?.scanLocation?.stateOrProvinceCode, | ||
zip: e?.scanLocation?.postalCode | ||
}, | ||
ProcessingOptions: 'INCLUDE_DETAILED_SCANS' | ||
date: new Date(e.date), | ||
description: e.eventDescription | ||
}; | ||
async.retry(function(callback) { | ||
async.timeout(fedExClient.track, 5000)(trackRequest, function(err, trackReply) { | ||
if (err) { | ||
return callback(err); | ||
} | ||
// Ensure event is after minDate (used to prevent data from reused tracking numbers) | ||
if (event.date < _options.minDate) { | ||
return; | ||
} | ||
if (trackReply.HighestSeverity === 'ERROR') { | ||
return callback(new Error(trackReply.Notifications[0].Message)); | ||
} | ||
if (e.exceptionDescription) { | ||
event.details = e.exceptionDescription; | ||
} | ||
callback(null, trackReply); | ||
}); | ||
}, callback); | ||
}, function(err, trackReplies) { | ||
if (err) { | ||
return callback(err); | ||
// Remove blacklisted words | ||
if (event.address.city) { | ||
event.address.city = event.address.city.replace(CITY_BLACKLIST, '').trim(); | ||
} | ||
// Sort track replies by timestamp | ||
trackReplies.sort((a, b) => b.CompletedTrackDetails[0].TrackDetails[0].StatusDetail.CreationTime - a.CompletedTrackDetails[0].TrackDetails[0].StatusDetail.CreationTime); | ||
if (DELIVERED_TRACKING_STATUS_CODES.includes(e.eventType)) { | ||
results.deliveredAt = new Date(e.date); | ||
} | ||
// Get the most recent track reply | ||
trackReply = trackReplies[0]; | ||
if (SHIPPED_TRACKING_STATUS_CODES.includes(e.eventType)) { | ||
results.shippedAt = new Date(e.date); | ||
} | ||
callback(null, trackReply); | ||
results.events.push(event); | ||
}); | ||
}); | ||
}, function(err, trackReply) { | ||
if (err) { | ||
return callback(err); | ||
} | ||
const results = { | ||
carrier: 'FedEx', | ||
events: [], | ||
raw: trackReply | ||
}; | ||
// Add url to carrier tracking page | ||
results.url = `https://www.fedex.com/apps/fedextrack/?tracknumbers=${encodeURIComponent(trackingNumber)}`; | ||
// Ensure track reply has events | ||
if (!trackReply?.CompletedTrackDetails?.[0]?.TrackDetails?.[0]?.Events?.length) { | ||
return callback(null, results); | ||
} | ||
trackReply.CompletedTrackDetails[0].TrackDetails[0].Events.forEach(e => { | ||
if (TRACKING_STATUS_CODES_BLACKLIST.includes(e.EventType)) { | ||
return; | ||
if (!results.shippedAt && results.deliveredAt) { | ||
results.shippedAt = results.deliveredAt; | ||
} | ||
const event = { | ||
address: { | ||
city: e?.Address?.City, | ||
country: e?.Address?.CountryCode, | ||
state: e?.Address?.StateOrProvinceCode, | ||
zip: e?.Address?.PostalCode | ||
}, | ||
date: new Date(e.Timestamp), | ||
description: e.EventDescription | ||
}; | ||
// Ensure event is after minDate (used to prevent data from reused tracking numbers) | ||
if (event.date < _options.minDate) { | ||
return; | ||
} | ||
if (e.StatusExceptionDescription) { | ||
event.details = e.StatusExceptionDescription; | ||
} | ||
// Remove blacklisted words | ||
if (event.address.city) { | ||
event.address.city = event.address.city.replace(CITY_BLACKLIST, '').trim(); | ||
} | ||
if (DELIVERED_TRACKING_STATUS_CODES.includes(e.EventType)) { | ||
results.deliveredAt = new Date(e.Timestamp); | ||
} | ||
if (SHIPPED_TRACKING_STATUS_CODES.includes(e.EventType)) { | ||
results.shippedAt = new Date(e.Timestamp); | ||
} | ||
results.events.push(event); | ||
callback(null, results); | ||
}); | ||
// Add url to carrier tracking page | ||
results.url = `https://www.fedex.com/apps/fedextrack/?tracknumbers=${encodeURIComponent(trackingNumber)}`; | ||
if (!results.shippedAt && results.deliveredAt) { | ||
results.shippedAt = results.deliveredAt; | ||
} | ||
callback(null, results); | ||
}); | ||
} | ||
}; | ||
} | ||
module.exports = FedEx; |
@@ -9,2 +9,6 @@ # Changelog | ||
## [2.0.0] - 2024-08-30 | ||
### Changed | ||
- Migrated the FedEx carrier integration away from the deprecated WSDL endpoints to the new OAuth/JSON endpoints. | ||
## [1.13.0] - 2024-05-08 | ||
@@ -11,0 +15,0 @@ ### Changed |
@@ -90,4 +90,4 @@ const NodeGeocoder = require('node-geocoder'); | ||
if (typeof options === 'function') { | ||
callback = options; | ||
options = {}; | ||
callback = options; | ||
} | ||
@@ -94,0 +94,0 @@ |
@@ -7,2 +7,4 @@ { | ||
"fast-xml-parser": "~3.21.0", | ||
"http-errors": "~2.0.0", | ||
"memory-cache": "~0.2.0", | ||
"moment-timezone": "~0.5.28", | ||
@@ -12,5 +14,5 @@ "node-geocoder": "~3.28.0", | ||
"pitney-bowes": "~0.3.1", | ||
"shipping-fedex": "0.2.0", | ||
"tz-lookup": "~6.1.25", | ||
"us-states-normalize": "~1.0.0" | ||
"us-states-normalize": "~1.0.0", | ||
"xml2js": "~0.4.23" | ||
}, | ||
@@ -45,3 +47,3 @@ "devDependencies": { | ||
}, | ||
"version": "1.14.0" | ||
"version": "2.0.0" | ||
} |
@@ -61,3 +61,3 @@ # Bloodhound | ||
Creates a new Bloodhound client. Each carrier requires a different combination of credentials (account numbers, meter numbers, passwords, user IDs, etc). | ||
Creates a new Bloodhound client. Each carrier requires a different combination of credentials (API keys, account numbers, passwords, user IDs, etc). | ||
@@ -74,7 +74,4 @@ By default, when Bloodhound encounters a timestamp without a UTC offset it will geocode using OpenStreetMap (which does not require an API key). You can optionally use a different geocoder. You can also optionally cache geocode results in Redis. | ||
fedEx: { | ||
account_number: '123456789', | ||
environment: 'live', | ||
key: 'abcdefghijklmnop', | ||
meter_number: '987654321', | ||
password: 'abcdefghijklmnopqrstuvwxy' | ||
api_key: 'abcdefghijklmnopqrstuvwxyz', | ||
secret_key: 'abcdefghijklmnopqrstuvwxyz' | ||
}, | ||
@@ -111,8 +108,4 @@ geocoder: { | ||
**fedEx** | ||
*geocoder** | ||
FedEx options are passed to the [shipping-fedex](https://www.npmjs.com/package/shipping-fedex) module. | ||
**geocoder** | ||
By default Bloodhound uses the OpenStreetMap geocode provider. You can optionally specify geocoder options which are passed to the [node-geocode](https://www.npmjs.com/package/node-geocoder) module. | ||
@@ -119,0 +112,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
79799
1236
12
168
1
+ Addedhttp-errors@~2.0.0
+ Addedmemory-cache@~0.2.0
+ Addedxml2js@~0.4.23
- Removedshipping-fedex@0.2.0
- Removeddebug@0.7.4(transitive)
- Removedextend@1.3.0(transitive)
- Removedfirst-chunk-stream@0.1.0(transitive)
- Removedis-utf8@0.2.1(transitive)
- Removedlodash@2.4.2(transitive)
- Removedselectn@0.9.6(transitive)
- Removedshipping-fedex@0.2.0(transitive)
- Removedsoap@0.9.5(transitive)
- Removedstrip-bom@0.3.1(transitive)