From 3a073130cec8fef80850c1ebaba2db24e7051a7c Mon Sep 17 00:00:00 2001 From: Rishi Arora Date: Sat, 24 Nov 2018 07:58:55 -0600 Subject: [PATCH 1/4] fixed a problem with missing timestamps in UPS --- src/ups.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ups.coffee b/src/ups.coffee index fd3c38d..7586acb 100644 --- a/src/ups.coffee +++ b/src/ups.coffee @@ -61,7 +61,7 @@ class UpsClient extends ShipperClient presentTimestamp: (dateString, timeString) -> return unless dateString? - timeString ?= '00:00:00' + timeString ?= '000000' formatSpec = 'YYYYMMDD HHmmss ZZ' moment("#{dateString} #{timeString} +0000", formatSpec).toDate() From 01465f7b8c4e55926cf0c075a880403498d111bd Mon Sep 17 00:00:00 2001 From: Rishi Arora Date: Sat, 24 Nov 2018 07:59:26 -0600 Subject: [PATCH 2/4] added a purolator client --- src/purolator.coffee | 115 +++++++++++++++++++++++++ test/purolator.coffee | 78 +++++++++++++++++ test/stub_data/purolator_delivered.xml | 1 + 3 files changed, 194 insertions(+) create mode 100644 src/purolator.coffee create mode 100644 test/purolator.coffee create mode 100644 test/stub_data/purolator_delivered.xml diff --git a/src/purolator.coffee b/src/purolator.coffee new file mode 100644 index 0000000..454126c --- /dev/null +++ b/src/purolator.coffee @@ -0,0 +1,115 @@ +{Builder, Parser} = require 'xml2js' +{find} = require 'underscore' +moment = require 'moment-timezone' +{titleCase, upperCase} = require 'change-case' +{ShipperClient} = require './shipper' + +class PurolatorClient extends ShipperClient + URI_BASE = 'https://devwebservices.purolator.com/EWS/V1' + + PROVINCES = [ + 'NL', 'PE', 'NS', 'NB', 'QC', 'ON', + 'MB', 'SK', 'AB', 'BC', 'YT', 'NT', 'NU' + ] + + STATUS_MAP = + 'Delivery': ShipperClient.STATUS_TYPES.DELIVERED + 'Undeliverable': ShipperClient.STATUS_TYPES.DELAYED + 'OnDelivery': ShipperClient.STATUS_TYPES.OUT_FOR_DELIVERY + + DESCRIPTION_MAP = + 'Arrived': ShipperClient.STATUS_TYPES.EN_ROUTE + 'Departed': ShipperClient.STATUS_TYPES.EN_ROUTE + 'Picked up': ShipperClient.STATUS_TYPES.EN_ROUTE + 'Shipment created': ShipperClient.STATUS_TYPES.SHIPPING + + constructor: ({@key, @password}) -> + super() + @parser = new Parser() + @builder = new Builder(renderOpts: pretty: true) + + generateRequest: (trk) -> + @builder.buildObject + 'SOAP-ENV:Envelope': + '$': + 'xmlns:ns0': 'http://purolator.com/pws/datatypes/v1' + 'xmlns:ns1': 'http://schemas.xmlsoap.org/soap/envelope/' + 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance' + 'xmlns:tns': 'http://purolator.com/pws/datatypes/v1' + 'xmlns:SOAP-ENV': 'http://schemas.xmlsoap.org/soap/envelope/' + 'SOAP-ENV:Header': + 'tns:RequestContext': + 'tns:Version': '1.2' + 'tns:Language': 'en' + 'tns:GroupID': 'xxx' + 'tns:RequestReference': 'Shiprack Package Tracker' + 'tns:UserToken': 'd222d727-66a9-4d83-ac9a-683e757c23a1' + 'ns1:Body': + 'ns0:TrackPackagesByPinRequest': + 'ns0:PINs': + 'ns0:PIN': + 'ns0:Value': trk + + validateResponse: (response, cb) -> + handleResponse = (xmlErr, trackResult) -> + return cb(xmlErr) if xmlErr? + body = trackResult?['s:Envelope']?['s:Body']?[0] + trackInfo = body?.TrackPackagesByPinResponse?[0]?.TrackingInformationList?[0] + scans = trackInfo?.TrackingInformation?[0]?.Scans?[0]?.Scan + return cb('Unrecognized response format') unless scans?.length + cb null, scans + @parser.reset() + @parser.parseString response, handleResponse + + getStatus: (data) -> + status = STATUS_MAP[data?.ScanType[0]] + return status if status? + for text, statusCode of DESCRIPTION_MAP + regex = new RegExp text, 'i' + if regex.test data?.ScanType[0] + status = statusCode + break + status + + presentLocation: (depot) -> + return null if upperCase(depot) is 'PUROLATOR' + words = depot?.split(' ') + lastWord = words?.pop() + if lastWord?.length is 2 and upperCase(lastWord) in PROVINCES + return "#{titleCase(words.join(' '))}, #{lastWord}" + else + return titleCase depot + + presentTimestamp: (datestr, timestr) -> + moment("#{datestr} #{timestr} +0000", 'YYYY-MM-DD HHmmss ZZ').toDate() + + getActivitiesAndStatus: (data) -> + activities = data?.map (scan) => + details: scan?.Description?[0] + location: @presentLocation scan?.Depot?[0]?.Name?[0] + timestamp: @presentTimestamp scan?.ScanDate?[0], scan?.ScanTime?[0] + activities: activities, status: @getStatus data?[0] + + getEta: (data) -> + + getService: (data) -> + + getWeight: (data) -> + + getDestination: (data) -> + + requestOptions: ({trackingNumber}) -> + method: 'POST' + uri: "#{URI_BASE}/Tracking/TrackingService.asmx" + headers: + 'SOAPAction': '"http://purolator.com/pws/service/v1/TrackPackagesByPin"' + 'Content-Type': 'text/xml; charset=utf-8' + 'Content-type': 'text/xml; charset=utf-8' + 'Soapaction': '"http://purolator.com/pws/service/v1/TrackPackagesByPin"' + auth: + user: @key + pass: @password + body: @generateRequest trackingNumber + + +module.exports = {PurolatorClient} diff --git a/test/purolator.coffee b/test/purolator.coffee new file mode 100644 index 0000000..f31d866 --- /dev/null +++ b/test/purolator.coffee @@ -0,0 +1,78 @@ +fs = require 'fs' +assert = require 'assert' +should = require('chai').should() +expect = require('chai').expect +{PurolatorClient} = require '../lib/purolator' +{ShipperClient} = require '../lib/shipper' +{Builder, Parser} = require 'xml2js' + +describe "purolator client", -> + _purolatorClient = null + _xmlParser = new Parser() + + before -> + _purolatorClient = new PurolatorClient + key: 'purolator-key' + password: 'my-password' + + describe "generateRequest", -> + _trackRequest = null + + before (done) -> + trackXml = _purolatorClient.generateRequest '330362235641' + _xmlParser.parseString trackXml, (err, data) -> + _trackRequest = data?['SOAP-ENV:Envelope'] + assert _trackRequest? + done() + + it 'contains the correct xml namespace and soap envelope', -> + _trackRequest.should.have.property '$' + _trackRequest['$']['xmlns:ns0'].should.equal 'http://purolator.com/pws/datatypes/v1' + _trackRequest['$']['xmlns:ns1'].should.equal 'http://schemas.xmlsoap.org/soap/envelope/' + _trackRequest['$']['xmlns:xsi'].should.equal 'http://www.w3.org/2001/XMLSchema-instance' + _trackRequest['$']['xmlns:tns'].should.equal 'http://purolator.com/pws/datatypes/v1' + _trackRequest['$']['xmlns:SOAP-ENV'].should.equal 'http://schemas.xmlsoap.org/soap/envelope/' + + it 'contains a valid request context', -> + _trackRequest.should.have.property 'SOAP-ENV:Header' + _context = _trackRequest['SOAP-ENV:Header'][0]['tns:RequestContext'][0] + _context.should.have.property 'tns:GroupID' + _context.should.have.property 'tns:RequestReference' + _context.should.have.property 'tns:UserToken' + _context['tns:Version'][0].should.equal '1.2' + _context['tns:Language'][0].should.equal 'en' + + it 'contains a valid tracking pin', -> + _trackRequest.should.have.property 'ns1:Body' + _pins = _trackRequest['ns1:Body'][0]['ns0:TrackPackagesByPinRequest'][0]['ns0:PINs'][0] + _pins['ns0:PIN'][0]['ns0:Value'][0].should.equal '330362235641' + + describe "integration tests", -> + _package = null + + describe "delivered package", -> + before (done) -> + fs.readFile 'test/stub_data/purolator_delivered.xml', 'utf8', (err, xmlDoc) -> + _purolatorClient.presentResponse xmlDoc, 'trk', (err, resp) -> + should.not.exist(err) + _package = resp + done() + + it "has a status of delivered", -> + expect(_package.status).to.equal ShipperClient.STATUS_TYPES.DELIVERED + + it "has 11 activities", -> + expect(_package.activities).to.have.length 11 + + it "has first activity with timestamp, location and details", -> + act = _package.activities[0] + expect(act.timestamp).to.deep.equal new Date '2015-10-01T16:43:00.000Z' + expect(act.details).to.equal 'Shipment delivered to' + expect(act.location).to.equal 'Burnaby, BC' + + it "has last activity with timestamp, location and details", -> + act = _package.activities[10] + expect(act.timestamp).to.deep.equal new Date '2015-10-01T16:42:00.000Z' + expect(act.details).to.equal 'Shipment created' + expect(act.location).to.equal null + diff --git a/test/stub_data/purolator_delivered.xml b/test/stub_data/purolator_delivered.xml new file mode 100644 index 0000000..f08ac41 --- /dev/null +++ b/test/stub_data/purolator_delivered.xml @@ -0,0 +1 @@ +Shiprack Package Tracker330362235641Delivery330362235641BURNABY, BC2015-10-01164300Shipment delivered tofalseK BONCI0GIFNot known or specifiedNot known or specifiedUndeliverable330362235641BURNABY, BC2015-10-01164300Available for pickup for 5 business days from arrival date at the counter.falseUndeliverable330362235641BURNABY, BC2015-10-01135900Attempted Delivery - Customer ClosedfalseOnDelivery330362235641BURNABY, BC2015-10-01135500On vehicle for deliveryfalseOther330362235641BURNABY, BC2015-09-29065100Arrived at sort facilityfalseOther330362235641VANCOUVER, BC2015-09-29055200Departed sort facilityfalseOther330362235641VANCOUVER, BC2015-09-29051400Arrived at sort facilityfalseOther330362235641WINNIPEG AIRPORT/AEROPORT, MB2015-09-28234900Departed sort facilityfalseOther330362235641REGINA, SK2015-09-28153100Arrived at sort facilityfalseOther330362235641REGINA, SK2015-09-28081900Picked up by Purolator atfalseUndeliverable330362235641Purolator2015-10-01164200Shipment createdfalse From dd9ac3b605f4e9928605a26538968b9ba5819c3a Mon Sep 17 00:00:00 2001 From: Rishi Arora Date: Sat, 24 Nov 2018 08:03:20 -0600 Subject: [PATCH 3/4] added purolator documentation --- README.md | 8 +++++++- src/main.coffee | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 10cc5fc..752225f 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ _*Note*: `shipit-api` Heroku app is not meant for production use, and there are * Amazon * A1 International * Prestige +* Purolator ## Usage @@ -56,7 +57,8 @@ Use it to initialize the shipper clients with your account credentials. DhlGmClient, CanadaPostClient, AmazonClient, - PrestigeClient + PrestigeClient, + PurolatorClient } = require 'shipit' ups = new UpsClient @@ -93,6 +95,10 @@ upsmi = new UpsMiClient() amazonClient = new AmazonClient() prestige = new PrestigeClient() + +purolator = new PurolatorClient + key: 'my-production-key' + password: 'my-purolator-password' ``` Use an initialized client to request tracking data. diff --git a/src/main.coffee b/src/main.coffee index f1b8a24..1fdba2c 100644 --- a/src/main.coffee +++ b/src/main.coffee @@ -10,6 +10,7 @@ {CanadaPostClient} = require './canada_post' {DhlGmClient} = require './dhlgm' {PrestigeClient} = require './prestige' +{PurolatorClient} = require './purolator' guessCarrier = require './guessCarrier' module.exports = { From 344a41d8b2e4fb4419d7f5aca2eaf0eb9a2fa06d Mon Sep 17 00:00:00 2001 From: Rishi Arora Date: Sun, 25 Nov 2018 07:14:04 -0600 Subject: [PATCH 4/4] added support for purolator dev vs. prod environment --- src/purolator.coffee | 16 +++++++++++----- test/purolator.coffee | 7 ++++--- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/purolator.coffee b/src/purolator.coffee index 454126c..7a41c79 100644 --- a/src/purolator.coffee +++ b/src/purolator.coffee @@ -5,7 +5,8 @@ moment = require 'moment-timezone' {ShipperClient} = require './shipper' class PurolatorClient extends ShipperClient - URI_BASE = 'https://devwebservices.purolator.com/EWS/V1' + DEV_URI_BASE = 'https://devwebservices.purolator.com/EWS/V1' + URI_BASE = 'https://webservices.purolator.com/EWS/V1' PROVINCES = [ 'NL', 'PE', 'NS', 'NB', 'QC', 'ON', @@ -23,13 +24,13 @@ class PurolatorClient extends ShipperClient 'Picked up': ShipperClient.STATUS_TYPES.EN_ROUTE 'Shipment created': ShipperClient.STATUS_TYPES.SHIPPING - constructor: ({@key, @password}) -> + constructor: ({@key, @password}, @options) -> super() @parser = new Parser() @builder = new Builder(renderOpts: pretty: true) generateRequest: (trk) -> - @builder.buildObject + req = 'SOAP-ENV:Envelope': '$': 'xmlns:ns0': 'http://purolator.com/pws/datatypes/v1' @@ -43,13 +44,18 @@ class PurolatorClient extends ShipperClient 'tns:Language': 'en' 'tns:GroupID': 'xxx' 'tns:RequestReference': 'Shiprack Package Tracker' - 'tns:UserToken': 'd222d727-66a9-4d83-ac9a-683e757c23a1' 'ns1:Body': 'ns0:TrackPackagesByPinRequest': 'ns0:PINs': 'ns0:PIN': 'ns0:Value': trk + if @options?.dev + req['SOAP-ENV:Envelope']['SOAP-ENV:Header']['tns:RequestContext']['tns:UserToken'] = + @options.token + + @builder.buildObject req + validateResponse: (response, cb) -> handleResponse = (xmlErr, trackResult) -> return cb(xmlErr) if xmlErr? @@ -100,7 +106,7 @@ class PurolatorClient extends ShipperClient requestOptions: ({trackingNumber}) -> method: 'POST' - uri: "#{URI_BASE}/Tracking/TrackingService.asmx" + uri: "#{if @options?.dev then DEV_URI_BASE else URI_BASE}/Tracking/TrackingService.asmx" headers: 'SOAPAction': '"http://purolator.com/pws/service/v1/TrackPackagesByPin"' 'Content-Type': 'text/xml; charset=utf-8' diff --git a/test/purolator.coffee b/test/purolator.coffee index f31d866..34a2294 100644 --- a/test/purolator.coffee +++ b/test/purolator.coffee @@ -11,9 +11,10 @@ describe "purolator client", -> _xmlParser = new Parser() before -> - _purolatorClient = new PurolatorClient - key: 'purolator-key' - password: 'my-password' + _purolatorClient = new PurolatorClient( + {key: 'purolator-key', password: 'my-password'}, + {dev: 'true', token: 'user-token'} + ) describe "generateRequest", -> _trackRequest = null