Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ _*Note*: `shipit-api` Heroku app is not meant for production use, and there are
* Amazon
* A1 International
* Prestige
* Purolator

## Usage

Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions src/main.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
{CanadaPostClient} = require './canada_post'
{DhlGmClient} = require './dhlgm'
{PrestigeClient} = require './prestige'
{PurolatorClient} = require './purolator'
guessCarrier = require './guessCarrier'

module.exports = {
Expand Down
121 changes: 121 additions & 0 deletions src/purolator.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
{Builder, Parser} = require 'xml2js'
{find} = require 'underscore'
moment = require 'moment-timezone'
{titleCase, upperCase} = require 'change-case'
{ShipperClient} = require './shipper'

class PurolatorClient extends ShipperClient
DEV_URI_BASE = 'https://devwebservices.purolator.com/EWS/V1'
URI_BASE = 'https://webservices.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}, @options) ->
super()
@parser = new Parser()
@builder = new Builder(renderOpts: pretty: true)

generateRequest: (trk) ->
req =
'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'
'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?
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: "#{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'
'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}
2 changes: 1 addition & 1 deletion src/ups.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
79 changes: 79 additions & 0 deletions test/purolator.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
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'},
{dev: 'true', token: 'user-token'}
)

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

1 change: 1 addition & 0 deletions test/stub_data/purolator_delivered.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"><s:Header><h:ResponseContext xmlns:h="http://purolator.com/pws/datatypes/v1" xmlns:i="http://www.w3.org/2001/XMLSchema-instance"><h:ResponseReference>Shiprack Package Tracker</h:ResponseReference></h:ResponseContext></s:Header><s:Body><TrackPackagesByPinResponse xmlns="http://purolator.com/pws/datatypes/v1" xmlns:i="http://www.w3.org/2001/XMLSchema-instance"><ResponseInformation><Errors/><InformationalMessages i:nil="true"/></ResponseInformation><TrackingInformationList><TrackingInformation><PIN><Value>330362235641</Value></PIN><Scans><Scan i:type="DeliveryScan"><ScanType>Delivery</ScanType><PIN><Value>330362235641</Value></PIN><Depot><Name>BURNABY, BC</Name></Depot><ScanDate>2015-10-01</ScanDate><ScanTime>164300</ScanTime><Description>Shipment delivered to</Description><Comment/><SummaryScanIndicator>false</SummaryScanIndicator><ScanDetails><DeliverySignature>K BONCI</DeliverySignature><SignatureImage/><SignatureImageSize>0</SignatureImageSize><SignatureImageFormat>GIF</SignatureImageFormat><DeliveryAddress/><DeliveryCompanyName/><PremiumServiceText>Not known or specified</PremiumServiceText><ProductTypeText/><SpecialHandlingText>Not known or specified</SpecialHandlingText><PaymentTypeText/></ScanDetails></Scan><Scan><ScanType>Undeliverable</ScanType><PIN><Value>330362235641</Value></PIN><Depot><Name>BURNABY, BC</Name></Depot><ScanDate>2015-10-01</ScanDate><ScanTime>164300</ScanTime><Description>Available for pickup for 5 business days from arrival date at the counter.</Description><Comment/><SummaryScanIndicator>false</SummaryScanIndicator></Scan><Scan><ScanType>Undeliverable</ScanType><PIN><Value>330362235641</Value></PIN><Depot><Name>BURNABY, BC</Name></Depot><ScanDate>2015-10-01</ScanDate><ScanTime>135900</ScanTime><Description>Attempted Delivery - Customer Closed</Description><Comment/><SummaryScanIndicator>false</SummaryScanIndicator></Scan><Scan i:type="OnDeliveryScan"><ScanType>OnDelivery</ScanType><PIN><Value>330362235641</Value></PIN><Depot><Name>BURNABY, BC</Name></Depot><ScanDate>2015-10-01</ScanDate><ScanTime>135500</ScanTime><Description>On vehicle for delivery</Description><Comment/><SummaryScanIndicator>false</SummaryScanIndicator><ScanDetails><DeliveryAddress/></ScanDetails></Scan><Scan><ScanType>Other</ScanType><PIN><Value>330362235641</Value></PIN><Depot><Name>BURNABY, BC</Name></Depot><ScanDate>2015-09-29</ScanDate><ScanTime>065100</ScanTime><Description>Arrived at sort facility</Description><Comment/><SummaryScanIndicator>false</SummaryScanIndicator></Scan><Scan><ScanType>Other</ScanType><PIN><Value>330362235641</Value></PIN><Depot><Name>VANCOUVER, BC</Name></Depot><ScanDate>2015-09-29</ScanDate><ScanTime>055200</ScanTime><Description>Departed sort facility</Description><Comment/><SummaryScanIndicator>false</SummaryScanIndicator></Scan><Scan><ScanType>Other</ScanType><PIN><Value>330362235641</Value></PIN><Depot><Name>VANCOUVER, BC</Name></Depot><ScanDate>2015-09-29</ScanDate><ScanTime>051400</ScanTime><Description>Arrived at sort facility</Description><Comment/><SummaryScanIndicator>false</SummaryScanIndicator></Scan><Scan><ScanType>Other</ScanType><PIN><Value>330362235641</Value></PIN><Depot><Name>WINNIPEG AIRPORT/AEROPORT, MB</Name></Depot><ScanDate>2015-09-28</ScanDate><ScanTime>234900</ScanTime><Description>Departed sort facility</Description><Comment/><SummaryScanIndicator>false</SummaryScanIndicator></Scan><Scan><ScanType>Other</ScanType><PIN><Value>330362235641</Value></PIN><Depot><Name>REGINA, SK</Name></Depot><ScanDate>2015-09-28</ScanDate><ScanTime>153100</ScanTime><Description>Arrived at sort facility</Description><Comment/><SummaryScanIndicator>false</SummaryScanIndicator></Scan><Scan><ScanType>Other</ScanType><PIN><Value>330362235641</Value></PIN><Depot><Name>REGINA, SK</Name></Depot><ScanDate>2015-09-28</ScanDate><ScanTime>081900</ScanTime><Description>Picked up by Purolator at</Description><Comment/><SummaryScanIndicator>false</SummaryScanIndicator></Scan><Scan><ScanType>Undeliverable</ScanType><PIN><Value>330362235641</Value></PIN><Depot><Name>Purolator</Name></Depot><ScanDate>2015-10-01</ScanDate><ScanTime>164200</ScanTime><Description>Shipment created</Description><Comment/><SummaryScanIndicator>false</SummaryScanIndicator></Scan></Scans><ResponseInformation i:nil="true"/></TrackingInformation></TrackingInformationList></TrackPackagesByPinResponse></s:Body></s:Envelope>