active_shipping 0.12.6 → 1.0.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +2 -0
  3. data.tar.gz.sig +0 -0
  4. data/{CHANGELOG → CHANGELOG.md} +6 -2
  5. data/CONTRIBUTING.md +32 -0
  6. data/{README.markdown → README.md} +45 -61
  7. data/lib/active_shipping.rb +20 -28
  8. data/lib/active_shipping/carrier.rb +82 -0
  9. data/lib/active_shipping/carriers.rb +33 -0
  10. data/lib/active_shipping/carriers/benchmark_carrier.rb +31 -0
  11. data/lib/active_shipping/carriers/bogus_carrier.rb +12 -0
  12. data/lib/active_shipping/carriers/canada_post.rb +253 -0
  13. data/lib/active_shipping/carriers/canada_post_pws.rb +870 -0
  14. data/lib/active_shipping/carriers/fedex.rb +579 -0
  15. data/lib/active_shipping/carriers/kunaki.rb +164 -0
  16. data/lib/active_shipping/carriers/new_zealand_post.rb +262 -0
  17. data/lib/active_shipping/carriers/shipwire.rb +181 -0
  18. data/lib/active_shipping/carriers/stamps.rb +861 -0
  19. data/lib/active_shipping/carriers/ups.rb +648 -0
  20. data/lib/active_shipping/carriers/usps.rb +642 -0
  21. data/lib/active_shipping/errors.rb +7 -0
  22. data/lib/active_shipping/label_response.rb +23 -0
  23. data/lib/active_shipping/location.rb +149 -0
  24. data/lib/active_shipping/package.rb +241 -0
  25. data/lib/active_shipping/rate_estimate.rb +64 -0
  26. data/lib/active_shipping/rate_response.rb +13 -0
  27. data/lib/active_shipping/response.rb +41 -0
  28. data/lib/active_shipping/shipment_event.rb +17 -0
  29. data/lib/active_shipping/shipment_packer.rb +73 -0
  30. data/lib/active_shipping/shipping_response.rb +12 -0
  31. data/lib/active_shipping/tracking_response.rb +52 -0
  32. data/lib/active_shipping/version.rb +1 -1
  33. data/lib/vendor/quantified/test/length_test.rb +2 -2
  34. data/lib/vendor/xml_node/test/test_parsing.rb +1 -1
  35. metadata +58 -36
  36. metadata.gz.sig +0 -0
  37. data/lib/active_shipping/shipping/base.rb +0 -13
  38. data/lib/active_shipping/shipping/carrier.rb +0 -84
  39. data/lib/active_shipping/shipping/carriers.rb +0 -23
  40. data/lib/active_shipping/shipping/carriers/benchmark_carrier.rb +0 -33
  41. data/lib/active_shipping/shipping/carriers/bogus_carrier.rb +0 -14
  42. data/lib/active_shipping/shipping/carriers/canada_post.rb +0 -257
  43. data/lib/active_shipping/shipping/carriers/canada_post_pws.rb +0 -874
  44. data/lib/active_shipping/shipping/carriers/fedex.rb +0 -581
  45. data/lib/active_shipping/shipping/carriers/kunaki.rb +0 -166
  46. data/lib/active_shipping/shipping/carriers/new_zealand_post.rb +0 -262
  47. data/lib/active_shipping/shipping/carriers/shipwire.rb +0 -184
  48. data/lib/active_shipping/shipping/carriers/stamps.rb +0 -864
  49. data/lib/active_shipping/shipping/carriers/ups.rb +0 -650
  50. data/lib/active_shipping/shipping/carriers/usps.rb +0 -649
  51. data/lib/active_shipping/shipping/errors.rb +0 -9
  52. data/lib/active_shipping/shipping/label_response.rb +0 -25
  53. data/lib/active_shipping/shipping/location.rb +0 -152
  54. data/lib/active_shipping/shipping/package.rb +0 -243
  55. data/lib/active_shipping/shipping/rate_estimate.rb +0 -66
  56. data/lib/active_shipping/shipping/rate_response.rb +0 -15
  57. data/lib/active_shipping/shipping/response.rb +0 -43
  58. data/lib/active_shipping/shipping/shipment_event.rb +0 -19
  59. data/lib/active_shipping/shipping/shipment_packer.rb +0 -75
  60. data/lib/active_shipping/shipping/shipping_response.rb +0 -14
  61. data/lib/active_shipping/shipping/tracking_response.rb +0 -54
@@ -1,23 +0,0 @@
1
- require 'active_shipping/shipping/carriers/benchmark_carrier'
2
- require 'active_shipping/shipping/carriers/bogus_carrier'
3
- require 'active_shipping/shipping/carriers/ups'
4
- require 'active_shipping/shipping/carriers/usps'
5
- require 'active_shipping/shipping/carriers/fedex'
6
- require 'active_shipping/shipping/carriers/shipwire'
7
- require 'active_shipping/shipping/carriers/kunaki'
8
- require 'active_shipping/shipping/carriers/canada_post'
9
- require 'active_shipping/shipping/carriers/new_zealand_post'
10
- require 'active_shipping/shipping/carriers/canada_post_pws'
11
- require 'active_shipping/shipping/carriers/stamps'
12
-
13
- module ActiveMerchant
14
- module Shipping
15
- module Carriers
16
- class <<self
17
- def all
18
- [BenchmarkCarrier, BogusCarrier, UPS, USPS, FedEx, Shipwire, Kunaki, CanadaPost, NewZealandPost, CanadaPostPWS, Stamps]
19
- end
20
- end
21
- end
22
- end
23
- end
@@ -1,33 +0,0 @@
1
- # The time a 3rd-party shipping provider takes to respond to a request varies greatly.
2
- # This class simulates these unpredictable delays in shipping rate retrieval so that
3
- # load-testing tools run into situations that more accurately reflect the real world.
4
-
5
- module ActiveMerchant
6
- module Shipping
7
- class BenchmarkCarrier < Carrier
8
- cattr_reader :name
9
- @@name = "Benchmark Carrier"
10
-
11
- def find_rates(origin, destination, packages, options = {})
12
- origin = Location.from(origin)
13
- destination = Location.from(destination)
14
- packages = Array(packages)
15
-
16
- delay_time = generate_simulated_lag
17
-
18
- bogus_estimate = RateEstimate.new(
19
- origin, destination, @@name,
20
- "Free Benchmark Shipping", :total_price => 0, :currency => 'USD',
21
- :packages => packages, :delivery_range => [Time.now.utc.strftime("%Y-%d-%m"), Time.now.utc.strftime("%Y-%d-%m")]
22
- )
23
- RateResponse.new(true, "Success (delayed #{delay_time} seconds)", {:rate => 'free'}, :rates => [bogus_estimate], :xml => "<rate>free</rate>")
24
- end
25
-
26
- private
27
-
28
- def generate_simulated_lag(max_delay = 30)
29
- sleep Random.rand * max_delay
30
- end
31
- end
32
- end
33
- end
@@ -1,14 +0,0 @@
1
- module ActiveMerchant
2
- module Shipping
3
- class BogusCarrier < Carrier
4
- cattr_reader :name
5
- @@name = "Bogus Carrier"
6
-
7
- def find_rates(origin, destination, packages, options = {})
8
- origin = Location.from(origin)
9
- destination = Location.from(destination)
10
- packages = Array(packages)
11
- end
12
- end
13
- end
14
- end
@@ -1,257 +0,0 @@
1
- require 'cgi'
2
-
3
- module ActiveMerchant
4
- module Shipping
5
- class CanadaPost < Carrier
6
- # NOTE!
7
- # A Merchant CPC Id must be assigned to you by Canada Post
8
- # CPC_DEMO_XML is just a public domain account for testing
9
-
10
- class CanadaPostRateResponse < RateResponse
11
- attr_reader :boxes, :postal_outlets
12
-
13
- def initialize(success, message, params = {}, options = {})
14
- @boxes = options[:boxes]
15
- @postal_outlets = options[:postal_outlets]
16
- super
17
- end
18
- end
19
-
20
- cattr_reader :name, :name_french
21
- @@name = "Canada Post"
22
- @@name_french = "Postes Canada"
23
-
24
- Box = Struct.new(:name, :weight, :expediter_weight, :length, :width, :height, :packedItems)
25
- PackedItem = Struct.new(:quantity, :description)
26
- PostalOutlet = Struct.new(:sequence_no, :distance, :name, :business_name, :postal_address, :business_hours)
27
-
28
- URL = "http://sellonline.canadapost.ca:30000"
29
- DOCTYPE = '<!DOCTYPE eparcel SYSTEM "http://sellonline.canadapost.ca/DevelopersResources/protocolV3/eParcel.dtd">'
30
-
31
- RESPONSE_CODES = {
32
- '1' => "All calculation was done",
33
- '2' => "Default shipping rates are returned due to a problem during the processing of the request.",
34
- '-2' => "Missing argument when calling module",
35
- '-5' => "No Item to ship",
36
- '-6' => "Illegal Item weight",
37
- '-7' => "Illegal item dimension",
38
- '-12' => "Can't open IM config file",
39
- '-13' => "Can't create log files",
40
- '-15' => "Invalid config file format",
41
- '-102' => "Invalid socket connection",
42
- '-106' => "Can't connect to server",
43
- '-1000' => "Unknow request type sent by client",
44
- '-1002' => "MAS Timed out",
45
- '-1004' => "Socket communication break",
46
- '-1005' => "Did not receive required data on socket.",
47
- '-2000' => "Unable to estabish socket connection with RSSS",
48
- '-2001' => "Merchant Id not found on server",
49
- '-2002' => "One or more parameter was not sent by the IM to the MAS",
50
- '-2003' => "Did not receive required data on socket.",
51
- '-2004' => "The request contains to many items to process it.",
52
- '-2005' => "The request received on socket is larger than the maximum allowed.",
53
- '-3000' => "Origin Postal Code is illegal",
54
- '-3001' => "Destination Postal Code/State Name/ Country is illegal",
55
- '-3002' => "Parcel too large to be shipped with CPC",
56
- '-3003' => "Parcel too small to be shipped with CPC",
57
- '-3004' => "Parcel too heavy to be shipped with CPC",
58
- '-3005' => "Internal error code returned by the rating DLL",
59
- '-3006' => "The pick up time format is invalid or not defined.",
60
- '-4000' => "Volumetric internal error",
61
- '-4001' => "Volumetric time out calculation error.",
62
- '-4002' => "No bins provided to the volumetric engine.",
63
- '-4003' => "No items provided to the volumetric engine.",
64
- '-4004' => "Item is too large to be packed",
65
- '-4005' => "Number of item more than maximum allowed",
66
- '-5000' => "XML Parsing error",
67
- '-5001' => "XML Tag not found",
68
- '-5002' => "Node Value Number format error",
69
- '-5003' => "Node value is empty",
70
- '-5004' => "Unable to create/parse XML Document",
71
- '-6000' => "Unable to open the database",
72
- '-6001' => "Unable to read from the database",
73
- '-6002' => "Unable to write to the database",
74
- '-50000' => "Internal problem - Please contact Sell Online Help Desk"
75
- }
76
-
77
- NON_ISO_COUNTRY_NAMES = {
78
- 'Russian Federation' => 'Russia'
79
- }
80
-
81
- def requirements
82
- [:login]
83
- end
84
-
85
- def find_rates(origin, destination, line_items = [], options = {})
86
- rate_request = build_rate_request(origin, destination, line_items, options)
87
- commit(rate_request, origin, destination, options)
88
- end
89
-
90
- def maximum_weight
91
- Mass.new(30, :kilograms)
92
- end
93
-
94
- def self.default_location
95
- {
96
- :country => 'CA',
97
- :province => 'ON',
98
- :city => 'Ottawa',
99
- :address1 => '61A York St',
100
- :postal_code => 'K1N5T2'
101
- }
102
- end
103
-
104
- protected
105
-
106
- def commit(request, origin, destination, options = {})
107
- parse_rate_response(ssl_post(URL, request), origin, destination, options)
108
- end
109
-
110
- private
111
-
112
- def build_rate_request(origin, destination, line_items = [], options = {})
113
- line_items = [line_items] unless line_items.is_a?(Array)
114
- origin = origin.is_a?(Location) ? origin : Location.new(origin)
115
- destination = destination.is_a?(Location) ? destination : Location.new(destination)
116
-
117
- xml_request = XmlNode.new('eparcel') do |root_node|
118
- root_node << XmlNode.new('language', @options[:french] ? 'fr' : 'en')
119
- root_node << XmlNode.new('ratesAndServicesRequest') do |request|
120
-
121
- request << XmlNode.new('merchantCPCID', @options[:login])
122
- request << XmlNode.new('fromPostalCode', origin.postal_code)
123
- request << XmlNode.new('turnAroundTime', options[:turn_around_time]) if options[:turn_around_time]
124
- request << XmlNode.new('itemsPrice', dollar_amount(line_items.map(&:value).compact.sum))
125
-
126
- # line items
127
- request << build_line_items(line_items)
128
-
129
- # delivery info
130
- # NOTE: These tags MUST be after line items
131
- request << XmlNode.new('city', destination.city)
132
- request << XmlNode.new('provOrState', destination.province)
133
- request << XmlNode.new('country', handle_non_iso_country_names(destination.country))
134
- request << XmlNode.new('postalCode', destination.postal_code)
135
- end
136
- end
137
-
138
- DOCTYPE + xml_request.to_s
139
- end
140
-
141
- def parse_rate_response(response, origin, destination, options = {})
142
- xml = REXML::Document.new(response)
143
- success = response_success?(xml)
144
- message = response_message(xml)
145
-
146
- rate_estimates = []
147
- boxes = []
148
- if success
149
- xml.elements.each('eparcel/ratesAndServicesResponse/product') do |product|
150
- service_name = (@options[:french] ? @@name_french : @@name) + " " + product.get_text('name').to_s
151
- service_code = product.attribute('id').to_s
152
-
153
- rate_estimates << RateEstimate.new(origin, destination, @@name, service_name,
154
- :service_code => service_code,
155
- :total_price => product.get_text('rate').to_s,
156
- :currency => 'CAD',
157
- :shipping_date => product.get_text('shippingDate').to_s,
158
- :delivery_range => [product.get_text('deliveryDate').to_s] * 2
159
- )
160
- end
161
-
162
- boxes = xml.elements.collect('eparcel/ratesAndServicesResponse/packing/box') do |box|
163
- b = Box.new
164
- b.packedItems = []
165
- b.name = box.get_text('name').to_s
166
- b.weight = box.get_text('weight').to_s.to_f
167
- b.expediter_weight = box.get_text('expediterWeight').to_s.to_f
168
- b.length = box.get_text('length').to_s.to_f
169
- b.width = box.get_text('width').to_s.to_f
170
- b.height = box.get_text('height').to_s.to_f
171
- b.packedItems = box.elements.collect('packedItem') do |item|
172
- p = PackedItem.new
173
- p.quantity = item.get_text('quantity').to_s.to_i
174
- p.description = item.get_text('description').to_s
175
- p
176
- end
177
- b
178
- end
179
-
180
- postal_outlets = xml.elements.collect('eparcel/ratesAndServicesResponse/nearestPostalOutlet') do |outlet|
181
- postal_outlet = PostalOutlet.new
182
- postal_outlet.sequence_no = outlet.get_text('postalOutletSequenceNo').to_s
183
- postal_outlet.distance = outlet.get_text('distance').to_s
184
- postal_outlet.name = outlet.get_text('outletName').to_s
185
- postal_outlet.business_name = outlet.get_text('businessName').to_s
186
-
187
- postal_outlet.postal_address = Location.new(
188
- :address1 => outlet.get_text('postalAddress/addressLine').to_s,
189
- :postal_code => outlet.get_text('postalAddress/postal_code').to_s,
190
- :city => outlet.get_text('postalAddress/municipality').to_s,
191
- :province => outlet.get_text('postalAddress/province').to_s,
192
- :country => 'Canada',
193
- :phone_number => outlet.get_text('phoneNumber').to_s
194
- )
195
-
196
- postal_outlet.business_hours = outlet.elements.collect('businessHours') do |hour|
197
- { :day_of_week => hour.get_text('dayOfWeek').to_s, :time => hour.get_text('time').to_s }
198
- end
199
-
200
- postal_outlet
201
- end
202
- end
203
-
204
- CanadaPostRateResponse.new(success, message, Hash.from_xml(response), :rates => rate_estimates, :xml => response, :boxes => boxes, :postal_outlets => postal_outlets)
205
- end
206
-
207
- def response_success?(xml)
208
- return false unless xml.get_text('eparcel/error').nil?
209
-
210
- value = xml.get_text('eparcel/ratesAndServicesResponse/statusCode').to_s
211
- value == '1' || value == '2'
212
- end
213
-
214
- def response_message(xml)
215
- if response_success?(xml)
216
- xml.get_text('eparcel/ratesAndServicesResponse/statusMessage').to_s
217
- else
218
- xml.get_text('eparcel/error/statusMessage').to_s
219
- end
220
- end
221
-
222
- # <!-- List of items in the shopping -->
223
- # <!-- cart -->
224
- # <!-- Each item is defined by : -->
225
- # <!-- - quantity (mandatory) -->
226
- # <!-- - size (mandatory) -->
227
- # <!-- - weight (mandatory) -->
228
- # <!-- - description (mandatory) -->
229
- # <!-- - ready to ship (optional) -->
230
-
231
- def build_line_items(line_items)
232
- XmlNode.new('lineItems') do |line_items_node|
233
- line_items.each do |line_item|
234
- line_items_node << XmlNode.new('item') do |item|
235
- item << XmlNode.new('quantity', 1)
236
- item << XmlNode.new('weight', line_item.kilograms)
237
- item << XmlNode.new('length', line_item.cm(:length).to_s)
238
- item << XmlNode.new('width', line_item.cm(:width).to_s)
239
- item << XmlNode.new('height', line_item.cm(:height).to_s)
240
- item << XmlNode.new('description', line_item.options[:description] || ' ')
241
- item << XmlNode.new('readyToShip', line_item.options[:ready_to_ship] || nil)
242
- # By setting the 'readyToShip' tag to true, Sell Online will not pack this item in the boxes defined in the merchant profile.
243
- end
244
- end
245
- end
246
- end
247
-
248
- def dollar_amount(cents)
249
- "%0.2f" % (cents / 100.0)
250
- end
251
-
252
- def handle_non_iso_country_names(country)
253
- NON_ISO_COUNTRY_NAMES[country.to_s] || country
254
- end
255
- end
256
- end
257
- end
@@ -1,874 +0,0 @@
1
- require 'cgi'
2
-
3
- module ActiveMerchant
4
- module Shipping
5
- class CanadaPostPWS < Carrier
6
- @@name = "Canada Post PWS"
7
-
8
- SHIPPING_SERVICES = {
9
- "DOM.RP" => "Regular Parcel",
10
- "DOM.EP" => "Expedited Parcel",
11
- "DOM.XP" => "Xpresspost",
12
- "DOM.XP.CERT" => "Xpresspost Certified",
13
- "DOM.PC" => "Priority",
14
- "DOM.LIB" => "Library Books",
15
-
16
- "USA.EP" => "Expedited Parcel USA",
17
- "USA.PW.ENV" => "Priority Worldwide Envelope USA",
18
- "USA.PW.PAK" => "Priority Worldwide pak USA",
19
- "USA.PW.PARCEL" => "Priority Worldwide Parcel USA",
20
- "USA.SP.AIR" => "Small Packet USA Air",
21
- "USA.SP.SURF" => "Small Packet USA Surface",
22
- "USA.XP" => "Xpresspost USA",
23
-
24
- "INT.XP" => "Xpresspost International",
25
- "INT.IP.AIR" => "International Parcel Air",
26
- "INT.IP.SURF" => "International Parcel Surface",
27
- "INT.PW.ENV" => "Priority Worldwide Envelope Int'l",
28
- "INT.PW.PAK" => "Priority Worldwide pak Int'l",
29
- "INT.PW.PARCEL" => "Priority Worldwide parcel Int'l",
30
- "INT.SP.AIR" => "Small Packet International Air",
31
- "INT.SP.SURF" => "Small Packet International Surface"
32
- }
33
-
34
- ENDPOINT = "https://soa-gw.canadapost.ca/" # production
35
-
36
- SHIPMENT_MIMETYPE = "application/vnd.cpc.ncshipment+xml"
37
- RATE_MIMETYPE = "application/vnd.cpc.ship.rate+xml"
38
- TRACK_MIMETYPE = "application/vnd.cpc.track+xml"
39
- REGISTER_MIMETYPE = "application/vnd.cpc.registration+xml"
40
-
41
- LANGUAGE = {
42
- 'en' => 'en-CA',
43
- 'fr' => 'fr-CA'
44
- }
45
-
46
- SHIPPING_OPTIONS = [:d2po, :d2po_office_id, :cov, :cov_amount, :cod, :cod_amount, :cod_includes_shipping,
47
- :cod_method_of_payment, :so, :dc, :dns, :pa18, :pa19, :hfp, :lad,
48
- :rase, :rts, :aban]
49
-
50
- RATES_OPTIONS = [:cov, :cov_amount, :cod, :so, :dc, :dns, :pa18, :pa19, :hfp, :lad]
51
-
52
- MAX_WEIGHT = 30 # kg
53
-
54
- attr_accessor :language, :endpoint, :logger, :platform_id, :customer_number
55
-
56
- def initialize(options = {})
57
- @language = LANGUAGE[options[:language]] || LANGUAGE['en']
58
- @endpoint = options[:endpoint] || ENDPOINT
59
- @platform_id = options[:platform_id]
60
- @customer_number = options[:customer_number]
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
- if e.response
82
- error_response(e.response.body, CPPWSTrackingResponse)
83
- else
84
- CPPWSTrackingResponse.new(false, e.message, {}, :carrier => @@name)
85
- end
86
- rescue InvalidPinFormatError
87
- CPPWSTrackingResponse.new(false, "Invalid Pin Format", {}, :carrier => @@name)
88
- end
89
-
90
- # line_items should be a list of PackageItem's
91
- def create_shipment(origin, destination, package, line_items = [], options = {})
92
- request_body = build_shipment_request(origin, destination, package, line_items, options)
93
- response = ssl_post(create_shipment_url(options), request_body, headers(options, SHIPMENT_MIMETYPE, SHIPMENT_MIMETYPE))
94
- parse_shipment_response(response)
95
- rescue ActiveMerchant::ResponseError, ActiveMerchant::Shipping::ResponseError => e
96
- error_response(e.response.body, CPPWSShippingResponse)
97
- rescue MissingCustomerNumberError
98
- CPPWSShippingResponse.new(false, "Missing Customer Number", {}, :carrier => @@name)
99
- end
100
-
101
- def retrieve_shipment(shipping_id, options = {})
102
- response = ssl_post(shipment_url(shipping_id, options), nil, headers(options, SHIPMENT_MIMETYPE, SHIPMENT_MIMETYPE))
103
- parse_shipment_response(response)
104
- end
105
-
106
- def find_shipment_receipt(shipping_id, options = {})
107
- response = ssl_get(shipment_receipt_url(shipping_id, options), headers(options, SHIPMENT_MIMETYPE, SHIPMENT_MIMETYPE))
108
- parse_shipment_receipt_response(response)
109
- end
110
-
111
- def retrieve_shipping_label(shipping_response, options = {})
112
- raise MissingShippingNumberError unless shipping_response && shipping_response.shipping_id
113
- ssl_get(shipping_response.label_url, headers(options, "application/pdf"))
114
- end
115
-
116
- def register_merchant(options = {})
117
- url = endpoint + "ot/token"
118
- response = ssl_post(url, nil, headers({}, REGISTER_MIMETYPE, REGISTER_MIMETYPE).merge("Content-Length" => "0"))
119
- parse_register_token_response(response)
120
- rescue ActiveMerchant::ResponseError, ActiveMerchant::Shipping::ResponseError => e
121
- error_response(e.response.body, CPPWSRegisterResponse)
122
- end
123
-
124
- def retrieve_merchant_details(options = {})
125
- raise MissingTokenIdError unless token_id = options[:token_id]
126
- url = endpoint + "ot/token/#{token_id}"
127
- response = ssl_get(url, headers({}, REGISTER_MIMETYPE, REGISTER_MIMETYPE))
128
- parse_merchant_details_response(response)
129
- rescue ActiveMerchant::ResponseError, ActiveMerchant::Shipping::ResponseError => e
130
- error_response(e.response.body, CPPWSMerchantDetailsResponse)
131
- rescue Exception => e
132
- raise ResponseError.new(e.message)
133
- end
134
-
135
- def find_services(country = nil, options = {})
136
- response = ssl_get(services_url(country), headers(options, RATE_MIMETYPE))
137
- parse_services_response(response)
138
- rescue ActiveMerchant::ResponseError, ActiveMerchant::Shipping::ResponseError => e
139
- error_response(e.response.body, CPPWSRateResponse)
140
- end
141
-
142
- def find_service_options(service_code, country, options = {})
143
- response = ssl_get(services_url(country, service_code), headers(options, RATE_MIMETYPE))
144
- parse_service_options_response(response)
145
- rescue ActiveMerchant::ResponseError, ActiveMerchant::Shipping::ResponseError => e
146
- error_response(e.response.body, CPPWSRateResponse)
147
- end
148
-
149
- def find_option_details(option_code, options = {})
150
- url = endpoint + "rs/ship/option/#{option_code}"
151
- response = ssl_get(url, headers(options, RATE_MIMETYPE))
152
- parse_option_response(response)
153
- rescue ActiveMerchant::ResponseError, ActiveMerchant::Shipping::ResponseError => e
154
- error_response(e.response.body, CPPWSRateResponse)
155
- end
156
-
157
- def maximum_weight
158
- Mass.new(MAX_WEIGHT, :kilograms)
159
- end
160
-
161
- # service discovery
162
-
163
- def parse_services_response(response)
164
- doc = REXML::Document.new(REXML::Text::unnormalize(response))
165
- service_nodes = doc.elements['services'].elements.collect('service') { |node| node }
166
- service_nodes.inject({}) do |result, node|
167
- service_code = node.get_text("service-code").to_s
168
- service_name = node.get_text("service-name").to_s
169
- service_link = node.elements["link"].attributes['href']
170
- service_link_media_type = node.elements["link"].attributes['media-type']
171
- result[service_code] = {
172
- :name => service_name,
173
- :link => service_link,
174
- :link_media_type => service_link_media_type
175
- }
176
- result
177
- end
178
- end
179
-
180
- def parse_service_options_response(response)
181
- doc = REXML::Document.new(REXML::Text::unnormalize(response))
182
- service_node = doc.elements['service']
183
- service_code = service_node.get_text("service-code").to_s
184
- service_name = service_node.get_text("service-name").to_s
185
- options_node = service_node.elements['options']
186
- unless options_node.blank?
187
- option_nodes = options_node.elements.collect('option') { |node| node }
188
- options = option_nodes.inject([]) do |result, node|
189
- option = {
190
- :code => node.get_text("option-code").to_s,
191
- :name => node.get_text("option-name").to_s,
192
- :required => node.get_text("mandatory").to_s == "false" ? false : true,
193
- :qualifier_required => node.get_text("qualifier-required").to_s == "false" ? false : true
194
- }
195
- option[:qualifier_max] = node.get_text("qualifier-max").to_s.to_i if node.get_text("qualifier-max")
196
- result << option
197
- result
198
- end
199
- end
200
- restrictions_node = service_node.elements['restrictions']
201
- dimensions_node = restrictions_node.elements['dimensional-restrictions']
202
- restrictions = {
203
- :min_weight => restrictions_node.elements["weight-restriction"].attributes['min'].to_i,
204
- :max_weight => restrictions_node.elements["weight-restriction"].attributes['max'].to_i,
205
- :min_length => dimensions_node.elements["length"].attributes['min'].to_f,
206
- :max_length => dimensions_node.elements["length"].attributes['max'].to_f,
207
- :min_height => dimensions_node.elements["height"].attributes['min'].to_f,
208
- :max_height => dimensions_node.elements["height"].attributes['max'].to_f,
209
- :min_width => dimensions_node.elements["width"].attributes['min'].to_f,
210
- :max_width => dimensions_node.elements["width"].attributes['max'].to_f
211
- }
212
-
213
- {
214
- :service_code => service_code,
215
- :service_name => service_name,
216
- :options => options,
217
- :restrictions => restrictions
218
- }
219
- end
220
-
221
- def parse_option_response(response)
222
- doc = REXML::Document.new(REXML::Text::unnormalize(response))
223
- option_node = doc.elements['option']
224
- conflicts = option_node.elements['conflicting-options'].elements.collect('option-code') { |node| node.get_text.to_s } unless option_node.elements['conflicting-options'].blank?
225
- prereqs = option_node.elements['prerequisite-options'].elements.collect('option-code') { |node| node.get_text.to_s } unless option_node.elements['prerequisite-options'].blank?
226
- option = {
227
- :code => option_node.get_text('option-code').to_s,
228
- :name => option_node.get_text('option-name').to_s,
229
- :class => option_node.get_text('option-class').to_s,
230
- :prints_on_label => option_node.get_text('prints-on-label').to_s == "false" ? false : true,
231
- :qualifier_required => option_node.get_text('qualifier-required').to_s == "false" ? false : true
232
- }
233
- option[:conflicting_options] = conflicts if conflicts
234
- option[:prerequisite_options] = prereqs if prereqs
235
-
236
- option[:qualifier_max] = option_node.get_text("qualifier-max").to_s.to_i if option_node.get_text("qualifier-max")
237
- option
238
- end
239
-
240
- # rating
241
-
242
- def build_rates_request(origin, destination, line_items = [], options = {}, package = nil, services = [])
243
- line_items = Array(line_items)
244
- xml = XmlNode.new('mailing-scenario', :xmlns => "http://www.canadapost.ca/ws/ship/rate") do |node|
245
- node << customer_number_node(options)
246
- node << contract_id_node(options)
247
- node << quote_type_node(options)
248
- node << expected_mailing_date_node(shipping_date(options)) if options[:shipping_date]
249
- options_node = shipping_options_node(RATES_OPTIONS, options)
250
- node << options_node if options_node && !options_node.children.count.zero?
251
- node << parcel_node(line_items, package)
252
- node << origin_node(origin)
253
- node << destination_node(destination)
254
- node << services_node(services) unless services.blank?
255
- end
256
- xml.to_s
257
- end
258
-
259
- def parse_rates_response(response, origin, destination)
260
- doc = REXML::Document.new(REXML::Text::unnormalize(response))
261
- raise ActiveMerchant::Shipping::ResponseError, "No Quotes" unless doc.elements['price-quotes']
262
-
263
- quotes = doc.elements['price-quotes'].elements.collect('price-quote') { |node| node }
264
- rates = quotes.map do |node|
265
- service_name = node.get_text("service-name").to_s
266
- service_code = node.get_text("service-code").to_s
267
- total_price = node.elements['price-details'].get_text("due").to_s
268
- expected_date = expected_date_from_node(node)
269
- options = {
270
- :service_code => service_code,
271
- :total_price => total_price,
272
- :currency => 'CAD',
273
- :delivery_range => [expected_date, expected_date]
274
- }
275
- RateEstimate.new(origin, destination, @@name, service_name, options)
276
- end
277
- CPPWSRateResponse.new(true, "", {}, :rates => rates)
278
- end
279
-
280
- # tracking
281
-
282
- def parse_tracking_response(response)
283
- doc = REXML::Document.new(REXML::Text::unnormalize(response))
284
- raise ActiveMerchant::Shipping::ResponseError, "No Tracking" unless root_node = doc.elements['tracking-detail']
285
-
286
- events = root_node.elements['significant-events'].elements.collect('occurrence') { |node| node }
287
-
288
- shipment_events = build_tracking_events(events)
289
- change_date = root_node.get_text('changed-expected-date').to_s
290
- expected_date = root_node.get_text('expected-delivery-date').to_s
291
- dest_postal_code = root_node.get_text('destination-postal-id').to_s
292
- destination = Location.new(:postal_code => dest_postal_code)
293
- origin = Location.new(origin_hash_for(root_node))
294
- options = {
295
- :carrier => @@name,
296
- :service_name => root_node.get_text('service-name').to_s,
297
- :expected_date => expected_date.blank? ? nil : Date.parse(expected_date),
298
- :changed_date => change_date.blank? ? nil : Date.parse(change_date),
299
- :change_reason => root_node.get_text('changed-expected-delivery-reason').to_s.strip,
300
- :destination_postal_code => root_node.get_text('destination-postal-id').to_s,
301
- :shipment_events => shipment_events,
302
- :tracking_number => root_node.get_text('pin').to_s,
303
- :origin => origin,
304
- :destination => destination,
305
- :customer_number => root_node.get_text('mailed-by-customer-number').to_s
306
- }
307
-
308
- CPPWSTrackingResponse.new(true, "", {}, options)
309
- end
310
-
311
- def build_tracking_events(events)
312
- events.map do |event|
313
- date = event.get_text('event-date').to_s
314
- time = event.get_text('event-time').to_s
315
- zone = event.get_text('event-time-zone').to_s
316
- timestamp = DateTime.parse("#{date} #{time} #{zone}")
317
- time = Time.utc(timestamp.utc.year, timestamp.utc.month, timestamp.utc.day, timestamp.utc.hour, timestamp.utc.min, timestamp.utc.sec)
318
- message = event.get_text('event-description').to_s
319
- location = [event.get_text('event-retail-name'), event.get_text('event-site'), event.get_text('event-province')].compact.join(", ")
320
- name = event.get_text('event-identifier').to_s
321
- ShipmentEvent.new(name, time, location, message)
322
- end
323
- end
324
-
325
- # shipping
326
-
327
- # options
328
- # :service => 'DOM.EP'
329
- # :notification_email
330
- # :packing_instructions
331
- # :show_postage_rate
332
- # :cod, :cod_amount, :insurance, :insurance_amount, :signature_required, :pa18, :pa19, :hfp, :dns, :lad
333
- #
334
- def build_shipment_request(origin, destination, package, line_items = [], options = {})
335
- origin = sanitize_location(origin)
336
- destination = sanitize_location(destination)
337
-
338
- xml = XmlNode.new('non-contract-shipment', :xmlns => "http://www.canadapost.ca/ws/ncshipment") do |root_node|
339
- root_node << XmlNode.new('delivery-spec') do |node|
340
- node << shipment_service_code_node(options)
341
- node << shipment_sender_node(origin, options)
342
- node << shipment_destination_node(destination, options)
343
- options_node = shipment_options_node(options)
344
- node << shipment_options_node(options) if options_node && !options_node.children.count.zero?
345
- node << shipment_parcel_node(package)
346
- node << shipment_notification_node(options)
347
- node << shipment_preferences_node(options)
348
- node << references_node(options) # optional > user defined custom notes
349
- node << shipment_customs_node(destination, line_items, options)
350
- # COD Remittance defaults to sender
351
- end
352
- end
353
- xml.to_s
354
- end
355
-
356
- def shipment_service_code_node(options)
357
- XmlNode.new('service-code', options[:service])
358
- end
359
-
360
- def shipment_sender_node(location, options)
361
- XmlNode.new('sender') do |node|
362
- node << XmlNode.new('name', location.name)
363
- node << XmlNode.new('company', location.company) if location.company.present?
364
- node << XmlNode.new('contact-phone', location.phone)
365
- node << XmlNode.new('address-details') do |innernode|
366
- innernode << XmlNode.new('address-line-1', location.address1)
367
- innernode << XmlNode.new('address-line-2', location.address2_and_3) unless location.address2_and_3.blank?
368
- innernode << XmlNode.new('city', location.city)
369
- innernode << XmlNode.new('prov-state', location.province)
370
- # innernode << XmlNode.new('country-code', location.country_code)
371
- innernode << XmlNode.new('postal-zip-code', location.postal_code)
372
- end
373
- end
374
- end
375
-
376
- def shipment_destination_node(location, options)
377
- XmlNode.new('destination') do |node|
378
- node << XmlNode.new('name', location.name)
379
- node << XmlNode.new('company', location.company) if location.company.present?
380
- node << XmlNode.new('client-voice-number', location.phone)
381
- node << XmlNode.new('address-details') do |innernode|
382
- innernode << XmlNode.new('address-line-1', location.address1)
383
- innernode << XmlNode.new('address-line-2', location.address2_and_3) unless location.address2_and_3.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
- def parse_shipment_response(response)
467
- doc = REXML::Document.new(REXML::Text::unnormalize(response))
468
- raise ActiveMerchant::Shipping::ResponseError, "No Shipping" unless root_node = doc.elements['non-contract-shipment-info']
469
- options = {
470
- :shipping_id => root_node.get_text('shipment-id').to_s,
471
- :tracking_number => root_node.get_text('tracking-pin').to_s,
472
- :details_url => root_node.elements["links/link[@rel='details']"].attributes['href'],
473
- :label_url => root_node.elements["links/link[@rel='label']"].attributes['href'],
474
- :receipt_url => root_node.elements["links/link[@rel='receipt']"].attributes['href']
475
- }
476
- CPPWSShippingResponse.new(true, "", {}, options)
477
- end
478
-
479
- def parse_register_token_response(response)
480
- doc = REXML::Document.new(REXML::Text::unnormalize(response))
481
- raise ActiveMerchant::Shipping::ResponseError, "No Registration Token" unless root_node = doc.elements['token']
482
- options = {
483
- :token_id => root_node.get_text('token-id').to_s
484
- }
485
- CPPWSRegisterResponse.new(true, "", {}, options)
486
- end
487
-
488
- def parse_merchant_details_response(response)
489
- doc = REXML::Document.new(REXML::Text::unnormalize(response))
490
- raise "No Merchant Info" unless root_node = doc.elements['merchant-info']
491
- raise "No Merchant Info" if root_node.get_text('customer-number').blank?
492
- options = {
493
- :customer_number => root_node.get_text('customer-number').to_s,
494
- :contract_number => root_node.get_text('contract-number').to_s,
495
- :username => root_node.get_text('merchant-username').to_s,
496
- :password => root_node.get_text('merchant-password').to_s,
497
- :has_default_credit_card => root_node.get_text('has-default-credit-card') == 'true'
498
- }
499
- CPPWSMerchantDetailsResponse.new(true, "", {}, options)
500
- end
501
-
502
- def parse_shipment_receipt_response(response)
503
- doc = REXML::Document.new(REXML::Text::unnormalize(response))
504
- root = doc.elements['non-contract-shipment-receipt']
505
- cc_details_node = root.elements['cc-receipt-details']
506
- service_standard_node = root.elements['service-standard']
507
- receipt = {
508
- :final_shipping_point => root.get_text("final-shipping-point").to_s,
509
- :shipping_point_name => root.get_text("shipping-point-name").to_s,
510
- :service_code => root.get_text("service-code").to_s,
511
- :rated_weight => root.get_text("rated-weight").to_s.to_f,
512
- :base_amount => root.get_text("base-amount").to_s.to_f,
513
- :pre_tax_amount => root.get_text("pre-tax-amount").to_s.to_f,
514
- :gst_amount => root.get_text("gst-amount").to_s.to_f,
515
- :pst_amount => root.get_text("pst-amount").to_s.to_f,
516
- :hst_amount => root.get_text("hst-amount").to_s.to_f,
517
- :charge_amount => cc_details_node.get_text("charge-amount").to_s.to_f,
518
- :currency => cc_details_node.get_text("currency").to_s,
519
- :expected_transit_days => service_standard_node.get_text("expected-transit-time").to_s.to_i,
520
- :expected_delivery_date => service_standard_node.get_text("expected-delivery-date").to_s
521
- }
522
- option_nodes = root.elements['priced-options'].elements.collect('priced-option') { |node| node } unless root.elements['priced-options'].blank?
523
-
524
- receipt[:priced_options] = if option_nodes
525
- option_nodes.inject({}) do |result, node|
526
- result[node.get_text("option-code").to_s] = node.get_text("option-price").to_s.to_f
527
- result
528
- end
529
- else
530
- []
531
- end
532
-
533
- receipt
534
- end
535
-
536
- def error_response(response, response_klass)
537
- doc = REXML::Document.new(REXML::Text::unnormalize(response))
538
- messages = doc.elements['messages'].elements.collect('message') { |node| node }
539
- message = messages.map { |m| m.get_text('description').to_s }.join(", ")
540
- code = messages.map { |m| m.get_text('code').to_s }.join(", ")
541
- response_klass.new(false, message, {}, :carrier => @@name, :code => code)
542
- end
543
-
544
- def log(msg)
545
- logger.debug(msg) if logger
546
- end
547
-
548
- private
549
-
550
- def tracking_url(pin)
551
- case pin.length
552
- when 12, 13, 16
553
- endpoint + "vis/track/pin/%s/detail" % pin
554
- when 15
555
- endpoint + "vis/track/dnc/%s/detail" % pin
556
- else
557
- raise InvalidPinFormatError
558
- end
559
- end
560
-
561
- def create_shipment_url(options)
562
- raise MissingCustomerNumberError unless customer_number = options[:customer_number]
563
- if @platform_id.present?
564
- endpoint + "rs/#{customer_number}-#{@platform_id}/ncshipment"
565
- else
566
- endpoint + "rs/#{customer_number}/ncshipment"
567
- end
568
- end
569
-
570
- def shipment_url(shipping_id, options = {})
571
- raise MissingCustomerNumberError unless customer_number = options[:customer_number]
572
- if @platform_id.present?
573
- endpoint + "rs/#{customer_number}-#{@platform_id}/ncshipment/#{shipping_id}"
574
- else
575
- endpoint + "rs/#{customer_number}/ncshipment/#{shipping_id}"
576
- end
577
- end
578
-
579
- def shipment_receipt_url(shipping_id, options = {})
580
- raise MissingCustomerNumberError unless customer_number = options[:customer_number]
581
- if @platform_id.present?
582
- endpoint + "rs/#{customer_number}-#{@platform_id}/ncshipment/#{shipping_id}/receipt"
583
- else
584
- endpoint + "rs/#{customer_number}/ncshipment/#{shipping_id}/receipt"
585
- end
586
- end
587
-
588
- def services_url(country = nil, service_code = nil)
589
- url = endpoint + "rs/ship/service"
590
- url += "/#{service_code}" if service_code
591
- url += "?country=#{country}" if country
592
- url
593
- end
594
-
595
- def customer_credentials_valid?(credentials)
596
- (credentials.keys & [:customer_api_key, :customer_secret]).any?
597
- end
598
-
599
- def encoded_authorization(customer_credentials = {})
600
- if customer_credentials_valid?(customer_credentials)
601
- "Basic %s" % Base64.encode64("#{customer_credentials[:customer_api_key]}:#{customer_credentials[:customer_secret]}")
602
- else
603
- "Basic %s" % Base64.encode64("#{@options[:api_key]}:#{@options[:secret]}")
604
- end
605
- end
606
-
607
- def headers(customer_credentials, accept = nil, content_type = nil)
608
- headers = {
609
- 'Authorization' => encoded_authorization(customer_credentials),
610
- 'Accept-Language' => language
611
- }
612
- headers['Accept'] = accept if accept
613
- headers['Content-Type'] = content_type if content_type
614
- headers['Platform-ID'] = platform_id if platform_id && customer_credentials_valid?(customer_credentials)
615
- headers
616
- end
617
-
618
- def customer_number_node(options)
619
- XmlNode.new("customer-number", options[:customer_number] || customer_number)
620
- end
621
-
622
- def contract_id_node(options)
623
- XmlNode.new("contract-id", options[:contract_id]) if options[:contract_id]
624
- end
625
-
626
- def quote_type_node(options)
627
- XmlNode.new("quote-type", 'commercial')
628
- end
629
-
630
- def expected_mailing_date_node(date_as_string)
631
- XmlNode.new("expected-mailing-date", date_as_string)
632
- end
633
-
634
- def parcel_node(line_items, package = nil, options = {})
635
- weight = sanitize_weight_kg(package && !package.kilograms.zero? ? package.kilograms.to_f : line_items.sum(&:kilograms).to_f)
636
- XmlNode.new('parcel-characteristics') do |el|
637
- el << XmlNode.new('weight', "%#2.3f" % weight)
638
- if package
639
- pkg_dim = package.cm
640
- if pkg_dim && !pkg_dim.select { |x| x != 0 }.empty?
641
- el << XmlNode.new('dimensions') do |dim|
642
- dim << XmlNode.new('length', '%.1f' % ((pkg_dim[2] * 10).round / 10.0)) if pkg_dim.size >= 3
643
- dim << XmlNode.new('width', '%.1f' % ((pkg_dim[1] * 10).round / 10.0)) if pkg_dim.size >= 2
644
- dim << XmlNode.new('height', '%.1f' % ((pkg_dim[0] * 10).round / 10.0)) if pkg_dim.size >= 1
645
- end
646
- end
647
- end
648
- el << XmlNode.new('mailing-tube', line_items.any?(&:tube?))
649
- el << XmlNode.new('oversized', true) if line_items.any?(&:oversized?)
650
- el << XmlNode.new('unpackaged', line_items.any?(&:unpackaged?))
651
- end
652
- end
653
-
654
- def origin_node(location)
655
- origin = sanitize_location(location)
656
- XmlNode.new("origin-postal-code", origin.zip)
657
- end
658
-
659
- def destination_node(location)
660
- destination = sanitize_location(location)
661
- case destination.country_code
662
- when 'CA'
663
- XmlNode.new('destination') do |node|
664
- node << XmlNode.new('domestic') do |x|
665
- x << XmlNode.new('postal-code', destination.postal_code)
666
- end
667
- end
668
-
669
- when 'US'
670
- XmlNode.new('destination') do |node|
671
- node << XmlNode.new('united-states') do |x|
672
- x << XmlNode.new('zip-code', destination.postal_code)
673
- end
674
- end
675
-
676
- else
677
- XmlNode.new('destination') do |dest|
678
- dest << XmlNode.new('international') do |dom|
679
- dom << XmlNode.new('country-code', destination.country_code)
680
- end
681
- end
682
- end
683
- end
684
-
685
- def services_node(services)
686
- XmlNode.new('services') do |node|
687
- services.each { |code| node << XmlNode.new('service-code', code) }
688
- end
689
- end
690
-
691
- def shipping_options_node(available_options, options = {})
692
- return if (options.symbolize_keys.keys & available_options).empty?
693
- XmlNode.new('options') do |el|
694
-
695
- if options[:cod] && options[:cod_amount]
696
- el << XmlNode.new('option') do |opt|
697
- opt << XmlNode.new('option-code', 'COD')
698
- opt << XmlNode.new('option-amount', options[:cod_amount])
699
- opt << XmlNode.new('option-qualifier-1', options[:cod_includes_shipping]) unless options[:cod_includes_shipping].blank?
700
- opt << XmlNode.new('option-qualifier-2', options[:cod_method_of_payment]) unless options[:cod_method_of_payment].blank?
701
- end
702
- end
703
-
704
- if options[:cov]
705
- el << XmlNode.new('option') do |opt|
706
- opt << XmlNode.new('option-code', 'COV')
707
- opt << XmlNode.new('option-amount', options[:cov_amount]) unless options[:cov_amount].blank?
708
- end
709
- end
710
-
711
- if options[:d2po]
712
- el << XmlNode.new('option') do |opt|
713
- opt << XmlNode.new('option-code', 'D2PO')
714
- opt << XmlNode.new('option-qualifier-2'. options[:d2po_office_id]) unless options[:d2po_office_id].blank?
715
- end
716
- end
717
-
718
- [:so, :dc, :pa18, :pa19, :hfp, :dns, :lad, :rase, :rts, :aban].each do |code|
719
- if options[code]
720
- el << XmlNode.new('option') do |opt|
721
- opt << XmlNode.new('option-code', code.to_s.upcase)
722
- end
723
- end
724
- end
725
- end
726
- end
727
-
728
- def expected_date_from_node(node)
729
- if service = node.elements['service-standard']
730
- expected_date = service.get_text("expected-delivery-date").to_s
731
- else
732
- expected_date = nil
733
- end
734
- expected_date
735
- end
736
-
737
- def shipping_date(options)
738
- DateTime.strptime((options[:shipping_date] || Time.now).to_s, "%Y-%m-%d")
739
- end
740
-
741
- def sanitize_location(location)
742
- location_hash = location.is_a?(Location) ? location.to_hash : location
743
- location_hash = sanitize_zip(location_hash)
744
- Location.new(location_hash)
745
- end
746
-
747
- def sanitize_zip(hash)
748
- [:postal_code, :zip].each do |attr|
749
- hash[attr].gsub!(/\s+/, '') if hash[attr]
750
- end
751
- hash
752
- end
753
-
754
- def sanitize_weight_kg(kg)
755
- kg == 0 ? 0.001 : kg
756
- end
757
-
758
- def sanitize_price_from_cents(value)
759
- value == 0 ? 0.01 : value.round / 100.0
760
- end
761
-
762
- def origin_hash_for(root_node)
763
- occurrences = root_node.get_elements('significant-events').first.get_elements('occurrence')
764
- earliest = occurrences.sort_by { |occurrence| time_of_occurrence occurrence }.first
765
-
766
- {
767
- city: earliest.get_text('event-site').to_s,
768
- province: earliest.get_text('event-province').to_s,
769
- address_1: earliest.get_text('event-retail-location-id').to_s,
770
- country: 'Canada'
771
- }
772
- end
773
-
774
- def time_of_occurrence(occurrence)
775
- time = occurrence.get_text('event_time')
776
- date = occurrence.get_text('event-date')
777
- time_zone = occurrence.get_text('event-date')
778
- DateTime.parse "#{date} #{time} #{time_zone}"
779
- end
780
- end
781
-
782
- module CPPWSErrorResponse
783
- attr_accessor :error_code
784
- def handle_error(message, options)
785
- @error_code = options[:code]
786
- end
787
- end
788
-
789
- class CPPWSRateResponse < RateResponse
790
- include CPPWSErrorResponse
791
-
792
- def initialize(success, message, params = {}, options = {})
793
- handle_error(message, options)
794
- super
795
- end
796
- end
797
-
798
- class CPPWSTrackingResponse < TrackingResponse
799
- DELIVERED_EVENT_CODES = %w(1496 1498 1499 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438)
800
- include CPPWSErrorResponse
801
-
802
- attr_reader :service_name, :expected_date, :changed_date, :change_reason, :customer_number
803
-
804
- def initialize(success, message, params = {}, options = {})
805
- handle_error(message, options)
806
- super
807
- @service_name = options[:service_name]
808
- @expected_date = options[:expected_date]
809
- @changed_date = options[:changed_date]
810
- @change_reason = options[:change_reason]
811
- @customer_number = options[:customer_number]
812
- end
813
-
814
- def delivered?
815
- !delivered_event.nil?
816
- end
817
-
818
- def actual_delivery_time
819
- delivered_event.time if delivered?
820
- end
821
-
822
- private
823
-
824
- def delivered_event
825
- @delivered_event ||= @shipment_events.detect { |event| DELIVERED_EVENT_CODES.include? event.name }
826
- end
827
- end
828
-
829
- class CPPWSShippingResponse < ShippingResponse
830
- include CPPWSErrorResponse
831
- attr_reader :label_url, :details_url, :receipt_url
832
- def initialize(success, message, params = {}, options = {})
833
- handle_error(message, options)
834
- super
835
- @label_url = options[:label_url]
836
- @details_url = options[:details_url]
837
- @receipt_url = options[:receipt_url]
838
- end
839
- end
840
-
841
- class CPPWSRegisterResponse < Response
842
- include CPPWSErrorResponse
843
- attr_reader :token_id
844
- def initialize(success, message, params = {}, options = {})
845
- handle_error(message, options)
846
- super
847
- @token_id = options[:token_id]
848
- end
849
-
850
- def redirect_url(customer_id, return_url)
851
- "http://www.canadapost.ca/cpotools/apps/drc/merchant?return-url=#{CGI::escape(return_url)}&token-id=#{token_id}&platform-id=#{customer_id}"
852
- end
853
- end
854
-
855
- class CPPWSMerchantDetailsResponse < Response
856
- include CPPWSErrorResponse
857
- attr_reader :customer_number, :contract_number, :username, :password, :has_default_credit_card
858
- def initialize(success, message, params = {}, options = {})
859
- handle_error(message, options)
860
- super
861
- @customer_number = options[:customer_number]
862
- @contract_number = options[:contract_number]
863
- @username = options[:username]
864
- @password = options[:password]
865
- @has_default_credit_card = options[:has_default_credit_card]
866
- end
867
- end
868
-
869
- class InvalidPinFormatError < StandardError; end
870
- class MissingCustomerNumberError < StandardError; end
871
- class MissingShippingNumberError < StandardError; end
872
- class MissingTokenIdError < StandardError; end
873
- end
874
- end