active_shipping 1.0.0.pre1 → 1.0.0.pre2

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.
@@ -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