shipppit-canada-post 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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