active_shipping 0.9.15 → 0.10.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.
- data/README.markdown +1 -1
- data/lib/active_shipping.rb +1 -0
- data/lib/active_shipping/shipping/carriers.rb +2 -1
- data/lib/active_shipping/shipping/carriers/canada_post.rb +5 -4
- data/lib/active_shipping/shipping/carriers/canada_post_pws.rb +837 -0
- data/lib/active_shipping/shipping/carriers/fedex.rb +33 -19
- data/lib/active_shipping/shipping/carriers/ups.rb +72 -69
- data/lib/active_shipping/shipping/carriers/ups.rb.orig +456 -0
- data/lib/active_shipping/shipping/carriers/usps.rb +185 -9
- data/lib/active_shipping/shipping/carriers/usps.rb.orig +616 -0
- data/lib/active_shipping/shipping/package.rb +109 -10
- data/lib/active_shipping/shipping/rate_estimate.rb +10 -6
- data/lib/active_shipping/shipping/shipping_response.rb +16 -0
- data/lib/active_shipping/shipping/tracking_response.rb +3 -1
- data/lib/active_shipping/version.rb +1 -1
- metadata +23 -3
data/README.markdown
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Active Shipping
|
2
2
|
|
3
|
-

|
3
|
+
[](https://travis-ci.org/Shopify/active_shipping)
|
4
4
|
|
5
5
|
This library interfaces with the web services of various shipping carriers. The goal is to abstract the features that are most frequently used into a pleasant and consistent Ruby API. Active Shipping is an extension of [Active Merchant][], and as such, it borrows heavily from conventions used in the latter.
|
6
6
|
|
data/lib/active_shipping.rb
CHANGED
@@ -41,6 +41,7 @@ require 'active_shipping/shipping/base'
|
|
41
41
|
require 'active_shipping/shipping/response'
|
42
42
|
require 'active_shipping/shipping/rate_response'
|
43
43
|
require 'active_shipping/shipping/tracking_response'
|
44
|
+
require 'active_shipping/shipping/shipping_response'
|
44
45
|
require 'active_shipping/shipping/package'
|
45
46
|
require 'active_shipping/shipping/location'
|
46
47
|
require 'active_shipping/shipping/rate_estimate'
|
@@ -6,13 +6,14 @@ require 'active_shipping/shipping/carriers/shipwire'
|
|
6
6
|
require 'active_shipping/shipping/carriers/kunaki'
|
7
7
|
require 'active_shipping/shipping/carriers/canada_post'
|
8
8
|
require 'active_shipping/shipping/carriers/new_zealand_post'
|
9
|
+
require 'active_shipping/shipping/carriers/canada_post_pws'
|
9
10
|
|
10
11
|
module ActiveMerchant
|
11
12
|
module Shipping
|
12
13
|
module Carriers
|
13
14
|
class <<self
|
14
15
|
def all
|
15
|
-
[BogusCarrier, UPS, USPS, FedEx, Shipwire, Kunaki, CanadaPost, NewZealandPost]
|
16
|
+
[BogusCarrier, UPS, USPS, FedEx, Shipwire, Kunaki, CanadaPost, NewZealandPost, CanadaPostPWS]
|
16
17
|
end
|
17
18
|
end
|
18
19
|
end
|
@@ -99,10 +99,10 @@ module ActiveMerchant
|
|
99
99
|
|
100
100
|
def self.default_location
|
101
101
|
{
|
102
|
-
:country
|
103
|
-
:province
|
104
|
-
:city
|
105
|
-
:address1
|
102
|
+
:country => 'CA',
|
103
|
+
:province => 'ON',
|
104
|
+
:city => 'Ottawa',
|
105
|
+
:address1 => '61A York St',
|
106
106
|
:postal_code => 'K1N5T2'
|
107
107
|
}
|
108
108
|
end
|
@@ -160,6 +160,7 @@ module ActiveMerchant
|
|
160
160
|
:service_code => service_code,
|
161
161
|
:total_price => product.get_text('rate').to_s,
|
162
162
|
:currency => 'CAD',
|
163
|
+
:shipping_date => product.get_text('shippingDate').to_s,
|
163
164
|
:delivery_range => [product.get_text('deliveryDate').to_s] * 2
|
164
165
|
)
|
165
166
|
end
|
@@ -0,0 +1,837 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
|
3
|
+
module ActiveMerchant
|
4
|
+
module Shipping
|
5
|
+
|
6
|
+
class CanadaPostPWS < Carrier
|
7
|
+
@@name = "Canada Post PWS"
|
8
|
+
|
9
|
+
SHIPPING_SERVICES = {
|
10
|
+
"DOM.RP" => "Regular Parcel",
|
11
|
+
"DOM.EP" => "Expedited Parcel",
|
12
|
+
"DOM.XP" => "Xpresspost",
|
13
|
+
"DOM.XP.CERT" => "Xpresspost Certified",
|
14
|
+
"DOM.PC" => "Priority",
|
15
|
+
"DOM.LIB" => "Library Books",
|
16
|
+
|
17
|
+
"USA.EP" => "Expedited Parcel USA",
|
18
|
+
"USA.PW.ENV" => "Priority Worldwide Envelope USA",
|
19
|
+
"USA.PW.PAK" => "Priority Worldwide pak USA",
|
20
|
+
"USA.PW.PARCEL" => "Priority Worldwide Parcel USA",
|
21
|
+
"USA.SP.AIR" => "Small Packet USA Air",
|
22
|
+
"USA.SP.SURF" => "Small Packet USA Surface",
|
23
|
+
"USA.XP" => "Xpresspost USA",
|
24
|
+
|
25
|
+
"INT.XP" => "Xpresspost International",
|
26
|
+
"INT.IP.AIR" => "International Parcel Air",
|
27
|
+
"INT.IP.SURF" => "International Parcel Surface",
|
28
|
+
"INT.PW.ENV" => "Priority Worldwide Envelope Int'l",
|
29
|
+
"INT.PW.PAK" => "Priority Worldwide pak Int'l",
|
30
|
+
"INT.PW.PARCEL" => "Priority Worldwide parcel Int'l",
|
31
|
+
"INT.SP.AIR" => "Small Packet International Air",
|
32
|
+
"INT.SP.SURF" => "Small Packet International Surface"
|
33
|
+
}
|
34
|
+
|
35
|
+
ENDPOINT = "https://soa-gw.canadapost.ca/" # production
|
36
|
+
|
37
|
+
SHIPMENT_MIMETYPE = "application/vnd.cpc.ncshipment+xml"
|
38
|
+
RATE_MIMETYPE = "application/vnd.cpc.ship.rate+xml"
|
39
|
+
TRACK_MIMETYPE = "application/vnd.cpc.track+xml"
|
40
|
+
REGISTER_MIMETYPE = "application/vnd.cpc.registration+xml"
|
41
|
+
|
42
|
+
LANGUAGE = {
|
43
|
+
'en' => 'en-CA',
|
44
|
+
'fr' => 'fr-CA'
|
45
|
+
}
|
46
|
+
|
47
|
+
SHIPPING_OPTIONS = [:d2po, :d2po_office_id, :cov, :cov_amount, :cod, :cod_amount, :cod_includes_shipping,
|
48
|
+
:cod_method_of_payment, :so, :dc, :dns, :pa18, :pa19, :hfp, :lad,
|
49
|
+
:rase, :rts, :aban]
|
50
|
+
|
51
|
+
RATES_OPTIONS = [:cov, :cov_amount, :cod, :so, :dc, :dns, :pa18, :pa19, :hfp, :lad]
|
52
|
+
|
53
|
+
MAX_WEIGHT = 30 # kg
|
54
|
+
|
55
|
+
attr_accessor :language, :endpoint, :logger, :platform_id
|
56
|
+
|
57
|
+
def initialize(options = {})
|
58
|
+
@language = LANGUAGE[options[:language]] || LANGUAGE['en']
|
59
|
+
@endpoint = options[:endpoint] || ENDPOINT
|
60
|
+
@platform_id = options[:platform_id]
|
61
|
+
super(options)
|
62
|
+
end
|
63
|
+
|
64
|
+
def requirements
|
65
|
+
[:api_key, :secret]
|
66
|
+
end
|
67
|
+
|
68
|
+
def find_rates(origin, destination, line_items = [], options = {}, package = nil, services = [])
|
69
|
+
url = endpoint + "rs/ship/price"
|
70
|
+
request = build_rates_request(origin, destination, line_items, options, package, services)
|
71
|
+
response = ssl_post(url, request, headers(options, RATE_MIMETYPE, RATE_MIMETYPE))
|
72
|
+
parse_rates_response(response, origin, destination)
|
73
|
+
rescue ActiveMerchant::ResponseError, ActiveMerchant::Shipping::ResponseError => e
|
74
|
+
error_response(e.response.body, CPPWSRateResponse)
|
75
|
+
end
|
76
|
+
|
77
|
+
def find_tracking_info(pin, options = {})
|
78
|
+
response = ssl_get(tracking_url(pin), headers(options, TRACK_MIMETYPE))
|
79
|
+
parse_tracking_response(response)
|
80
|
+
rescue ActiveMerchant::ResponseError, ActiveMerchant::Shipping::ResponseError => e
|
81
|
+
error_response(e.response.body, CPPWSTrackingResponse)
|
82
|
+
rescue InvalidPinFormatError => e
|
83
|
+
CPPWSTrackingResponse.new(false, "Invalid Pin Format", {}, {:carrier => @@name})
|
84
|
+
end
|
85
|
+
|
86
|
+
# line_items should be a list of PackageItem's
|
87
|
+
def create_shipment(origin, destination, package, line_items = [], options = {})
|
88
|
+
request_body = build_shipment_request(origin, destination, package, line_items, options)
|
89
|
+
response = ssl_post(create_shipment_url(options), request_body, headers(options, SHIPMENT_MIMETYPE, SHIPMENT_MIMETYPE))
|
90
|
+
parse_shipment_response(response)
|
91
|
+
rescue ActiveMerchant::ResponseError, ActiveMerchant::Shipping::ResponseError => e
|
92
|
+
error_response(e.response.body, CPPWSShippingResponse)
|
93
|
+
rescue MissingCustomerNumberError => e
|
94
|
+
CPPWSShippingResponse.new(false, "Missing Customer Number", {}, {:carrier => @@name})
|
95
|
+
end
|
96
|
+
|
97
|
+
def retrieve_shipment(shipping_id, options = {})
|
98
|
+
response = ssl_post(shipment_url(shipping_id, options), nil, headers(options, SHIPMENT_MIMETYPE, SHIPMENT_MIMETYPE))
|
99
|
+
shipping_response = parse_shipment_response(response)
|
100
|
+
end
|
101
|
+
|
102
|
+
def find_shipment_receipt(shipping_id, options = {})
|
103
|
+
response = ssl_get(shipment_receipt_url(shipping_id, options), headers(options, SHIPMENT_MIMETYPE, SHIPMENT_MIMETYPE))
|
104
|
+
shipping_response = parse_shipment_receipt_response(response)
|
105
|
+
end
|
106
|
+
|
107
|
+
def retrieve_shipping_label(shipping_response, options = {})
|
108
|
+
raise MissingShippingNumberError unless shipping_response && shipping_response.shipping_id
|
109
|
+
ssl_get(shipping_response.label_url, headers(options, "application/pdf"))
|
110
|
+
end
|
111
|
+
|
112
|
+
def register_merchant(options = {})
|
113
|
+
url = endpoint + "ot/token"
|
114
|
+
response = ssl_post(url, nil, headers({}, REGISTER_MIMETYPE, REGISTER_MIMETYPE).merge({"Content-Length" => "0"}))
|
115
|
+
parse_register_token_response(response)
|
116
|
+
rescue ActiveMerchant::ResponseError, ActiveMerchant::Shipping::ResponseError => e
|
117
|
+
error_response(e.response.body, CPPWSRegisterResponse)
|
118
|
+
end
|
119
|
+
|
120
|
+
def retrieve_merchant_details(options = {})
|
121
|
+
raise MissingTokenIdError unless token_id = options[:token_id]
|
122
|
+
url = endpoint + "ot/token/#{token_id}"
|
123
|
+
response = ssl_get(url, headers({}, REGISTER_MIMETYPE, REGISTER_MIMETYPE))
|
124
|
+
parse_merchant_details_response(response)
|
125
|
+
rescue ActiveMerchant::ResponseError, ActiveMerchant::Shipping::ResponseError => e
|
126
|
+
error_response(e.response.body, CPPWSMerchantDetailsResponse)
|
127
|
+
rescue Exception => e
|
128
|
+
raise ResponseError.new(e.message)
|
129
|
+
end
|
130
|
+
|
131
|
+
def find_services(country = nil, options = {})
|
132
|
+
response = ssl_get(services_url(country), headers(options, RATE_MIMETYPE))
|
133
|
+
parse_services_response(response)
|
134
|
+
rescue ActiveMerchant::ResponseError, ActiveMerchant::Shipping::ResponseError => e
|
135
|
+
error_response(e.response.body, CPPWSRateResponse)
|
136
|
+
end
|
137
|
+
|
138
|
+
def find_service_options(service_code, country, options = {})
|
139
|
+
response = ssl_get(services_url(country, service_code), headers(options, RATE_MIMETYPE))
|
140
|
+
parse_service_options_response(response)
|
141
|
+
rescue ActiveMerchant::ResponseError, ActiveMerchant::Shipping::ResponseError => e
|
142
|
+
error_response(e.response.body, CPPWSRateResponse)
|
143
|
+
end
|
144
|
+
|
145
|
+
def find_option_details(option_code, options = {})
|
146
|
+
url = endpoint + "rs/ship/option/#{option_code}"
|
147
|
+
response = ssl_get(url, headers(options, RATE_MIMETYPE))
|
148
|
+
parse_option_response(response)
|
149
|
+
rescue ActiveMerchant::ResponseError, ActiveMerchant::Shipping::ResponseError => e
|
150
|
+
error_response(e.response.body, CPPWSRateResponse)
|
151
|
+
end
|
152
|
+
|
153
|
+
def maximum_weight
|
154
|
+
Mass.new(MAX_WEIGHT, :kilograms)
|
155
|
+
end
|
156
|
+
|
157
|
+
# service discovery
|
158
|
+
|
159
|
+
def parse_services_response(response)
|
160
|
+
doc = REXML::Document.new(REXML::Text::unnormalize(response))
|
161
|
+
service_nodes = doc.elements['services'].elements.collect('service') {|node| node }
|
162
|
+
services = service_nodes.inject({}) do |result, node|
|
163
|
+
service_code = node.get_text("service-code").to_s
|
164
|
+
service_name = node.get_text("service-name").to_s
|
165
|
+
service_link = node.elements["link"].attributes['href']
|
166
|
+
service_link_media_type = node.elements["link"].attributes['media-type']
|
167
|
+
result[service_code] = {
|
168
|
+
:name => service_name,
|
169
|
+
:link => service_link,
|
170
|
+
:link_media_type => service_link_media_type
|
171
|
+
}
|
172
|
+
result
|
173
|
+
end
|
174
|
+
services
|
175
|
+
end
|
176
|
+
|
177
|
+
def parse_service_options_response(response)
|
178
|
+
doc = REXML::Document.new(REXML::Text::unnormalize(response))
|
179
|
+
service_node = doc.elements['service']
|
180
|
+
service_code = service_node.get_text("service-code").to_s
|
181
|
+
service_name = service_node.get_text("service-name").to_s
|
182
|
+
options_node = service_node.elements['options']
|
183
|
+
unless options_node.blank?
|
184
|
+
option_nodes = options_node.elements.collect('option') {|node| node}
|
185
|
+
options = option_nodes.inject([]) do |result, node|
|
186
|
+
option = {
|
187
|
+
:code => node.get_text("option-code").to_s,
|
188
|
+
:name => node.get_text("option-name").to_s,
|
189
|
+
:required => node.get_text("mandatory").to_s == "false" ? false : true,
|
190
|
+
:qualifier_required => node.get_text("qualifier-required").to_s == "false" ? false : true
|
191
|
+
}
|
192
|
+
option[:qualifier_max] = node.get_text("qualifier-max").to_s.to_i if node.get_text("qualifier-max")
|
193
|
+
result << option
|
194
|
+
result
|
195
|
+
end
|
196
|
+
end
|
197
|
+
restrictions_node = service_node.elements['restrictions']
|
198
|
+
dimensions_node = restrictions_node.elements['dimensional-restrictions']
|
199
|
+
restrictions = {
|
200
|
+
:min_weight => restrictions_node.elements["weight-restriction"].attributes['min'].to_i,
|
201
|
+
:max_weight => restrictions_node.elements["weight-restriction"].attributes['max'].to_i,
|
202
|
+
:min_length => dimensions_node.elements["length"].attributes['min'].to_f,
|
203
|
+
:max_length => dimensions_node.elements["length"].attributes['max'].to_f,
|
204
|
+
:min_height => dimensions_node.elements["height"].attributes['min'].to_f,
|
205
|
+
:max_height => dimensions_node.elements["height"].attributes['max'].to_f,
|
206
|
+
:min_width => dimensions_node.elements["width"].attributes['min'].to_f,
|
207
|
+
:max_width => dimensions_node.elements["width"].attributes['max'].to_f,
|
208
|
+
}
|
209
|
+
|
210
|
+
{
|
211
|
+
:service_code => service_code,
|
212
|
+
:service_name => service_name,
|
213
|
+
:options => options,
|
214
|
+
:restrictions => restrictions
|
215
|
+
}
|
216
|
+
end
|
217
|
+
|
218
|
+
def parse_option_response(response)
|
219
|
+
doc = REXML::Document.new(REXML::Text::unnormalize(response))
|
220
|
+
option_node = doc.elements['option']
|
221
|
+
conflicts = option_node.elements['conflicting-options'].elements.collect('option-code') {|node| node.get_text.to_s} unless option_node.elements['conflicting-options'].blank?
|
222
|
+
prereqs = option_node.elements['prerequisite-options'].elements.collect('option-code') {|node| node.get_text.to_s} unless option_node.elements['prerequisite-options'].blank?
|
223
|
+
option = {
|
224
|
+
:code => option_node.get_text('option-code').to_s,
|
225
|
+
:name => option_node.get_text('option-name').to_s,
|
226
|
+
:class => option_node.get_text('option-class').to_s,
|
227
|
+
:prints_on_label => option_node.get_text('prints-on-label').to_s == "false" ? false : true,
|
228
|
+
:qualifier_required => option_node.get_text('qualifier-required').to_s == "false" ? false : true
|
229
|
+
}
|
230
|
+
option[:conflicting_options] = conflicts if conflicts
|
231
|
+
option[:prerequisite_options] = prereqs if prereqs
|
232
|
+
|
233
|
+
option[:qualifier_max] = option_node.get_text("qualifier-max").to_s.to_i if option_node.get_text("qualifier-max")
|
234
|
+
option
|
235
|
+
end
|
236
|
+
|
237
|
+
# rating
|
238
|
+
|
239
|
+
def build_rates_request(origin, destination, line_items = [], options = {}, package = nil, services = [])
|
240
|
+
xml = XmlNode.new('mailing-scenario', :xmlns => "http://www.canadapost.ca/ws/ship/rate") do |node|
|
241
|
+
node << customer_number_node(options)
|
242
|
+
node << contract_id_node(options)
|
243
|
+
node << quote_type_node(options)
|
244
|
+
node << expected_mailing_date_node(shipping_date(options)) if options[:shipping_date]
|
245
|
+
options_node = shipping_options_node(RATES_OPTIONS, options)
|
246
|
+
node << options_node if options_node && !options_node.children.count.zero?
|
247
|
+
node << parcel_node(line_items, package)
|
248
|
+
node << origin_node(origin)
|
249
|
+
node << destination_node(destination)
|
250
|
+
node << services_node(services) unless services.blank?
|
251
|
+
end
|
252
|
+
xml.to_s
|
253
|
+
end
|
254
|
+
|
255
|
+
def parse_rates_response(response, origin, destination)
|
256
|
+
doc = REXML::Document.new(REXML::Text::unnormalize(response))
|
257
|
+
raise ActiveMerchant::Shipping::ResponseError, "No Quotes" unless doc.elements['price-quotes']
|
258
|
+
|
259
|
+
quotes = doc.elements['price-quotes'].elements.collect('price-quote') {|node| node }
|
260
|
+
rates = quotes.map do |node|
|
261
|
+
service_name = node.get_text("service-name").to_s
|
262
|
+
service_code = node.get_text("service-code").to_s
|
263
|
+
total_price = node.elements['price-details'].get_text("due").to_s
|
264
|
+
expected_date = expected_date_from_node(node)
|
265
|
+
options = {
|
266
|
+
:service_code => service_code,
|
267
|
+
:total_price => total_price,
|
268
|
+
:currency => 'CAD',
|
269
|
+
:delivery_range => [expected_date, expected_date]
|
270
|
+
}
|
271
|
+
RateEstimate.new(origin, destination, @@name, service_name, options)
|
272
|
+
end
|
273
|
+
CPPWSRateResponse.new(true, "", {}, :rates => rates)
|
274
|
+
end
|
275
|
+
|
276
|
+
|
277
|
+
# tracking
|
278
|
+
|
279
|
+
def parse_tracking_response(response)
|
280
|
+
doc = REXML::Document.new(REXML::Text::unnormalize(response))
|
281
|
+
raise ActiveMerchant::Shipping::ResponseError, "No Tracking" unless root_node = doc.elements['tracking-detail']
|
282
|
+
|
283
|
+
events = root_node.elements['significant-events'].elements.collect('occurrence') {|node| node }
|
284
|
+
|
285
|
+
shipment_events = build_tracking_events(events)
|
286
|
+
change_date = root_node.get_text('changed-expected-date').to_s
|
287
|
+
expected_date = root_node.get_text('expected-delivery-date').to_s
|
288
|
+
dest_postal_code = root_node.get_text('destination-postal-id').to_s
|
289
|
+
destination = Location.new(:postal_code => dest_postal_code)
|
290
|
+
origin = Location.new({})
|
291
|
+
options = {
|
292
|
+
:carrier => @@name,
|
293
|
+
:service_name => root_node.get_text('service-name').to_s,
|
294
|
+
:expected_date => Date.parse(expected_date),
|
295
|
+
:changed_date => change_date.blank? ? nil : Date.parse(change_date),
|
296
|
+
:change_reason => root_node.get_text('changed-expected-delivery-reason').to_s.strip,
|
297
|
+
:destination_postal_code => root_node.get_text('destination-postal-id').to_s,
|
298
|
+
:shipment_events => shipment_events,
|
299
|
+
:tracking_number => root_node.get_text('pin').to_s,
|
300
|
+
:origin => origin,
|
301
|
+
:destination => destination,
|
302
|
+
:customer_number => root_node.get_text('mailed-by-customer-number').to_s
|
303
|
+
}
|
304
|
+
|
305
|
+
CPPWSTrackingResponse.new(true, "", {}, options)
|
306
|
+
end
|
307
|
+
|
308
|
+
def build_tracking_events(events)
|
309
|
+
events.map do |event|
|
310
|
+
date = event.get_text('event-date').to_s
|
311
|
+
time = event.get_text('event-time').to_s
|
312
|
+
zone = event.get_text('event-time-zone').to_s
|
313
|
+
timestamp = DateTime.parse("#{date} #{time} #{zone}")
|
314
|
+
time = Time.utc(timestamp.utc.year, timestamp.utc.month, timestamp.utc.day, timestamp.utc.hour, timestamp.utc.min, timestamp.utc.sec)
|
315
|
+
message = event.get_text('event-description').to_s
|
316
|
+
location = [event.get_text('event-retail-name'), event.get_text('event-site'), event.get_text('event-province')].compact.join(", ")
|
317
|
+
name = event.get_text('event-identifier').to_s
|
318
|
+
ShipmentEvent.new(name, time, location, message)
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
|
323
|
+
# shipping
|
324
|
+
|
325
|
+
# options
|
326
|
+
# :service => 'DOM.EP'
|
327
|
+
# :notification_email
|
328
|
+
# :packing_instructions
|
329
|
+
# :show_postage_rate
|
330
|
+
# :cod, :cod_amount, :insurance, :insurance_amount, :signature_required, :pa18, :pa19, :hfp, :dns, :lad
|
331
|
+
#
|
332
|
+
def build_shipment_request(origin_hash, destination_hash, package, line_items = [], options = {})
|
333
|
+
origin = Location.new(sanitize_zip(origin_hash))
|
334
|
+
destination = Location.new(sanitize_zip(destination_hash))
|
335
|
+
|
336
|
+
xml = XmlNode.new('non-contract-shipment', :xmlns => "http://www.canadapost.ca/ws/ncshipment") do |root_node|
|
337
|
+
root_node << XmlNode.new('delivery-spec') do |node|
|
338
|
+
node << shipment_service_code_node(options)
|
339
|
+
node << shipment_sender_node(origin, options)
|
340
|
+
node << shipment_destination_node(destination, options)
|
341
|
+
options_node = shipment_options_node(options)
|
342
|
+
node << shipment_options_node(options) if options_node && !options_node.children.count.zero?
|
343
|
+
node << shipment_parcel_node(package)
|
344
|
+
node << shipment_notification_node(options)
|
345
|
+
node << shipment_preferences_node(options)
|
346
|
+
node << references_node(options) # optional > user defined custom notes
|
347
|
+
node << shipment_customs_node(destination, line_items, options)
|
348
|
+
# COD Remittance defaults to sender
|
349
|
+
end
|
350
|
+
end
|
351
|
+
xml.to_s
|
352
|
+
end
|
353
|
+
|
354
|
+
def shipment_service_code_node(options)
|
355
|
+
XmlNode.new('service-code', options[:service])
|
356
|
+
end
|
357
|
+
|
358
|
+
def shipment_sender_node(location, options)
|
359
|
+
XmlNode.new('sender') do |node|
|
360
|
+
node << XmlNode.new('name', location.name)
|
361
|
+
node << XmlNode.new('company', location.company) if location.company.present?
|
362
|
+
node << XmlNode.new('contact-phone', location.phone)
|
363
|
+
node << XmlNode.new('address-details') do |innernode|
|
364
|
+
innernode << XmlNode.new('address-line-1', location.address1)
|
365
|
+
address2 = [location.address2, location.address3].reject(&:blank?).join(", ")
|
366
|
+
innernode << XmlNode.new('address-line-2', address2) unless address2.blank?
|
367
|
+
innernode << XmlNode.new('city', location.city)
|
368
|
+
innernode << XmlNode.new('prov-state', location.province)
|
369
|
+
#innernode << XmlNode.new('country-code', location.country_code)
|
370
|
+
innernode << XmlNode.new('postal-zip-code', location.postal_code)
|
371
|
+
end
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
def shipment_destination_node(location, options)
|
376
|
+
XmlNode.new('destination') do |node|
|
377
|
+
node << XmlNode.new('name', location.name)
|
378
|
+
node << XmlNode.new('company', location.company) if location.company.present?
|
379
|
+
node << XmlNode.new('client-voice-number', location.phone)
|
380
|
+
node << XmlNode.new('address-details') do |innernode|
|
381
|
+
innernode << XmlNode.new('address-line-1', location.address1)
|
382
|
+
address2 = [location.address2, location.address3].reject(&:blank?).join(", ")
|
383
|
+
innernode << XmlNode.new('address-line-2', address2) unless address2.blank?
|
384
|
+
innernode << XmlNode.new('city', location.city)
|
385
|
+
innernode << XmlNode.new('prov-state', location.province) unless location.province.blank?
|
386
|
+
innernode << XmlNode.new('country-code', location.country_code)
|
387
|
+
innernode << XmlNode.new('postal-zip-code', location.postal_code)
|
388
|
+
end
|
389
|
+
end
|
390
|
+
end
|
391
|
+
|
392
|
+
def shipment_options_node(options)
|
393
|
+
shipping_options_node(SHIPPING_OPTIONS, options)
|
394
|
+
end
|
395
|
+
|
396
|
+
def shipment_notification_node(options)
|
397
|
+
return unless options[:notification_email]
|
398
|
+
XmlNode.new('notification') do |node|
|
399
|
+
node << XmlNode.new('email', options[:notification_email])
|
400
|
+
node << XmlNode.new('on-shipment', true)
|
401
|
+
node << XmlNode.new('on-exception', true)
|
402
|
+
node << XmlNode.new('on-delivery', true)
|
403
|
+
end
|
404
|
+
end
|
405
|
+
|
406
|
+
def shipment_preferences_node(options)
|
407
|
+
XmlNode.new('preferences') do |node|
|
408
|
+
node << XmlNode.new('show-packing-instructions', options[:packing_instructions] || true)
|
409
|
+
node << XmlNode.new('show-postage-rate', options[:show_postage_rate] || false)
|
410
|
+
node << XmlNode.new('show-insured-value', true)
|
411
|
+
end
|
412
|
+
end
|
413
|
+
|
414
|
+
def references_node(options)
|
415
|
+
# custom values
|
416
|
+
# XmlNode.new('references') do |node|
|
417
|
+
# end
|
418
|
+
end
|
419
|
+
|
420
|
+
def shipment_customs_node(destination, line_items, options)
|
421
|
+
return unless destination.country_code != 'CA'
|
422
|
+
|
423
|
+
XmlNode.new('customs') do |node|
|
424
|
+
currency = options[:currency] || "CAD"
|
425
|
+
node << XmlNode.new('currency',currency)
|
426
|
+
node << XmlNode.new('conversion-from-cad',options[:conversion_from_cad].to_s) if currency != 'CAD' && options[:conversion_from_cad]
|
427
|
+
node << XmlNode.new('reason-for-export','SOG') # SOG - Sale of Goods
|
428
|
+
node << XmlNode.new('other-reason',options[:customs_other_reason]) if (options[:customs_reason_for_export] && options[:customs_other_reason])
|
429
|
+
node << XmlNode.new('additional-customs-info',options[:customs_addition_info]) if options[:customs_addition_info]
|
430
|
+
node << XmlNode.new('sku-list') do |sku|
|
431
|
+
line_items.each do |line_item|
|
432
|
+
sku << XmlNode.new('item') do |item|
|
433
|
+
item << XmlNode.new('hs-tariff-code', line_item.hs_code) if line_item.hs_code && !line_item.hs_code.empty?
|
434
|
+
item << XmlNode.new('sku', line_item.sku) if line_item.sku && !line_item.sku.empty?
|
435
|
+
item << XmlNode.new('customs-description', line_item.name.slice(0,44))
|
436
|
+
item << XmlNode.new('unit-weight', '%#2.3f' % sanitize_weight_kg(line_item.kg))
|
437
|
+
item << XmlNode.new('customs-value-per-unit', '%.2f' % sanitize_price_from_cents(line_item.value))
|
438
|
+
item << XmlNode.new('customs-number-of-units', line_item.quantity)
|
439
|
+
item << XmlNode.new('country-of-origin', line_item.options[:country_of_origin]) if line_item.options && line_item.options[:country_of_origin] && !line_item.options[:country_of_origin].empty?
|
440
|
+
item << XmlNode.new('province-of-origin', line_item.options[:province_of_origin]) if line_item.options && line_item.options[:province_of_origin] && !line_item.options[:province_of_origin].empty?
|
441
|
+
end
|
442
|
+
end
|
443
|
+
end
|
444
|
+
|
445
|
+
end
|
446
|
+
end
|
447
|
+
|
448
|
+
def shipment_parcel_node(package, options ={})
|
449
|
+
weight = sanitize_weight_kg(package.kilograms.to_f)
|
450
|
+
XmlNode.new('parcel-characteristics') do |el|
|
451
|
+
el << XmlNode.new('weight', "%#2.3f" % weight)
|
452
|
+
pkg_dim = package.cm
|
453
|
+
if pkg_dim && !pkg_dim.select{|x| x != 0}.empty?
|
454
|
+
el << XmlNode.new('dimensions') do |dim|
|
455
|
+
dim << XmlNode.new('length', '%.1f' % ((pkg_dim[2]*10).round / 10.0)) if pkg_dim.size >= 3
|
456
|
+
dim << XmlNode.new('width', '%.1f' % ((pkg_dim[1]*10).round / 10.0)) if pkg_dim.size >= 2
|
457
|
+
dim << XmlNode.new('height', '%.1f' % ((pkg_dim[0]*10).round / 10.0)) if pkg_dim.size >= 1
|
458
|
+
end
|
459
|
+
end
|
460
|
+
el << XmlNode.new('document', false)
|
461
|
+
el << XmlNode.new('mailing-tube', package.tube?)
|
462
|
+
el << XmlNode.new('unpackaged', package.unpackaged?)
|
463
|
+
end
|
464
|
+
end
|
465
|
+
|
466
|
+
|
467
|
+
def parse_shipment_response(response)
|
468
|
+
doc = REXML::Document.new(REXML::Text::unnormalize(response))
|
469
|
+
raise ActiveMerchant::Shipping::ResponseError, "No Shipping" unless root_node = doc.elements['non-contract-shipment-info']
|
470
|
+
options = {
|
471
|
+
:shipping_id => root_node.get_text('shipment-id').to_s,
|
472
|
+
:tracking_number => root_node.get_text('tracking-pin').to_s,
|
473
|
+
:details_url => root_node.elements["links/link[@rel='details']"].attributes['href'],
|
474
|
+
:label_url => root_node.elements["links/link[@rel='label']"].attributes['href'],
|
475
|
+
:receipt_url => root_node.elements["links/link[@rel='receipt']"].attributes['href']
|
476
|
+
}
|
477
|
+
CPPWSShippingResponse.new(true, "", {}, options)
|
478
|
+
end
|
479
|
+
|
480
|
+
def parse_register_token_response(response)
|
481
|
+
doc = REXML::Document.new(REXML::Text::unnormalize(response))
|
482
|
+
raise ActiveMerchant::Shipping::ResponseError, "No Registration Token" unless root_node = doc.elements['token']
|
483
|
+
options = {
|
484
|
+
:token_id => root_node.get_text('token-id').to_s
|
485
|
+
}
|
486
|
+
CPPWSRegisterResponse.new(true, "", {}, options)
|
487
|
+
end
|
488
|
+
|
489
|
+
def parse_merchant_details_response(response)
|
490
|
+
doc = REXML::Document.new(REXML::Text::unnormalize(response))
|
491
|
+
raise "No Merchant Info" unless root_node = doc.elements['merchant-info']
|
492
|
+
raise "No Merchant Info" if root_node.get_text('customer-number').blank?
|
493
|
+
options = {
|
494
|
+
:customer_number => root_node.get_text('customer-number').to_s,
|
495
|
+
:contract_number => root_node.get_text('contract-number').to_s,
|
496
|
+
:username => root_node.get_text('merchant-username').to_s,
|
497
|
+
:password => root_node.get_text('merchant-password').to_s,
|
498
|
+
:has_default_credit_card => root_node.get_text('has-default-credit-card') == 'true'
|
499
|
+
}
|
500
|
+
CPPWSMerchantDetailsResponse.new(true, "", {}, options)
|
501
|
+
end
|
502
|
+
|
503
|
+
def parse_shipment_receipt_response(response)
|
504
|
+
doc = REXML::Document.new(REXML::Text::unnormalize(response))
|
505
|
+
root = doc.elements['non-contract-shipment-receipt']
|
506
|
+
cc_details_node = root.elements['cc-receipt-details']
|
507
|
+
service_standard_node = root.elements['service-standard']
|
508
|
+
receipt = {
|
509
|
+
:final_shipping_point => root.get_text("final-shipping-point").to_s,
|
510
|
+
:shipping_point_name => root.get_text("shipping-point-name").to_s,
|
511
|
+
:service_code => root.get_text("service-code").to_s,
|
512
|
+
:rated_weight => root.get_text("rated-weight").to_s.to_f,
|
513
|
+
:base_amount => root.get_text("base-amount").to_s.to_f,
|
514
|
+
:pre_tax_amount => root.get_text("pre-tax-amount").to_s.to_f,
|
515
|
+
:gst_amount => root.get_text("gst-amount").to_s.to_f,
|
516
|
+
:pst_amount => root.get_text("pst-amount").to_s.to_f,
|
517
|
+
:hst_amount => root.get_text("hst-amount").to_s.to_f,
|
518
|
+
:charge_amount => cc_details_node.get_text("charge-amount").to_s.to_f,
|
519
|
+
:currency => cc_details_node.get_text("currency").to_s,
|
520
|
+
:expected_transit_days => service_standard_node.get_text("expected-transit-time").to_s.to_i,
|
521
|
+
:expected_delivery_date => service_standard_node.get_text("expected-delivery-date").to_s
|
522
|
+
}
|
523
|
+
option_nodes = root.elements['priced-options'].elements.collect('priced-option') {|node| node} unless root.elements['priced-options'].blank?
|
524
|
+
|
525
|
+
receipt[:priced_options] = if option_nodes
|
526
|
+
option_nodes.inject({}) do |result, node|
|
527
|
+
result[node.get_text("option-code").to_s] = node.get_text("option-price").to_s.to_f
|
528
|
+
result
|
529
|
+
end
|
530
|
+
else
|
531
|
+
[]
|
532
|
+
end
|
533
|
+
|
534
|
+
receipt
|
535
|
+
end
|
536
|
+
|
537
|
+
def error_response(response, response_klass)
|
538
|
+
doc = REXML::Document.new(REXML::Text::unnormalize(response))
|
539
|
+
messages = doc.elements['messages'].elements.collect('message') {|node| node }
|
540
|
+
message = messages.map {|m| m.get_text('description').to_s }.join(", ")
|
541
|
+
code = messages.map {|m| m.get_text('code').to_s }.join(", ")
|
542
|
+
response_klass.new(false, message, {}, {:carrier => @@name, :code => code})
|
543
|
+
end
|
544
|
+
|
545
|
+
def log(msg)
|
546
|
+
logger.debug(msg) if logger
|
547
|
+
end
|
548
|
+
|
549
|
+
private
|
550
|
+
|
551
|
+
def tracking_url(pin)
|
552
|
+
case pin.length
|
553
|
+
when 12,13,16
|
554
|
+
endpoint + "vis/track/pin/%s/detail" % pin
|
555
|
+
when 15
|
556
|
+
endpoint + "vis/track/dnc/%s/detail" % pin
|
557
|
+
else
|
558
|
+
raise InvalidPinFormatError
|
559
|
+
end
|
560
|
+
end
|
561
|
+
|
562
|
+
def create_shipment_url(options)
|
563
|
+
raise MissingCustomerNumberError unless customer_number = options[:customer_number]
|
564
|
+
if @platform_id.present?
|
565
|
+
endpoint + "rs/#{customer_number}-#{@platform_id}/ncshipment"
|
566
|
+
else
|
567
|
+
endpoint + "rs/#{customer_number}/ncshipment"
|
568
|
+
end
|
569
|
+
end
|
570
|
+
|
571
|
+
def shipment_url(shipping_id, options={})
|
572
|
+
raise MissingCustomerNumberError unless customer_number = options[:customer_number]
|
573
|
+
if @platform_id.present?
|
574
|
+
endpoint + "rs/#{customer_number}-#{@platform_id}/ncshipment/#{shipping_id}"
|
575
|
+
else
|
576
|
+
endpoint + "rs/#{customer_number}/ncshipment/#{shipping_id}"
|
577
|
+
end
|
578
|
+
end
|
579
|
+
|
580
|
+
def shipment_receipt_url(shipping_id, options={})
|
581
|
+
raise MissingCustomerNumberError unless customer_number = options[:customer_number]
|
582
|
+
if @platform_id.present?
|
583
|
+
endpoint + "rs/#{customer_number}-#{@platform_id}/ncshipment/#{shipping_id}/receipt"
|
584
|
+
else
|
585
|
+
endpoint + "rs/#{customer_number}/ncshipment/#{shipping_id}/receipt"
|
586
|
+
end
|
587
|
+
end
|
588
|
+
|
589
|
+
def services_url(country=nil, service_code=nil)
|
590
|
+
url = endpoint + "rs/ship/service"
|
591
|
+
url += "/#{service_code}" if service_code
|
592
|
+
url += "?country=#{country}" if country
|
593
|
+
url
|
594
|
+
end
|
595
|
+
|
596
|
+
def customer_credentials_valid?(credentials)
|
597
|
+
(credentials.keys & [:customer_api_key, :customer_secret]).any?
|
598
|
+
end
|
599
|
+
|
600
|
+
def encoded_authorization(customer_credentials = {})
|
601
|
+
if customer_credentials_valid?(customer_credentials)
|
602
|
+
"Basic %s" % Base64.encode64("#{customer_credentials[:customer_api_key]}:#{customer_credentials[:customer_secret]}")
|
603
|
+
else
|
604
|
+
"Basic %s" % Base64.encode64("#{@options[:api_key]}:#{@options[:secret]}")
|
605
|
+
end
|
606
|
+
end
|
607
|
+
|
608
|
+
def headers(customer_credentials, accept = nil, content_type = nil)
|
609
|
+
headers = {
|
610
|
+
'Authorization' => encoded_authorization(customer_credentials),
|
611
|
+
'Accept-Language' => language
|
612
|
+
}
|
613
|
+
headers['Accept'] = accept if accept
|
614
|
+
headers['Content-Type'] = content_type if content_type
|
615
|
+
headers['Platform-ID'] = platform_id if platform_id && customer_credentials_valid?(customer_credentials)
|
616
|
+
headers
|
617
|
+
end
|
618
|
+
|
619
|
+
def customer_number_node(options)
|
620
|
+
XmlNode.new("customer-number", options[:customer_number])
|
621
|
+
end
|
622
|
+
|
623
|
+
def contract_id_node(options)
|
624
|
+
XmlNode.new("contract-id", options[:contract_id]) if options[:contract_id]
|
625
|
+
end
|
626
|
+
|
627
|
+
def quote_type_node(options)
|
628
|
+
XmlNode.new("quote-type", 'commercial')
|
629
|
+
end
|
630
|
+
|
631
|
+
def expected_mailing_date_node(date_as_string)
|
632
|
+
XmlNode.new("expected-mailing-date", date_as_string)
|
633
|
+
end
|
634
|
+
|
635
|
+
def parcel_node(line_items, package = nil, options ={})
|
636
|
+
weight = sanitize_weight_kg(package && !package.kilograms.zero? ? package.kilograms.to_f : line_items.sum(&:kilograms).to_f)
|
637
|
+
XmlNode.new('parcel-characteristics') do |el|
|
638
|
+
el << XmlNode.new('weight', "%#2.3f" % weight)
|
639
|
+
if package
|
640
|
+
pkg_dim = package.cm
|
641
|
+
if pkg_dim && !pkg_dim.select{|x| x != 0}.empty?
|
642
|
+
el << XmlNode.new('dimensions') do |dim|
|
643
|
+
dim << XmlNode.new('length', '%.1f' % ((pkg_dim[2]*10).round / 10.0)) if pkg_dim.size >= 3
|
644
|
+
dim << XmlNode.new('width', '%.1f' % ((pkg_dim[1]*10).round / 10.0)) if pkg_dim.size >= 2
|
645
|
+
dim << XmlNode.new('height', '%.1f' % ((pkg_dim[0]*10).round / 10.0)) if pkg_dim.size >= 1
|
646
|
+
end
|
647
|
+
end
|
648
|
+
end
|
649
|
+
el << XmlNode.new('mailing-tube', line_items.any?(&:tube?))
|
650
|
+
el << XmlNode.new('oversized', true) if line_items.any?(&:oversized?)
|
651
|
+
el << XmlNode.new('unpackaged', line_items.any?(&:unpackaged?))
|
652
|
+
end
|
653
|
+
end
|
654
|
+
|
655
|
+
def origin_node(location_hash)
|
656
|
+
origin = Location.new(sanitize_zip(location_hash))
|
657
|
+
XmlNode.new("origin-postal-code", origin.zip)
|
658
|
+
end
|
659
|
+
|
660
|
+
def destination_node(location_hash)
|
661
|
+
destination = Location.new(sanitize_zip(location_hash))
|
662
|
+
case destination.country_code
|
663
|
+
when 'CA'
|
664
|
+
XmlNode.new('destination') do |node|
|
665
|
+
node << XmlNode.new('domestic') do |x|
|
666
|
+
x << XmlNode.new('postal-code', destination.postal_code)
|
667
|
+
end
|
668
|
+
end
|
669
|
+
|
670
|
+
when 'US'
|
671
|
+
XmlNode.new('destination') do |node|
|
672
|
+
node << XmlNode.new('united-states') do |x|
|
673
|
+
x << XmlNode.new('zip-code', destination.postal_code)
|
674
|
+
end
|
675
|
+
end
|
676
|
+
|
677
|
+
else
|
678
|
+
XmlNode.new('destination') do |dest|
|
679
|
+
dest << XmlNode.new('international') do |dom|
|
680
|
+
dom << XmlNode.new('country-code', destination.country_code)
|
681
|
+
end
|
682
|
+
end
|
683
|
+
end
|
684
|
+
end
|
685
|
+
|
686
|
+
def services_node(services)
|
687
|
+
XmlNode.new('services') do |node|
|
688
|
+
services.each {|code| node << XmlNode.new('service-code', code)}
|
689
|
+
end
|
690
|
+
end
|
691
|
+
|
692
|
+
def shipping_options_node(available_options, options = {})
|
693
|
+
return if (options.symbolize_keys.keys & available_options).empty?
|
694
|
+
XmlNode.new('options') do |el|
|
695
|
+
|
696
|
+
if options[:cod] && options[:cod_amount]
|
697
|
+
el << XmlNode.new('option') do |opt|
|
698
|
+
opt << XmlNode.new('option-code', 'COD')
|
699
|
+
opt << XmlNode.new('option-amount', options[:cod_amount])
|
700
|
+
opt << XmlNode.new('option-qualifier-1', options[:cod_includes_shipping]) unless options[:cod_includes_shipping].blank?
|
701
|
+
opt << XmlNode.new('option-qualifier-2', options[:cod_method_of_payment]) unless options[:cod_method_of_payment].blank?
|
702
|
+
end
|
703
|
+
end
|
704
|
+
|
705
|
+
if options[:cov]
|
706
|
+
el << XmlNode.new('option') do |opt|
|
707
|
+
opt << XmlNode.new('option-code', 'COV')
|
708
|
+
opt << XmlNode.new('option-amount', options[:cov_amount]) unless options[:cov_amount].blank?
|
709
|
+
end
|
710
|
+
end
|
711
|
+
|
712
|
+
if options[:d2po]
|
713
|
+
el << XmlNode.new('option') do |opt|
|
714
|
+
opt << XmlNode.new('option-code', 'D2PO')
|
715
|
+
opt << XmlNode.new('option-qualifier-2'. options[:d2po_office_id]) unless options[:d2po_office_id].blank?
|
716
|
+
end
|
717
|
+
end
|
718
|
+
|
719
|
+
[:so, :dc, :pa18, :pa19, :hfp, :dns, :lad, :rase, :rts, :aban].each do |code|
|
720
|
+
if options[code]
|
721
|
+
el << XmlNode.new('option') do |opt|
|
722
|
+
opt << XmlNode.new('option-code', code.to_s.upcase)
|
723
|
+
end
|
724
|
+
end
|
725
|
+
end
|
726
|
+
end
|
727
|
+
end
|
728
|
+
|
729
|
+
def expected_date_from_node(node)
|
730
|
+
if service = node.elements['service-standard']
|
731
|
+
expected_date = service.get_text("expected-delivery-date").to_s
|
732
|
+
else
|
733
|
+
expected_date = nil
|
734
|
+
end
|
735
|
+
expected_date
|
736
|
+
end
|
737
|
+
|
738
|
+
def shipping_date(options)
|
739
|
+
DateTime.strptime((options[:shipping_date] || Time.now).to_s, "%Y-%m-%d")
|
740
|
+
end
|
741
|
+
|
742
|
+
def sanitize_zip(hash)
|
743
|
+
[:postal_code, :zip].each do |attr|
|
744
|
+
hash[attr].gsub!(/\s+/,'') if hash[attr]
|
745
|
+
end
|
746
|
+
hash
|
747
|
+
end
|
748
|
+
|
749
|
+
def sanitize_weight_kg(kg)
|
750
|
+
return kg == 0 ? 0.001 : kg;
|
751
|
+
end
|
752
|
+
|
753
|
+
def sanitize_price_from_cents(value)
|
754
|
+
return value == 0 ? 0.01 : value.round / 100.0
|
755
|
+
end
|
756
|
+
|
757
|
+
end
|
758
|
+
|
759
|
+
module CPPWSErrorResponse
|
760
|
+
attr_accessor :error_code
|
761
|
+
def handle_error(message, options)
|
762
|
+
@error_code = options[:code]
|
763
|
+
end
|
764
|
+
end
|
765
|
+
|
766
|
+
class CPPWSRateResponse < RateResponse
|
767
|
+
include CPPWSErrorResponse
|
768
|
+
|
769
|
+
def initialize(success, message, params = {}, options = {})
|
770
|
+
handle_error(message, options)
|
771
|
+
super
|
772
|
+
end
|
773
|
+
end
|
774
|
+
|
775
|
+
class CPPWSTrackingResponse < TrackingResponse
|
776
|
+
include CPPWSErrorResponse
|
777
|
+
|
778
|
+
attr_reader :service_name, :expected_date, :changed_date, :change_reason, :customer_number
|
779
|
+
|
780
|
+
def initialize(success, message, params = {}, options = {})
|
781
|
+
handle_error(message, options)
|
782
|
+
super
|
783
|
+
@service_name = options[:service_name]
|
784
|
+
@expected_date = options[:expected_date]
|
785
|
+
@changed_date = options[:changed_date]
|
786
|
+
@change_reason = options[:change_reason]
|
787
|
+
@customer_number = options[:customer_number]
|
788
|
+
end
|
789
|
+
end
|
790
|
+
|
791
|
+
class CPPWSShippingResponse < ShippingResponse
|
792
|
+
include CPPWSErrorResponse
|
793
|
+
attr_reader :label_url, :details_url, :receipt_url
|
794
|
+
def initialize(success, message, params = {}, options = {})
|
795
|
+
handle_error(message, options)
|
796
|
+
super
|
797
|
+
@label_url = options[:label_url]
|
798
|
+
@details_url = options[:details_url]
|
799
|
+
@receipt_url = options[:receipt_url]
|
800
|
+
end
|
801
|
+
end
|
802
|
+
|
803
|
+
class CPPWSRegisterResponse < Response
|
804
|
+
include CPPWSErrorResponse
|
805
|
+
attr_reader :token_id
|
806
|
+
def initialize(success, message, params = {}, options = {})
|
807
|
+
handle_error(message, options)
|
808
|
+
super
|
809
|
+
@token_id = options[:token_id]
|
810
|
+
end
|
811
|
+
|
812
|
+
def redirect_url(customer_id, return_url)
|
813
|
+
"http://www.canadapost.ca/cpotools/apps/drc/merchant?return-url=#{CGI::escape(return_url)}&token-id=#{token_id}&platform-id=#{customer_id}"
|
814
|
+
end
|
815
|
+
end
|
816
|
+
|
817
|
+
class CPPWSMerchantDetailsResponse < Response
|
818
|
+
include CPPWSErrorResponse
|
819
|
+
attr_reader :customer_number, :contract_number, :username, :password, :has_default_credit_card
|
820
|
+
def initialize(success, message, params = {}, options = {})
|
821
|
+
handle_error(message, options)
|
822
|
+
super
|
823
|
+
@customer_number = options[:customer_number]
|
824
|
+
@contract_number = options[:contract_number]
|
825
|
+
@username = options[:username]
|
826
|
+
@password = options[:password]
|
827
|
+
@has_default_credit_card = options[:has_default_credit_card]
|
828
|
+
end
|
829
|
+
end
|
830
|
+
|
831
|
+
class InvalidPinFormatError < StandardError; end
|
832
|
+
class MissingCustomerNumberError < StandardError; end
|
833
|
+
class MissingShippingNumberError < StandardError; end
|
834
|
+
class MissingTokenIdError < StandardError; end
|
835
|
+
|
836
|
+
end
|
837
|
+
end
|