active_shipping 1.0.0.pre1 → 1.0.0.pre2

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,3 @@
1
- require 'builder'
2
-
3
1
  module ActiveShipping
4
2
  class Kunaki < Carrier
5
3
  self.retry_safe = true
@@ -77,31 +75,30 @@ module ActiveShipping
77
75
  private
78
76
 
79
77
  def build_request(destination, options)
80
- xml = Builder::XmlMarkup.new
81
- xml.instruct!
82
- xml.tag! 'ShippingOptions' do
83
- xml.tag! 'AddressInfo' do
84
- xml.tag! 'Country', COUNTRIES[destination.country_code]
85
-
86
- state = %w(US CA).include?(destination.country_code.to_s) ? destination.state : ''
87
-
88
- xml.tag! 'State_Province', state
89
- xml.tag! 'PostalCode', destination.zip
90
- end
78
+ country = COUNTRIES[destination.country_code]
79
+ state_province = %w(US CA).include?(destination.country_code.to_s) ? destination.state : ''
80
+
81
+ builder = Nokogiri::XML::Builder.new do |xml|
82
+ xml.ShippingOptions do
83
+ xml.AddressInfo do
84
+ xml.Country(country)
85
+ xml.State_Province(state_province)
86
+ xml.PostalCode(destination.zip)
87
+ end
91
88
 
92
- options[:items].each do |item|
93
- xml.tag! 'Product' do
94
- xml.tag! 'ProductId', item[:sku]
95
- xml.tag! 'Quantity', item[:quantity]
89
+ options[:items].each do |item|
90
+ xml.Product do
91
+ xml.ProductId(item[:sku])
92
+ xml.Quantity(item[:quantity])
93
+ end
96
94
  end
97
95
  end
98
96
  end
99
- xml.target!
97
+ builder.to_xml
100
98
  end
101
99
 
102
100
  def commit(origin, destination, options)
103
101
  request = build_request(destination, options)
104
-
105
102
  response = parse( ssl_post(URL, request, "Content-Type" => "text/xml") )
106
103
 
107
104
  RateResponse.new(success?(response), message_from(response), response,
@@ -126,15 +123,15 @@ module ActiveShipping
126
123
  response = {}
127
124
  response["Options"] = []
128
125
 
129
- document = REXML::Document.new(sanitize(xml))
126
+ document = Nokogiri.XML(sanitize(xml))
130
127
 
131
- response["ErrorCode"] = parse_child_text(document.root, "ErrorCode")
132
- response["ErrorText"] = parse_child_text(document.root, "ErrorText")
128
+ response["ErrorCode"] = document.at('/Response/ErrorCode').text
129
+ response["ErrorText"] = document.at('/Response/ErrorText').text
133
130
 
134
- document.root.elements.each("Option") do |e|
131
+ document.xpath("Response/Option").each do |node|
135
132
  rate = {}
136
- rate["Description"] = parse_child_text(e, "Description")
137
- rate["Price"] = parse_child_text(e, "Price")
133
+ rate["Description"] = node.at("Description").text
134
+ rate["Price"] = node.at("Price").text
138
135
  response["Options"] << rate
139
136
  end
140
137
  response
@@ -147,12 +144,6 @@ module ActiveShipping
147
144
  result
148
145
  end
149
146
 
150
- def parse_child_text(parent, name)
151
- if element = parent.elements[name]
152
- element.text
153
- end
154
- end
155
-
156
147
  def success?(response)
157
148
  response["ErrorCode"] == "0"
158
149
  end
@@ -179,66 +179,68 @@ module ActiveShipping
179
179
  end
180
180
 
181
181
  def build_access_request
182
- xml_request = XmlNode.new('AccessRequest') do |access_request|
183
- access_request << XmlNode.new('AccessLicenseNumber', @options[:key])
184
- access_request << XmlNode.new('UserId', @options[:login])
185
- access_request << XmlNode.new('Password', @options[:password])
182
+ xml_builder = Nokogiri::XML::Builder.new do |xml|
183
+ xml.AccessRequest do
184
+ xml.AccessLicenseNumber(@options[:key])
185
+ xml.UserId(@options[:password])
186
+ xml.Password(@options[:password])
187
+ end
186
188
  end
187
- xml_request.to_s
189
+ xml_builder.to_xml
188
190
  end
189
191
 
190
192
  def build_rate_request(origin, destination, packages, options = {})
191
- packages = Array(packages)
192
- xml_request = XmlNode.new('RatingServiceSelectionRequest') do |root_node|
193
- root_node << XmlNode.new('Request') do |request|
194
- request << XmlNode.new('RequestAction', 'Rate')
195
- request << XmlNode.new('RequestOption', 'Shop')
196
- # not implemented: 'Rate' RequestOption to specify a single service query
197
- # request << XmlNode.new('RequestOption', ((options[:service].nil? or options[:service] == :all) ? 'Shop' : 'Rate'))
198
- end
199
-
200
- pickup_type = options[:pickup_type] || :daily_pickup
193
+ xml_builder = Nokogiri::XML::Builder.new do |xml|
194
+ xml.RatingServiceSelectionRequest do
195
+ xml.Request do
196
+ xml.RequestAction('Rate')
197
+ xml.RequestOption('Shop')
198
+ # not implemented: 'Rate' RequestOption to specify a single service query
199
+ # xml.RequestOption((options[:service].nil? or options[:service] == :all) ? 'Shop' : 'Rate')
200
+ end
201
201
 
202
- root_node << XmlNode.new('PickupType') do |pickup_type_node|
203
- pickup_type_node << XmlNode.new('Code', PICKUP_CODES[pickup_type])
204
- # not implemented: PickupType/PickupDetails element
205
- end
206
- cc = options[:customer_classification] || DEFAULT_CUSTOMER_CLASSIFICATIONS[pickup_type]
207
- root_node << XmlNode.new('CustomerClassification') do |cc_node|
208
- cc_node << XmlNode.new('Code', CUSTOMER_CLASSIFICATIONS[cc])
209
- end
202
+ pickup_type = options[:pickup_type] || :daily_pickup
210
203
 
211
- root_node << XmlNode.new('Shipment') do |shipment|
212
- # not implemented: Shipment/Description element
213
- shipment << build_location_node('Shipper', (options[:shipper] || origin), options)
214
- shipment << build_location_node('ShipTo', destination, options)
215
- if options[:shipper] and options[:shipper] != origin
216
- shipment << build_location_node('ShipFrom', origin, options)
204
+ xml.PickupType do
205
+ xml.Code(PICKUP_CODES[pickup_type])
206
+ # not implemented: PickupType/PickupDetails element
217
207
  end
218
208
 
219
- # not implemented: * Shipment/ShipmentWeight element
220
- # * Shipment/ReferenceNumber element
221
- # * Shipment/Service element
222
- # * Shipment/PickupDate element
223
- # * Shipment/ScheduledDeliveryDate element
224
- # * Shipment/ScheduledDeliveryTime element
225
- # * Shipment/AlternateDeliveryTime element
226
- # * Shipment/DocumentsOnly element
227
-
228
- packages.each do |package|
229
- options[:imperial] ||= IMPERIAL_COUNTRIES.include?(origin.country_code(:alpha2))
230
- shipment << build_package_node(package, options)
209
+ cc = options[:customer_classification] || DEFAULT_CUSTOMER_CLASSIFICATIONS[pickup_type]
210
+ xml.CustomerClassification do
211
+ xml.Code(CUSTOMER_CLASSIFICATIONS[cc])
231
212
  end
232
213
 
233
- # not implemented: * Shipment/ShipmentServiceOptions element
234
- if options[:origin_account]
235
- shipment << XmlNode.new("RateInformation") do |rate_info_node|
236
- rate_info_node << XmlNode.new("NegotiatedRatesIndicator")
214
+ xml.Shipment do
215
+ # not implemented: Shipment/Description element
216
+ build_location_node(xml, 'Shipper', (options[:shipper] || origin), options)
217
+ build_location_node(xml, 'ShipTo', destination, options)
218
+ build_location_node(xml, 'ShipFrom', origin, options) if options[:shipper] && options[:shipper] != origin
219
+
220
+ # not implemented: * Shipment/ShipmentWeight element
221
+ # * Shipment/ReferenceNumber element
222
+ # * Shipment/Service element
223
+ # * Shipment/PickupDate element
224
+ # * Shipment/ScheduledDeliveryDate element
225
+ # * Shipment/ScheduledDeliveryTime element
226
+ # * Shipment/AlternateDeliveryTime element
227
+ # * Shipment/DocumentsOnly element
228
+
229
+ Array(packages).each do |package|
230
+ options[:imperial] ||= IMPERIAL_COUNTRIES.include?(origin.country_code(:alpha2))
231
+ build_package_node(xml, package, options)
232
+ end
233
+
234
+ # not implemented: * Shipment/ShipmentServiceOptions element
235
+ if options[:origin_account]
236
+ xml.RateInformation do
237
+ xml.NegotiatedRatesIndicator
238
+ end
237
239
  end
238
240
  end
239
241
  end
240
242
  end
241
- xml_request.to_s
243
+ xml_builder.to_xml
242
244
  end
243
245
 
244
246
  # Build XML node to request a shipping label for the given packages.
@@ -257,222 +259,239 @@ module ActiveShipping
257
259
  def build_shipment_request(origin, destination, packages, options = {})
258
260
  # There are a lot of unimplemented elements, documenting all of them
259
261
  # wouldprobably be unhelpful.
260
-
261
- xml_request = XmlNode.new('ShipmentConfirmRequest') do |root_node|
262
- root_node << XmlNode.new('Request') do |request|
263
- # Required element and the text must be "ShipConfirm"
264
- request << XmlNode.new('RequestAction', 'ShipConfirm')
265
- # Required element cotnrols level of address validation.
266
- request << XmlNode.new('RequestOption', options[:optional_processing] || 'validate')
267
- # Optional element to identify transactions between client and server.
268
- if options[:customer_context]
269
- request << XmlNode.new('TransactionReference') do |refer|
270
- refer << XmlNode.new('CustomerContext', options[:customer_context])
262
+ xml_builder = Nokogiri::XML::Builder.new do |xml|
263
+ xml.ShipmentConfirmRequest do
264
+ xml.Request do
265
+ # Required element and the text must be "ShipConfirm"
266
+ xml.RequestAction('ShipConfirm')
267
+ # Required element cotnrols level of address validation.
268
+ xml.RequestOption(options[:optional_processing] || 'validate')
269
+ # Optional element to identify transactions between client and server.
270
+ if options[:customer_context]
271
+ xml.TransactionReference do
272
+ xml.CustomerContext(options[:customer_context])
273
+ end
271
274
  end
272
275
  end
273
- end
274
- root_node << XmlNode.new('Shipment') do |shipment|
275
- # Required element.
276
- shipment << XmlNode.new('Service') do |service|
277
- service << XmlNode.new('Code', options[:service_code] || '14')
278
- service << XmlNode.new('Description', options[:service_description] || 'Next Day Air Early AM')
279
- end
280
- # Required element. The delivery destination.
281
- shipment << build_location_node('ShipTo', destination, {})
282
- # Required element. The company whose account is responsible for the label(s).
283
- shipment << build_location_node('Shipper', options[:shipper] || origin, {})
284
- # Required if pickup is different different from shipper's address.
285
- if options[:ship_from]
286
- shipment << build_location_node('ShipFrom', options[:ship_from], {})
287
- end
288
- # Optional.
289
- if options[:saturday_delivery]
290
- shipment << XmlNode.new('ShipmentServiceOptions') do |opts|
291
- opts << XmlNode.new('SaturdayDelivery')
276
+
277
+ xml.Shipment do
278
+ # Required element.
279
+ xml.Service do
280
+ xml.Code(options[:service_code] || '14')
281
+ xml.Description(options[:service_description] || 'Next Day Air Early AM')
292
282
  end
293
- end
294
- # Optional.
295
- if options[:origin_account]
296
- shipment << XmlNode.new('RateInformation') do |rate|
297
- rate << XmlNode.new('NegotiatedRatesIndicator')
283
+
284
+ # Required element. The delivery destination.
285
+ build_location_node(xml, 'ShipTo', destination, {})
286
+ # Required element. The company whose account is responsible for the label(s).
287
+ build_location_node(xml, 'Shipper', options[:shipper] || origin, {})
288
+ # Required if pickup is different different from shipper's address.
289
+ build_location_node(xml, 'ShipFrom', options[:ship_from], {}) if options[:ship_from]
290
+
291
+ # Optional.
292
+ if options[:saturday_delivery]
293
+ xml.ShipmentServiceOptions do
294
+ xml.SaturdayDelivery
295
+ end
298
296
  end
299
- end
300
- # Optional.
301
- if options[:shipment] && options[:shipment][:reference_number]
302
- shipment << XmlNode.new("ReferenceNumber") do |ref_node|
303
- ref_node << XmlNode.new("Code", options[:shipment][:reference_number][:code] || "")
304
- ref_node << XmlNode.new("Value", options[:shipment][:reference_number][:value])
297
+
298
+ # Optional.
299
+ if options[:origin_account]
300
+ xml.RateInformation do
301
+ xml.NegotiatedRatesIndicator
302
+ end
305
303
  end
306
- end
307
- # Conditionally required. Either this element or an ItemizedPaymentInformation
308
- # is needed. However, only PaymentInformation is not implemented.
309
- shipment << XmlNode.new('PaymentInformation') do |payment|
310
- payment << XmlNode.new('Prepaid') do |prepay|
311
- prepay << XmlNode.new('BillShipper') do |bill|
312
- bill << XmlNode.new('AccountNumber', options[:origin_account])
304
+
305
+ # Optional.
306
+ if options[:shipment] && options[:shipment][:reference_number]
307
+ xml.ReferenceNumber do
308
+ xml.Code(options[:shipment][:reference_number][:code] || "")
309
+ xml.Value(options[:shipment][:reference_number][:value])
313
310
  end
314
311
  end
312
+
313
+ # Conditionally required. Either this element or an ItemizedPaymentInformation
314
+ # is needed. However, only PaymentInformation is not implemented.
315
+ xml.PaymentInformation do
316
+ xml.Prepaid do
317
+ xml.BillShipper do
318
+ xml.AccountNumber(options[:origin_account])
319
+ end
320
+ end
321
+ end
322
+
323
+ # A request may specify multiple packages.
324
+ options[:imperial] ||= IMPERIAL_COUNTRIES.include?(origin.country_code(:alpha2))
325
+ Array(packages).each do |package|
326
+ build_package_node(xml, package, options)
327
+ end
315
328
  end
316
- # A request may specify multiple packages.
317
- options[:imperial] ||= IMPERIAL_COUNTRIES.include?(origin.country_code(:alpha2))
318
- packages.each do |package|
319
- shipment << build_package_node(package, options)
320
- end
321
- end
322
- # I don't know all of the options that UPS supports for labels
323
- # so I'm going with something very simple for now.
324
- root_node << XmlNode.new('LabelSpecification') do |specification|
325
- specification << XmlNode.new('LabelPrintMethod') do |print_method|
326
- print_method << XmlNode.new('Code', 'GIF')
327
- end
328
- specification << XmlNode.new('HTTPUserAgent', 'Mozilla/4.5') # hmmm
329
- specification << XmlNode.new('LabelImageFormat', 'GIF') do |image_format|
330
- image_format << XmlNode.new('Code', 'GIF')
329
+
330
+ # I don't know all of the options that UPS supports for labels
331
+ # so I'm going with something very simple for now.
332
+ xml.LabelSpecification do
333
+ xml.LabelPrintMethod do
334
+ xml.Code('GIF')
335
+ end
336
+ xml.HTTPUserAgent('Mozilla/4.5') # hmmm
337
+ xml.LabelImageFormat('GIF') do
338
+ xml.Code('GIF')
339
+ end
331
340
  end
332
341
  end
333
342
  end
334
- xml_request.to_s
343
+ xml_builder.to_xml
335
344
  end
336
345
 
337
346
  def build_accept_request(digest, options = {})
338
- xml_request = XmlNode.new('ShipmentAcceptRequest') do |root_node|
339
- root_node << XmlNode.new('Request') do |request|
340
- request << XmlNode.new('RequestAction', 'ShipAccept')
347
+ xml_builder = Nokogiri::XML::Builder.new do |xml|
348
+ xml.ShipmentAcceptRequest do
349
+ xml.Request do
350
+ xml.RequestAction('ShipAccept')
351
+ end
352
+ xml.ShipmentDigest(digest)
341
353
  end
342
- root_node << XmlNode.new('ShipmentDigest', digest)
343
354
  end
344
- xml_request.to_s
355
+ xml_builder.to_xml
345
356
  end
346
357
 
347
358
  def build_tracking_request(tracking_number, options = {})
348
- xml_request = XmlNode.new('TrackRequest') do |root_node|
349
- root_node << XmlNode.new('Request') do |request|
350
- request << XmlNode.new('RequestAction', 'Track')
351
- request << XmlNode.new('RequestOption', '1')
359
+ xml_builder = Nokogiri::XML::Builder.new do |xml|
360
+ xml.TrackRequest do
361
+ xml.Request do
362
+ xml.RequestAction('Track')
363
+ xml.RequestOption('1')
364
+ end
365
+ xml.TrackingNumber(tracking_number.to_s)
352
366
  end
353
- root_node << XmlNode.new('TrackingNumber', tracking_number.to_s)
354
367
  end
355
- xml_request.to_s
368
+ xml_builder.to_xml
356
369
  end
357
370
 
358
- def build_location_node(name, location, options = {})
371
+ def build_location_node(xml, name, location, options = {})
359
372
  # not implemented: * Shipment/Shipper/Name element
360
373
  # * Shipment/(ShipTo|ShipFrom)/CompanyName element
361
374
  # * Shipment/(Shipper|ShipTo|ShipFrom)/AttentionName element
362
375
  # * Shipment/(Shipper|ShipTo|ShipFrom)/TaxIdentificationNumber element
363
- XmlNode.new(name) do |location_node|
376
+ xml.public_send(name) do
364
377
  # You must specify the shipper name when creating labels.
365
378
  if shipper_name = (options[:origin_name] || @options[:origin_name])
366
- location_node << XmlNode.new('Name', shipper_name)
379
+ xml.Name(shipper_name)
367
380
  end
368
- location_node << XmlNode.new('PhoneNumber', location.phone.gsub(/[^\d]/, '')) unless location.phone.blank?
369
- location_node << XmlNode.new('FaxNumber', location.fax.gsub(/[^\d]/, '')) unless location.fax.blank?
381
+ xml.PhoneNumber(location.phone.gsub(/[^\d]/, '')) unless location.phone.blank?
382
+ xml.FaxNumber(location.fax.gsub(/[^\d]/, '')) unless location.fax.blank?
370
383
 
371
384
  if name == 'Shipper' and (origin_account = options[:origin_account] || @options[:origin_account])
372
- location_node << XmlNode.new('ShipperNumber', origin_account)
385
+ xml.ShipperNumber(origin_account)
373
386
  elsif name == 'ShipTo' and (destination_account = options[:destination_account] || @options[:destination_account])
374
- location_node << XmlNode.new('ShipperAssignedIdentificationNumber', destination_account)
387
+ xml.ShipperAssignedIdentificationNumber(destination_account)
375
388
  end
376
389
 
377
390
  if name = location.company_name || location.name
378
- location_node << XmlNode.new('CompanyName', name)
391
+ xml.CompanyName(name)
379
392
  end
380
393
 
381
394
  if phone = location.phone
382
- location_node << XmlNode.new('PhoneNumber', phone)
395
+ xml.PhoneNumber(phone)
383
396
  end
384
397
 
385
398
  if attn = location.name
386
- location_node << XmlNode.new('AttentionName', attn)
399
+ xml.AttentionName(attn)
387
400
  end
388
401
 
389
- location_node << XmlNode.new('Address') do |address|
390
- address << XmlNode.new("AddressLine1", location.address1) unless location.address1.blank?
391
- address << XmlNode.new("AddressLine2", location.address2) unless location.address2.blank?
392
- address << XmlNode.new("AddressLine3", location.address3) unless location.address3.blank?
393
- address << XmlNode.new("City", location.city) unless location.city.blank?
394
- address << XmlNode.new("StateProvinceCode", location.province) unless location.province.blank?
402
+ xml.Address do
403
+ xml.AddressLine1(location.address1) unless location.address1.blank?
404
+ xml.AddressLine2(location.address2) unless location.address2.blank?
405
+ xml.AddressLine3(location.address3) unless location.address3.blank?
406
+ xml.City(location.city) unless location.city.blank?
407
+ xml.StateProvinceCode(location.province) unless location.province.blank?
395
408
  # StateProvinceCode required for negotiated rates but not otherwise, for some reason
396
- address << XmlNode.new("PostalCode", location.postal_code) unless location.postal_code.blank?
397
- address << XmlNode.new("CountryCode", location.country_code(:alpha2)) unless location.country_code(:alpha2).blank?
398
- address << XmlNode.new("ResidentialAddressIndicator", true) unless location.commercial? # the default should be that UPS returns residential rates for destinations that it doesn't know about
409
+ xml.PostalCode(location.postal_code) unless location.postal_code.blank?
410
+ xml.CountryCode(location.country_code(:alpha2)) unless location.country_code(:alpha2).blank?
411
+ xml.ResidentialAddressIndicator(true) unless location.commercial? # the default should be that UPS returns residential rates for destinations that it doesn't know about
399
412
  # not implemented: Shipment/(Shipper|ShipTo|ShipFrom)/Address/ResidentialAddressIndicator element
400
413
  end
401
414
  end
402
415
  end
403
416
 
404
- def build_package_node(package, options = {})
405
- XmlNode.new("Package") do |package_node|
417
+ def build_package_node(xml, package, options = {})
418
+ xml.Package do
406
419
 
407
420
  # not implemented: * Shipment/Package/PackagingType element
408
421
  # * Shipment/Package/Description element
409
422
 
410
- package_node << XmlNode.new("PackagingType") do |packaging_type|
411
- packaging_type << XmlNode.new("Code", '02')
423
+ xml.PackagingType do
424
+ xml.Code('02')
412
425
  end
413
426
 
414
- package_node << XmlNode.new("Dimensions") do |dimensions|
415
- dimensions << XmlNode.new("UnitOfMeasurement") do |units|
416
- units << XmlNode.new("Code", options[:imperial] ? 'IN' : 'CM')
427
+ xml.Dimensions do
428
+ xml.UnitOfMeasurement do
429
+ xml.Code(options[:imperial] ? 'IN' : 'CM')
417
430
  end
418
431
  [:length, :width, :height].each do |axis|
419
432
  value = ((options[:imperial] ? package.inches(axis) : package.cm(axis)).to_f * 1000).round / 1000.0 # 3 decimals
420
- dimensions << XmlNode.new(axis.to_s.capitalize, [value, 0.1].max)
433
+ xml.public_send(axis.to_s.capitalize, [value, 0.1].max)
421
434
  end
422
435
  end
423
436
 
424
- package_node << XmlNode.new("PackageWeight") do |package_weight|
425
- package_weight << XmlNode.new("UnitOfMeasurement") do |units|
426
- units << XmlNode.new("Code", options[:imperial] ? 'LBS' : 'KGS')
437
+ xml.PackageWeight do
438
+ xml.UnitOfMeasurement do
439
+ xml.Code(options[:imperial] ? 'LBS' : 'KGS')
427
440
  end
428
441
 
429
442
  value = ((options[:imperial] ? package.lbs : package.kgs).to_f * 1000).round / 1000.0 # 3 decimals
430
- package_weight << XmlNode.new("Weight", [value, 0.1].max)
443
+ xml.Weight([value, 0.1].max)
431
444
  end
432
445
 
433
446
  if options[:package] && options[:package][:reference_number]
434
- package_node << XmlNode.new("ReferenceNumber") do |ref_node|
435
- ref_node << XmlNode.new("Code", options[:package][:reference_number][:code] || "")
436
- ref_node << XmlNode.new("Value", options[:package][:reference_number][:value])
447
+ xml.ReferenceNumber do
448
+ xml.Code(options[:package][:reference_number][:code] || "")
449
+ xml.Value(options[:package][:reference_number][:value])
437
450
  end
438
451
  end
439
452
 
440
- package_node
441
-
442
453
  # not implemented: * Shipment/Package/LargePackageIndicator element
443
454
  # * Shipment/Package/PackageServiceOptions element
444
455
  # * Shipment/Package/AdditionalHandling element
445
456
  end
446
457
  end
447
458
 
459
+ def build_document(xml, expected_root_tag)
460
+ document = Nokogiri.XML(xml)
461
+ if document.root.nil? || document.root.name != expected_root_tag
462
+ raise ActiveShipping::ResponseContentError.new(StandardError.new('Invalid document'), xml)
463
+ end
464
+ document
465
+ rescue Nokogiri::XML::SyntaxError => e
466
+ raise ActiveShipping::ResponseContentError.new(e, xml)
467
+ end
468
+
448
469
  def parse_rate_response(origin, destination, packages, response, options = {})
449
- xml = REXML::Document.new(response)
470
+ xml = build_document(response, 'RatingServiceSelectionResponse')
450
471
  success = response_success?(xml)
451
472
  message = response_message(xml)
452
473
 
453
474
  if success
454
- rate_estimates = []
455
-
456
- xml.elements.each('/*/RatedShipment') do |rated_shipment|
457
- service_code = rated_shipment.get_text('Service/Code').to_s
458
- days_to_delivery = rated_shipment.get_text('GuaranteedDaysToDelivery').to_s.to_i
475
+ rate_estimates = xml.root.css('> RatedShipment').map do |rated_shipment|
476
+ service_code = rated_shipment.at('Service/Code').text
477
+ days_to_delivery = rated_shipment.at('GuaranteedDaysToDelivery').text.to_i
459
478
  days_to_delivery = nil if days_to_delivery == 0
460
- rate_estimates << RateEstimate.new(origin, destination, @@name,
461
- service_name_for(origin, service_code),
462
- :total_price => rated_shipment.get_text('TotalCharges/MonetaryValue').to_s.to_f,
463
- :insurance_price => rated_shipment.get_text('ServiceOptionsCharges/MonetaryValue').to_s.to_f,
464
- :currency => rated_shipment.get_text('TotalCharges/CurrencyCode').to_s,
465
- :service_code => service_code,
466
- :packages => packages,
467
- :delivery_range => [timestamp_from_business_day(days_to_delivery)],
468
- :negotiated_rate => rated_shipment.get_text('NegotiatedRates/NetSummaryCharges/GrandTotal/MonetaryValue').to_s.to_f)
479
+ RateEstimate.new(origin, destination, @@name, service_name_for(origin, service_code),
480
+ :total_price => rated_shipment.at('TotalCharges/MonetaryValue').text.to_f,
481
+ :insurance_price => rated_shipment.at('ServiceOptionsCharges/MonetaryValue').text.to_f,
482
+ :currency => rated_shipment.at('TotalCharges/CurrencyCode').text,
483
+ :service_code => service_code,
484
+ :packages => packages,
485
+ :delivery_range => [timestamp_from_business_day(days_to_delivery)],
486
+ :negotiated_rate => rated_shipment.at('NegotiatedRates/NetSummaryCharges/GrandTotal/MonetaryValue').try(:text).to_f
487
+ )
469
488
  end
470
489
  end
471
490
  RateResponse.new(success, message, Hash.from_xml(response).values.first, :rates => rate_estimates, :xml => response, :request => last_request)
472
491
  end
473
492
 
474
493
  def parse_tracking_response(response, options = {})
475
- xml = REXML::Document.new(response)
494
+ xml = build_document(response, 'TrackResponse')
476
495
  success = response_success?(xml)
477
496
  message = response_message(xml)
478
497
 
@@ -482,19 +501,19 @@ module ActiveShipping
482
501
  delivered, exception = false
483
502
  shipment_events = []
484
503
 
485
- first_shipment = xml.elements['/*/Shipment']
486
- first_package = first_shipment.elements['Package']
487
- tracking_number = first_shipment.get_text('ShipmentIdentificationNumber | Package/TrackingNumber').to_s
504
+ first_shipment = xml.root.at('Shipment')
505
+ first_package = first_shipment.at('Package')
506
+ tracking_number = first_shipment.at_xpath('ShipmentIdentificationNumber | Package/TrackingNumber').text
488
507
 
489
508
  # Build status hash
490
- status_nodes = first_package.elements.to_a('Activity/Status/StatusType')
509
+ status_nodes = first_package.css('Activity > Status > StatusType')
491
510
 
492
511
  # Prefer a delivery node
493
- status_node = status_nodes.detect { |x| x.get_text('Code').to_s == 'D' }
512
+ status_node = status_nodes.detect { |x| x.at('Code').text == 'D' }
494
513
  status_node ||= status_nodes.first
495
514
 
496
- status_code = status_node.get_text('Code').to_s
497
- status_description = status_node.get_text('Description').to_s
515
+ status_code = status_node.at('Code').text
516
+ status_description = status_node.at('Description').text
498
517
  status = TRACKING_STATUS_CODES[status_code]
499
518
 
500
519
  if status_description =~ /out.*delivery/i
@@ -502,23 +521,23 @@ module ActiveShipping
502
521
  end
503
522
 
504
523
  origin, destination = %w(Shipper ShipTo).map do |location|
505
- location_from_address_node(first_shipment.elements["#{location}/Address"])
524
+ location_from_address_node(first_shipment.at("#{location}/Address"))
506
525
  end
507
526
 
508
527
  # Get scheduled delivery date
509
528
  unless status == :delivered
510
529
  scheduled_delivery_date = parse_ups_datetime(
511
- :date => first_shipment.get_text('ScheduledDeliveryDate'),
530
+ :date => first_shipment.at('ScheduledDeliveryDate'),
512
531
  :time => nil
513
532
  )
514
533
  end
515
534
 
516
- activities = first_package.get_elements('Activity')
535
+ activities = first_package.css('> Activity')
517
536
  unless activities.empty?
518
537
  shipment_events = activities.map do |activity|
519
- description = activity.get_text('Status/StatusType/Description').to_s
520
- zoneless_time = parse_ups_datetime(:time => activity.get_text('Time'), :date => activity.get_text('Date'))
521
- location = location_from_address_node(activity.elements['ActivityLocation/Address'])
538
+ description = activity.at('Status/StatusType/Description').text
539
+ zoneless_time = parse_ups_datetime(:time => activity.at('Time'), :date => activity.at('Date'))
540
+ location = location_from_address_node(activity.at('ActivityLocation/Address'))
522
541
  ShipmentEvent.new(description, zoneless_time, location)
523
542
  end
524
543
 
@@ -541,9 +560,9 @@ module ActiveShipping
541
560
  # Has the shipment been delivered?
542
561
  if status == :delivered
543
562
  delivered_activity = activities.first
544
- delivery_signature = delivered_activity.get_text('ActivityLocation/SignedForByName').to_s
545
- if delivered_activity.get_text('Status/StatusType/Code') == 'D'
546
- actual_delivery_date = parse_ups_datetime(:date => delivered_activity.get_text('Date'), :time => delivered_activity.get_text('Time'))
563
+ delivery_signature = delivered_activity.at('ActivityLocation/SignedForByName').try(:text)
564
+ if delivered_activity.at('Status/StatusType/Code').text == 'D'
565
+ actual_delivery_date = parse_ups_datetime(:date => delivered_activity.at('Date'), :time => delivered_activity.at('Time'))
547
566
  end
548
567
  unless destination
549
568
  destination = shipment_events[-1].location
@@ -575,18 +594,18 @@ module ActiveShipping
575
594
  def location_from_address_node(address)
576
595
  return nil unless address
577
596
  Location.new(
578
- :country => node_text_or_nil(address.elements['CountryCode']),
579
- :postal_code => node_text_or_nil(address.elements['PostalCode']),
580
- :province => node_text_or_nil(address.elements['StateProvinceCode']),
581
- :city => node_text_or_nil(address.elements['City']),
582
- :address1 => node_text_or_nil(address.elements['AddressLine1']),
583
- :address2 => node_text_or_nil(address.elements['AddressLine2']),
584
- :address3 => node_text_or_nil(address.elements['AddressLine3'])
585
- )
597
+ :country => address.at('CountryCode').try(:text),
598
+ :postal_code => address.at('PostalCode').try(:text),
599
+ :province => address.at('StateProvinceCode').try(:text),
600
+ :city => address.at('City').try(:text),
601
+ :address1 => address.at('AddressLine1').try(:text),
602
+ :address2 => address.at('AddressLine2').try(:text),
603
+ :address3 => address.at('AddressLine3').try(:text)
604
+ )
586
605
  end
587
606
 
588
607
  def parse_ups_datetime(options = {})
589
- time, date = options[:time].to_s, options[:date].to_s
608
+ time, date = options[:time].try(:text), options[:date].text
590
609
  if time.nil?
591
610
  hour, minute, second = 0
592
611
  else
@@ -597,24 +616,24 @@ module ActiveShipping
597
616
  Time.utc(year, month, day, hour, minute, second)
598
617
  end
599
618
 
600
- def response_success?(xml)
601
- xml.get_text('/*/Response/ResponseStatusCode').to_s == '1'
619
+ def response_success?(document)
620
+ document.root.at('Response/ResponseStatusCode').text == '1'
602
621
  end
603
622
 
604
- def response_message(xml)
605
- xml.get_text('/*/Response/Error/ErrorDescription | /*/Response/ResponseStatusDescription').to_s
623
+ def response_message(document)
624
+ document.root.at_xpath('Response/Error/ErrorDescription | Response/ResponseStatusDescription').text
606
625
  end
607
626
 
608
627
  def response_digest(xml)
609
- xml.get_text('/*/ShipmentDigest').to_s
628
+ xml.root.at('ShipmentDigest').text
610
629
  end
611
630
 
612
631
  def parse_ship_confirm(response)
613
- REXML::Document.new(response)
632
+ build_document(response, 'ShipmentConfirmResponse')
614
633
  end
615
634
 
616
635
  def parse_ship_accept(response)
617
- xml = REXML::Document.new(response)
636
+ xml = build_document(response, 'ShipmentAcceptResponse')
618
637
  success = response_success?(xml)
619
638
  message = response_message(xml)
620
639