shipppit-canada-post 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,17 @@
1
+ module CanadaPost
2
+ class Credentials
3
+ include Helpers
4
+
5
+ attr_reader :username, :password, :customer_number, :mode
6
+
7
+ def initialize(options={})
8
+ requires!(options, :username, :password, :customer_number, :mode)
9
+ @username = options[:username]
10
+ @password = options[:password]
11
+ @customer_number = options[:customer_number]
12
+ @mode = options[:mode]
13
+ end
14
+
15
+ end
16
+
17
+ end
@@ -0,0 +1,15 @@
1
+ module CanadaPost
2
+ module Helpers
3
+
4
+ private
5
+ # Helper method to validate required fields
6
+ def requires!(hash, *params)
7
+ params.each { |param| raise RateError, "Missing Required Parameter #{param}" if hash[param].nil? }
8
+ end
9
+
10
+ def underscorize(key) #:nodoc:
11
+ key.to_s.sub(/^(v[0-9]+|ns):/, "").gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').gsub(/([a-z\d])([A-Z])/,'\1_\2').downcase
12
+ end
13
+
14
+ end
15
+ end
@@ -0,0 +1,27 @@
1
+ module CanadaPost
2
+ class Rate
3
+
4
+ attr_accessor :service_type, :service_code, :service_link, :total_net_charge, :rate_type,
5
+ :total_base_charge, :guaranteed_delivery, :expected_transit_time,
6
+ :expected_delivery_date, :am_delivery
7
+
8
+ def initialize(options={})
9
+ @service_type = options[:service_name]
10
+ @service_code = options[:service_code]
11
+ @service_link = options[:service_link]
12
+ @total_net_charge = options[:price_details][:due]
13
+ @total_base_charge = options[:price_details][:base]
14
+ @gst_taxes = options[:price_details][:taxes][:gst]
15
+ @pst_taxes = options[:price_details][:taxes][:pst]
16
+ @hst_taxes = options[:price_details][:taxes][:hst]
17
+ @expected_transit_time = options[:service_standard][:expected_transit_time]
18
+ @expected_delivery_date = options[:service_standard][:expected_delivery_date]
19
+ @guaranteed_delivery = options[:service_standard][:guaranteed_delivery]
20
+ @am_delivery = options[:service_standard][:am_delivery]
21
+ end
22
+
23
+ def total_taxes
24
+ (@gst_taxes.to_f + @pst_taxes.to_f + @hst_taxes.to_f).to_s
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,155 @@
1
+ require 'httparty'
2
+ require 'nokogiri'
3
+ require 'active_support/core_ext/hash'
4
+ require 'canada_post/helpers'
5
+ require 'canada_post/rate'
6
+ require 'canada_post/shipment'
7
+
8
+ module CanadaPost
9
+ module Request
10
+ class Base
11
+ include Helpers
12
+ include HTTParty
13
+
14
+ # CanadaPost API Test URL
15
+ TEST_URL = "https://ct.soa-gw.canadapost.ca"
16
+
17
+ # CanadaPost API Production URL
18
+ PRODUCTION_URL = "https://soa-gw.canadapost.ca"
19
+
20
+ # CanadaPost API TEST CONTRACT ID
21
+ TEST_CONTRACT_ID = "0042708517"
22
+
23
+ # List of available Option Codes
24
+ # SO - Signature
25
+ # COV - Coverage (requires qualifier)
26
+ # COD - COD (requires qualifier)
27
+ # PA18 - Proof of Age Required - 18
28
+ # PA19 - Proof of Age Required - 19
29
+ # HFP - Card for pickup
30
+ # DNS - Do not safe drop
31
+ # LAD - Leave at door - do not card
32
+ OPTION_CODES = ["SO", "COV", "COD", "PA18", "PA19", "HFP", "DNS", "LAD"]
33
+
34
+ # List of available Service Codes
35
+ # DOM.RP - Regular Parcel
36
+ # DOM.EP - Expedited Parcel
37
+ # DOM.XP - Xpresspost
38
+ # DOM.XP.CERT - Xpresspost Certified
39
+ # DOM.PC - Priority
40
+ # DOM.DT - Delivered Tonight
41
+ # DOM.LIB - Library Books
42
+ # USA.EP - Expedited Parcel USA
43
+ # USA.PW.ENV - Priority Worldwide Envelope USA
44
+ # USA.PW.PAK - Priority Worldwide pak USA
45
+ # USA.PW.Parcel - Priority Worldwide Parcel USA
46
+ # USA.SP.AIR - Small Packet USA Air
47
+ # USA.TP - Tracked Package - USA
48
+ # USA.TP.LVM - Tracked Package - USA (LVM) (large volume mailers)
49
+ # USA.XP - Xpresspost USA
50
+ # INT.XP - Xpresspost international
51
+ # INT.IP.AIR - International Parcel Air
52
+ # INT.IP.SURF - International Parcel Surface
53
+ # INT.PW.ENV - Priority Worldwide Envelope Int'l
54
+ # INT.PW.PAK - Priority Worldwide pak Int'l
55
+ # INT.PW.PARCEL - Priority Worldwide Parcel Int'l
56
+ # INT.SP.AIR - Small Packet International Air
57
+ # INT.SP.SURF - Small Packet International Surface
58
+ # INT.TP - Tracked Package - International
59
+ SERVICE_CODES = {
60
+ "DOM.RP" => 'Regular Parcel',
61
+ "DOM.EP" => 'Expedited Parcel',
62
+ "DOM.XP" => 'Xpresspost',
63
+ "DOM.XP.CERT" => 'Xpresspost Certified',
64
+ "DOM.PC" => 'Priority',
65
+ "DOM.DT" => 'Delivered Tonight',
66
+ "DOM.LIB" => 'Library Books',
67
+ "USA.EP" => 'Expedited Parcel USA',
68
+ "USA.PW.ENV" => 'Priority Worldwide Envelope USA',
69
+ "USA.PW.PAK" => 'Priority Worldwide pak USA',
70
+ "USA.PW.PARCEL" => 'Priority Worldwide Parcel USA',
71
+ "USA.SP.AIR" => 'Small Packet USA Air',
72
+ "USA.TP" => 'Tracked Package - USA',
73
+ "USA.TP.LVM" => 'Tracked Package - USA (LVM) (large volume mailers)',
74
+ "USA.XP" => 'Xpresspost USA',
75
+ "INT.XP" => 'Xpresspost international',
76
+ "INT.IP.AIR" => 'International Parcel Air',
77
+ "INT.IP.SURF" => 'International Parcel Surface',
78
+ "INT.PW.ENV" => "Priority Worldwide Envelope Int'l",
79
+ "INT.PW.PAK" => "Priority Worldwide pak Int'l",
80
+ "INT.PW.PARCEL" => "Priority Worldwide Parcel Int'l",
81
+ "INT.SP.AIR" => 'Small Packet International Air',
82
+ "INT.SP.SURF" => 'Small Packet International Surface',
83
+ "INT.TP" => 'Tracked Package - International'
84
+ }
85
+
86
+ def initialize(credentials, options = {})
87
+ @credentials = credentials
88
+ @authorization = {username: @credentials.username, password: @credentials.password}
89
+ @customer_number = @credentials.customer_number
90
+ end
91
+
92
+ # def initialize(credentials, options={})
93
+ # requires!(options, :shipper, :recipient, :package)
94
+ # @credentials = credentials
95
+ # @shipper, @recipient, @package, @service_type = options[:shipper], options[:recipient], options[:package], options[:service_type]
96
+ # @authorization = { username: @credentials.username, password: @credentials.password }
97
+ # @customer_number = @credentials.customer_number
98
+ # end
99
+
100
+ def process_request
101
+ raise NotImplementedError, "Override #process_request in subclass"
102
+ end
103
+
104
+ # Sends POST request to CanadaPost API and parse the response,
105
+ # a class object (Shipment, Rate...) is created if the response is successful
106
+ def client(url, body, headers)
107
+ self.class.post(
108
+ url,
109
+ body: body,
110
+ headers: headers,
111
+ basic_auth: @authorization
112
+ )
113
+ end
114
+
115
+ def api_url
116
+ @credentials.mode == "production" ? PRODUCTION_URL : TEST_URL
117
+ end
118
+
119
+ def build_xml
120
+ raise NotImplementedError, "Override #build_xml in subclass"
121
+ end
122
+
123
+ # Parse response, convert keys to underscore symbols
124
+ def parse_response(response)
125
+ response = Hash.from_xml(response.parsed_response.gsub("\n", "")) if response.parsed_response.is_a? String
126
+ response = sanitize_response_keys(response)
127
+ end
128
+
129
+ def process_response(api_response)
130
+ shipping_response = {errors: ''}
131
+ response = parse_response(api_response)
132
+ if response[:messages].present?
133
+ response[:messages].each do |key, message|
134
+ shipping_response[:errors] << message[:description].split('}').last
135
+ end
136
+ return shipping_response
137
+ end
138
+
139
+ return response
140
+ end
141
+
142
+ # Recursively sanitizes the response object by cleaning up any hash keys.
143
+ def sanitize_response_keys(response)
144
+ if response.is_a?(Hash)
145
+ response.inject({}) { |result, (key, value)| result[underscorize(key).to_sym] = sanitize_response_keys(value); result }
146
+ elsif response.is_a?(Array)
147
+ response.collect { |result| sanitize_response_keys(result) }
148
+ else
149
+ response
150
+ end
151
+ end
152
+
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,103 @@
1
+ module CanadaPost
2
+ module Request
3
+ class Manifest < Base
4
+
5
+ attr_accessor :phone, :destination, :group_id
6
+
7
+ def initialize(credentials, options={})
8
+ @credentials = credentials
9
+ if options.present?
10
+ @phone = options[:phone]
11
+ @destination = options[:destination]
12
+ @group_id = options[:group_id]
13
+ end
14
+ super(credentials)
15
+ end
16
+
17
+ def process_request
18
+ api_response = self.class.post(
19
+ api_url,
20
+ body: build_xml,
21
+ headers: manifest_header,
22
+ basic_auth: @authorization
23
+ )
24
+ process_response(api_response)
25
+ end
26
+
27
+ def get_manifest(url)
28
+ api_response = self.class.get(
29
+ url,
30
+ headers: manifest_header,
31
+ basic_auth: @authorization
32
+ )
33
+ process_response(api_response)
34
+ end
35
+
36
+ def get_artifact(url)
37
+ self.class.get(
38
+ url,
39
+ headers: artifact_header,
40
+ basic_auth: @authorization
41
+ )
42
+ end
43
+
44
+ private
45
+
46
+ def api_url
47
+ api_url = @credentials.mode == "production" ? PRODUCTION_URL : TEST_URL
48
+ api_url += "/rs/#{@credentials.customer_number}/#{@credentials.customer_number}/manifest"
49
+ end
50
+
51
+ def manifest_header
52
+ {
53
+ 'Content-type' => 'application/vnd.cpc.manifest-v7+xml',
54
+ 'Accept' => 'application/vnd.cpc.manifest-v7+xml'
55
+ }
56
+ end
57
+
58
+ def artifact_header
59
+ {
60
+ 'Content-type' => 'application/pdf',
61
+ 'Accept' => 'application/pdf'
62
+ }
63
+ end
64
+
65
+ def build_xml
66
+ ns = "http://www.canadapost.ca/ws/manifest-v7"
67
+ xsi = 'http://www.w3.org/2001/XMLSchema-instance'
68
+ builder = Nokogiri::XML::Builder.new do |xml|
69
+ xml.send(:"transmit-set", :'xmlns:xsi' => xsi, xmlns: ns) {
70
+ xml.send(:'group-ids') {
71
+ xml.send(:'group-id', @group_id)
72
+ }
73
+ xml.send(:'detailed-manifests', true)
74
+ xml.send(:'method-of-payment', 'Account')
75
+ xml.send(:'manifest-address') {
76
+ add_manifest_details(xml)
77
+ }
78
+ }
79
+ end
80
+ builder.doc.root.to_xml
81
+ end
82
+
83
+ def add_manifest_details(xml)
84
+ xml.send(:'manifest-company', @destination[:company])
85
+ xml.send(:'manifest-name', @destination[:name])
86
+ xml.send(:'phone-number', @phone)
87
+ xml.send(:'address-details') {
88
+ manifest_address(xml, @destination[:address_details])
89
+ }
90
+ end
91
+
92
+ def manifest_address(xml, params)
93
+ xml.send(:'address-line-1', params[:address])
94
+ xml.send(:'city', params[:city])
95
+ xml.send(:'prov-state', params[:state])
96
+ if params[:postal_code].present?
97
+ xml.send(:'postal-zip-code', params[:postal_code].gsub(' ', ''))
98
+ end
99
+ end
100
+
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,122 @@
1
+ module CanadaPost
2
+ module Request
3
+ class Rate < Base
4
+
5
+ attr_accessor :shipper, :recipient, :package
6
+
7
+ def initialize(credentials, options={})
8
+ requires!(options, :shipper, :recipient, :package)
9
+ @credentials = credentials
10
+ @shipper, @recipient, @package, @service_type = options[:shipper], options[:recipient], options[:package], options[:service_type]
11
+ super(credentials)
12
+ end
13
+
14
+ def process_request
15
+ api_response = client(rate_url, build_xml, rate_headers)
16
+ response = parse_response(api_response)
17
+
18
+ if success?(response)
19
+ rate_reply_details = response[:price_quotes][:price_quote] || []
20
+ rate_reply_details = [rate_reply_details] if rate_reply_details.is_a? Hash
21
+ rate_reply_details.map do |rate_reply|
22
+ CanadaPost::Rate.new(rate_reply)
23
+ end
24
+ else
25
+ error_message =
26
+ if response[:messages]
27
+ response[:messages][:message][:description]
28
+ else
29
+ 'api_response.response'
30
+ end
31
+ raise RateError, error_message
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def rate_url
38
+ api_url + "/rs/ship/price"
39
+ end
40
+
41
+ def rate_headers
42
+ {
43
+ 'Content-type' => 'application/vnd.cpc.ship.rate-v3+xml',
44
+ 'Accept' => 'application/vnd.cpc.ship.rate-v3+xml'
45
+ }
46
+ end
47
+
48
+ def build_xml
49
+ ns = "http://www.canadapost.ca/ws/ship/rate-v3"
50
+ builder = Nokogiri::XML::Builder.new do |xml|
51
+ xml.send(:"mailing-scenario", xmlns: ns) {
52
+ add_requested_shipment(xml)
53
+ }
54
+ end
55
+ builder.doc.root.to_xml
56
+ end
57
+
58
+ def add_requested_shipment(xml)
59
+ xml.send(:"customer-number", @customer_number)
60
+ add_package(xml)
61
+ add_services(xml)
62
+ add_shipper(xml)
63
+ add_recipient(xml)
64
+ end
65
+
66
+ def add_shipper(xml)
67
+ xml.send(:"origin-postal-code", @shipper[:postal_code])
68
+ end
69
+
70
+ def add_recipient(xml)
71
+ xml.destination {
72
+ add_destination(xml)
73
+ }
74
+ end
75
+
76
+ def add_package(xml)
77
+ xml.send(:"parcel-characteristics") {
78
+ xml.weight @package[:weight][:value]
79
+ if @package[:dimensions]
80
+ xml.dimensions {
81
+ xml.height @package[:dimensions][:height].round(1)
82
+ xml.width @package[:dimensions][:width].round(1)
83
+ xml.length @package[:dimensions][:length].round(1)
84
+ }
85
+ end
86
+ if @package[:cylinder]
87
+ xml.send(:"mailing-tube", @package[:cylinder])
88
+ end
89
+ }
90
+ end
91
+
92
+ def add_destination(xml)
93
+ if @recipient[:country_code] == "CA"
94
+ xml.domestic {
95
+ xml.send(:"postal-code", @recipient[:postal_code])
96
+ }
97
+ elsif @recipient[:country_code] == "US"
98
+ xml.send(:"united-states") {
99
+ xml.send(:"zip-code", @recipient[:postal_code])
100
+ }
101
+ else
102
+ xml.international {
103
+ xml.send(:"country-code", @recipient[:country_code])
104
+ }
105
+ end
106
+ end
107
+
108
+ def add_services(xml)
109
+ if @service_type
110
+ xml.services {
111
+ xml.send(:"service-code", @service_type)
112
+ }
113
+ end
114
+ end
115
+
116
+ def success?(response)
117
+ response[:price_quotes]
118
+ end
119
+
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,49 @@
1
+ module CanadaPost
2
+ module Request
3
+ class Registration < Base
4
+
5
+ def initialize(credentials)
6
+ @credentials = credentials
7
+ super(credentials)
8
+ end
9
+
10
+ def get_token
11
+ api_response = self.class.post(
12
+ api_url,
13
+ headers: api_header,
14
+ basic_auth: @authorization
15
+ )
16
+ shipping_response = process_response(api_response)
17
+ if shipping_response[:token].present?
18
+ shipping_response[:token]
19
+ else
20
+ shipping_response
21
+ end
22
+ end
23
+
24
+ def merchant_info(token)
25
+ merchant_url = api_url + "/#{token}"
26
+ api_response = self.class.get(
27
+ merchant_url,
28
+ headers: api_header,
29
+ basic_auth: @authorization
30
+ )
31
+ process_response(api_response)
32
+ end
33
+
34
+ private
35
+
36
+ def api_header
37
+ {
38
+ 'Accept-Language' => 'en-CA',
39
+ 'Accept' => 'application/vnd.cpc.registration+xml'
40
+ }
41
+ end
42
+
43
+ def api_url
44
+ api_url = @credentials.mode == "production" ? PRODUCTION_URL : TEST_URL
45
+ api_url += "/ot/token"
46
+ end
47
+ end
48
+ end
49
+ end