active_shipping 1.0.0.pre4 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +0 -0
  4. data/.yardopts +0 -1
  5. data/CHANGELOG.md +17 -0
  6. data/CONTRIBUTING.md +2 -2
  7. data/Gemfile.activesupport32 +1 -0
  8. data/README.md +1 -1
  9. data/Rakefile +1 -1
  10. data/active_shipping.gemspec +2 -2
  11. data/lib/active_shipping.rb +2 -3
  12. data/lib/active_shipping/carriers.rb +1 -0
  13. data/lib/active_shipping/carriers/canada_post_pws.rb +281 -266
  14. data/lib/active_shipping/carriers/correios.rb +285 -0
  15. data/lib/active_shipping/carriers/fedex.rb +205 -199
  16. data/lib/active_shipping/carriers/stamps.rb +218 -219
  17. data/lib/active_shipping/carriers/ups.rb +287 -48
  18. data/lib/active_shipping/carriers/usps.rb +3 -3
  19. data/lib/active_shipping/delivery_date_estimate.rb +21 -0
  20. data/lib/active_shipping/delivery_date_estimates_response.rb +11 -0
  21. data/lib/active_shipping/errors.rb +20 -1
  22. data/lib/active_shipping/location.rb +0 -5
  23. data/lib/active_shipping/response.rb +0 -15
  24. data/lib/active_shipping/version.rb +1 -1
  25. data/test/credentials.yml +11 -3
  26. data/test/fixtures/xml/correios/book_response.xml +13 -0
  27. data/test/fixtures/xml/correios/book_response_invalid.xml +13 -0
  28. data/test/fixtures/xml/correios/clothes_response.xml +43 -0
  29. data/test/fixtures/xml/correios/poster_response.xml +23 -0
  30. data/test/fixtures/xml/correios/shoes_response.xml +43 -0
  31. data/test/fixtures/xml/fedex/tracking_request.xml +13 -11
  32. data/test/fixtures/xml/fedex/tracking_response_bad_tracking_number.xml +20 -0
  33. data/test/fixtures/xml/fedex/tracking_response_delivered_at_door.xml +254 -0
  34. data/test/fixtures/xml/fedex/tracking_response_delivered_at_facility.xml +403 -0
  35. data/test/fixtures/xml/fedex/tracking_response_delivered_with_signature.xml +269 -0
  36. data/test/fixtures/xml/fedex/tracking_response_in_transit.xml +127 -0
  37. data/test/fixtures/xml/fedex/tracking_response_multiple_results.xml +100 -0
  38. data/test/fixtures/xml/fedex/tracking_response_not_found.xml +52 -0
  39. data/test/fixtures/xml/fedex/tracking_response_shipment_exception.xml +209 -0
  40. data/test/fixtures/xml/ups/delivery_dates_response.xml +140 -0
  41. data/test/fixtures/xml/ups/package_exceeds_maximum_length.xml +12 -0
  42. data/test/fixtures/xml/ups/rate_single_service.xml +54 -0
  43. data/test/fixtures/xml/ups/rescheduled_shipment.xml +204 -0
  44. data/test/fixtures/xml/ups/test_real_home_as_residential_destination_response.xml +290 -1
  45. data/test/fixtures/xml/usps/delivered_extended_tracking_response.xml +11 -0
  46. data/test/remote/canada_post_pws_platform_test.rb +35 -22
  47. data/test/remote/canada_post_pws_test.rb +32 -40
  48. data/test/remote/correios_test.rb +83 -0
  49. data/test/remote/fedex_test.rb +95 -13
  50. data/test/remote/stamps_test.rb +1 -0
  51. data/test/remote/ups_test.rb +77 -40
  52. data/test/remote/usps_test.rb +13 -1
  53. data/test/test_helper.rb +12 -2
  54. data/test/unit/carriers/canada_post_pws_rating_test.rb +66 -59
  55. data/test/unit/carriers/canada_post_pws_shipping_test.rb +34 -23
  56. data/test/unit/carriers/correios_test.rb +244 -0
  57. data/test/unit/carriers/fedex_test.rb +161 -156
  58. data/test/unit/carriers/ups_test.rb +193 -1
  59. data/test/unit/carriers/usps_test.rb +14 -0
  60. data/test/unit/location_test.rb +0 -10
  61. metadata +63 -46
  62. metadata.gz.sig +0 -0
  63. data/lib/vendor/test_helper.rb +0 -6
  64. data/lib/vendor/xml_node/README +0 -36
  65. data/lib/vendor/xml_node/Rakefile +0 -21
  66. data/lib/vendor/xml_node/benchmark/bench_generation.rb +0 -30
  67. data/lib/vendor/xml_node/init.rb +0 -1
  68. data/lib/vendor/xml_node/lib/xml_node.rb +0 -221
  69. data/lib/vendor/xml_node/test/test_generating.rb +0 -89
  70. data/lib/vendor/xml_node/test/test_parsing.rb +0 -40
  71. data/test/fixtures/xml/fedex/tracking_response.xml +0 -151
  72. data/test/fixtures/xml/fedex/tracking_response_empty_destination.xml +0 -76
  73. data/test/fixtures/xml/fedex/tracking_response_no_destination.xml +0 -139
  74. data/test/fixtures/xml/fedex/tracking_response_no_ship_time.xml +0 -150
  75. data/test/fixtures/xml/fedex/tracking_response_with_estimated_delivery_date.xml +0 -95
  76. data/test/fixtures/xml/fedex/tracking_response_with_shipper_address.xml +0 -71
@@ -15,7 +15,8 @@ module ActiveShipping
15
15
  :rates => 'ups.app/xml/Rate',
16
16
  :track => 'ups.app/xml/Track',
17
17
  :ship_confirm => 'ups.app/xml/ShipConfirm',
18
- :ship_accept => 'ups.app/xml/ShipAccept'
18
+ :ship_accept => 'ups.app/xml/ShipAccept',
19
+ :delivery_dates => 'ups.app/xml/TimeInTransit'
19
20
  }
20
21
 
21
22
  PICKUP_CODES = HashWithIndifferentAccess.new(
@@ -102,6 +103,22 @@ module ActiveShipping
102
103
 
103
104
  IMPERIAL_COUNTRIES = %w(US LR MM)
104
105
 
106
+ DEFAULT_SERVICE_NAME_TO_CODE = Hash[UPS::DEFAULT_SERVICES.to_a.map(&:reverse)]
107
+ DEFAULT_SERVICE_NAME_TO_CODE['UPS 2nd Day Air'] = "02"
108
+ DEFAULT_SERVICE_NAME_TO_CODE['UPS 3 Day Select'] = "12"
109
+
110
+ SHIPMENT_DELIVERY_CONFIRMATION_CODES = {
111
+ delivery_confirmation_signature_required: 1,
112
+ delivery_confirmation_adult_signature_required: 2
113
+ }
114
+
115
+ PACKAGE_DELIVERY_CONFIRMATION_CODES = {
116
+ delivery_confirmation: 1,
117
+ delivery_confirmation_signature_required: 2,
118
+ delivery_confirmation_adult_signature_required: 3,
119
+ usps_delivery_confirmation: 4
120
+ }
121
+
105
122
  def requirements
106
123
  [:key, :login, :password]
107
124
  end
@@ -144,8 +161,8 @@ module ActiveShipping
144
161
  xml = parse_ship_confirm(confirm_response)
145
162
  success = response_success?(xml)
146
163
  message = response_message(xml)
147
- digest = response_digest(xml)
148
164
  raise message unless success
165
+ digest = response_digest(xml)
149
166
 
150
167
  # STEP 2: Accept. Use shipment digest in first response to get the actual label.
151
168
  accept_request = build_accept_request(digest, options)
@@ -164,6 +181,16 @@ module ActiveShipping
164
181
  end
165
182
  end
166
183
 
184
+ def get_delivery_date_estimates(origin, destination, packages, pickup_date=Date.current, options = {})
185
+ origin, destination = upsified_location(origin), upsified_location(destination)
186
+ options = @options.merge(options)
187
+ packages = Array(packages)
188
+ access_request = build_access_request
189
+ dates_request = build_delivery_dates_request(origin, destination, packages, pickup_date, options)
190
+ response = commit(:delivery_dates, save_request(access_request + dates_request), (options[:test] || false))
191
+ parse_delivery_dates_response(origin, destination, packages, response, options)
192
+ end
193
+
167
194
  protected
168
195
 
169
196
  def upsified_location(location)
@@ -182,7 +209,7 @@ module ActiveShipping
182
209
  xml_builder = Nokogiri::XML::Builder.new do |xml|
183
210
  xml.AccessRequest do
184
211
  xml.AccessLicenseNumber(@options[:key])
185
- xml.UserId(@options[:password])
212
+ xml.UserId(@options[:login])
186
213
  xml.Password(@options[:password])
187
214
  end
188
215
  end
@@ -194,9 +221,7 @@ module ActiveShipping
194
221
  xml.RatingServiceSelectionRequest do
195
222
  xml.Request do
196
223
  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')
224
+ xml.RequestOption((options[:service].nil?) ? 'Shop' : 'Rate')
200
225
  end
201
226
 
202
227
  pickup_type = options[:pickup_type] || :daily_pickup
@@ -226,13 +251,19 @@ module ActiveShipping
226
251
  # * Shipment/AlternateDeliveryTime element
227
252
  # * Shipment/DocumentsOnly element
228
253
 
254
+ unless options[:service].nil?
255
+ xml.Service do
256
+ xml.Code(options[:service])
257
+ end
258
+ end
259
+
229
260
  Array(packages).each do |package|
230
261
  options[:imperial] ||= IMPERIAL_COUNTRIES.include?(origin.country_code(:alpha2))
231
262
  build_package_node(xml, package, options)
232
263
  end
233
264
 
234
265
  # not implemented: * Shipment/ShipmentServiceOptions element
235
- if options[:origin_account]
266
+ if options[:negotiated_rates]
236
267
  xml.RateInformation do
237
268
  xml.NegotiatedRatesIndicator
238
269
  end
@@ -251,18 +282,34 @@ module ActiveShipping
251
282
  # * shipper: who is sending the package and where it should be returned
252
283
  # if it is undeliverable.
253
284
  # * ship_from: where the package is picked up.
254
- # * service_code: default to '14'
255
- # * service_descriptor: default to 'Next Day Air Early AM'
285
+ # * service_code: default to '03'
256
286
  # * saturday_delivery: any truthy value causes this element to exist
257
287
  # * optional_processing: 'validate' (blank) or 'nonvalidate' or blank
258
- #
259
- def build_shipment_request(origin, destination, packages, options = {})
260
- # There are a lot of unimplemented elements, documenting all of them
261
- # wouldprobably be unhelpful.
288
+ # * paperless_invoice: set to truthy if using paperless invoice to ship internationally
289
+ # * terms_of_shipment: used with paperless invoice to specify who pays duties and taxes
290
+ # * reference_numbers: Array of hashes with :value => a reference number value and optionally :code => reference number type
291
+ # * prepay: if truthy the shipper will be bill immediatly. Otherwise the shipper is billed when the label is used.
292
+ # * negotiated_rates: if truthy negotiated rates will be requested from ups. Only valid if shipper account has negotiated rates.
293
+ # * delivery_confirmation: Can be set to any key from SHIPMENT_DELIVERY_CONFIRMATION_CODES. Can also be set on package level via package.options
294
+ def build_shipment_request(origin, destination, packages, options={})
295
+ packages = Array(packages)
296
+ options[:international] = origin.country.name != destination.country.name
297
+ options[:imperial] ||= IMPERIAL_COUNTRIES.include?(origin.country_code(:alpha2))
298
+
299
+ if allow_package_level_reference_numbers(origin, destination)
300
+ if options[:reference_numbers]
301
+ packages.each do |package|
302
+ package.options[:reference_numbers] = options[:reference_numbers]
303
+ end
304
+ end
305
+ options[:reference_numbers] = []
306
+ end
307
+
308
+ handle_delivery_confirmation_options(origin, destination, packages, options)
309
+
262
310
  xml_builder = Nokogiri::XML::Builder.new do |xml|
263
311
  xml.ShipmentConfirmRequest do
264
312
  xml.Request do
265
- # Required element and the text must be "ShipConfirm"
266
313
  xml.RequestAction('ShipConfirm')
267
314
  # Required element cotnrols level of address validation.
268
315
  xml.RequestOption(options[:optional_processing] || 'validate')
@@ -275,54 +322,96 @@ module ActiveShipping
275
322
  end
276
323
 
277
324
  xml.Shipment do
278
- # Required element.
279
325
  xml.Service do
280
- xml.Code(options[:service_code] || '14')
281
- xml.Description(options[:service_description] || 'Next Day Air Early AM')
326
+ xml.Code(options[:service_code] || '03')
282
327
  end
283
328
 
284
- # Required element. The delivery destination.
285
- build_location_node(xml, 'ShipTo', destination, {})
329
+ build_location_node(xml, 'ShipTo', destination, options)
330
+ build_location_node(xml, 'ShipFrom', origin, options)
286
331
  # 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]
332
+ build_location_node(xml, 'Shipper', options[:shipper] || origin, options)
290
333
 
291
- # Optional.
292
334
  if options[:saturday_delivery]
293
335
  xml.ShipmentServiceOptions do
294
336
  xml.SaturdayDelivery
295
337
  end
296
338
  end
297
339
 
298
- # Optional.
299
340
  if options[:origin_account]
300
341
  xml.RateInformation do
301
342
  xml.NegotiatedRatesIndicator
302
343
  end
303
344
  end
304
345
 
305
- # Optional.
306
- if options[:shipment] && options[:shipment][:reference_number]
346
+ Array(options[:reference_numbers]).each do |reference_num_info|
307
347
  xml.ReferenceNumber do
308
- xml.Code(options[:shipment][:reference_number][:code] || "")
309
- xml.Value(options[:shipment][:reference_number][:value])
348
+ xml.Code(reference_num_info[:code] || "")
349
+ xml.Value(reference_num_info[:value])
310
350
  end
311
351
  end
312
352
 
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])
353
+ if options[:prepay]
354
+ xml.PaymentInformation do
355
+ xml.Prepaid do
356
+ xml.BillShipper do
357
+ xml.AccountNumber(options[:origin_account])
358
+ end
319
359
  end
320
360
  end
361
+ else
362
+ xml.ItemizedPaymentInformation do
363
+ xml.ShipmentCharge do
364
+ # Type '01' means 'Transportation'
365
+ # This node specifies who will be billed for transportation.
366
+ xml.Type('01')
367
+ xml.BillShipper do
368
+ xml.AccountNumber(options[:origin_account])
369
+ end
370
+ end
371
+ if options[:terms_of_shipment] == 'DDP'
372
+ # DDP stands for delivery duty paid and means the shipper will cover duties and taxes
373
+ # Otherwise UPS will charge the receiver
374
+ xml.ShipmentCharge do
375
+ xml.Type('02') # Type '02' means 'Duties and Taxes'
376
+ xml.BillShipper do
377
+ xml.AccountNumber(options[:origin_account])
378
+ end
379
+ end
380
+ end
381
+ end
382
+ end
383
+
384
+ if options[:international]
385
+ build_location_node(xml, 'SoldTo', options[:sold_to] || destination, options)
386
+
387
+ if origin.country_code(:alpha2) == 'US' && ['CA', 'PR'].include?(destination.country_code(:alpha2))
388
+ # Required for shipments from the US to Puerto Rico or Canada
389
+ xml.InvoiceLineTotal do
390
+ total_value = packages.inject(0) {|sum, package| sum + (package.value || 0)}
391
+ xml.MonetaryValue(total_value)
392
+ end
393
+ end
394
+
395
+ contents_description = packages.map {|p| p.options[:description]}.compact.join(',')
396
+ unless contents_description.empty?
397
+ xml.Description(contents_description)
398
+ end
399
+ end
400
+
401
+ xml.ShipmentServiceOptions do
402
+ if delivery_confirmation = options[:delivery_confirmation]
403
+ xml.DeliveryConfirmation do
404
+ xml.DCISType(SHIPMENT_DELIVERY_CONFIRMATION_CODES[delivery_confirmation])
405
+ end
406
+ end
407
+
408
+ if options[:international]
409
+ build_international_forms(xml, origin, destination, packages, options)
410
+ end
321
411
  end
322
412
 
323
413
  # A request may specify multiple packages.
324
- options[:imperial] ||= IMPERIAL_COUNTRIES.include?(origin.country_code(:alpha2))
325
- Array(packages).each do |package|
414
+ packages.each do |package|
326
415
  build_package_node(xml, package, options)
327
416
  end
328
417
  end
@@ -343,6 +432,61 @@ module ActiveShipping
343
432
  xml_builder.to_xml
344
433
  end
345
434
 
435
+ def build_delivery_dates_request(origin, destination, packages, pickup_date, options={})
436
+ xml_builder = Nokogiri::XML::Builder.new do |xml|
437
+
438
+ xml.TimeInTransitRequest do
439
+ xml.Request do
440
+ xml.RequestAction('TimeInTransit')
441
+ end
442
+
443
+ build_address_artifact_format_location(xml, 'TransitFrom', origin)
444
+ build_address_artifact_format_location(xml, 'TransitTo', destination)
445
+
446
+ xml.InvoiceLineTotal do
447
+ xml.CurrencyCode('USD')
448
+ total_value = packages.inject(0) {|sum, package| sum + package.value}
449
+ xml.MonetaryValue(total_value)
450
+ end
451
+
452
+ xml.PickupDate(pickup_date.strftime('%Y%m%d'))
453
+ end
454
+ end
455
+
456
+ xml_builder.to_xml
457
+ end
458
+
459
+ def build_international_forms(xml, origin, destination, packages, options)
460
+ if options[:paperless_invoice]
461
+ xml.InternationalForms do
462
+ xml.FormType('01') # 01 is "Invoice"
463
+ xml.InvoiceDate(options[:invoice_date] || Date.today.strftime('%Y%m%d'))
464
+ xml.ReasonForExport(options[:reason_for_export] || 'SALE')
465
+ xml.CurrencyCode(options[:currency_code] || 'USD')
466
+
467
+ if options[:terms_of_shipment]
468
+ xml.TermsOfShipment(options[:terms_of_shipment])
469
+ end
470
+
471
+ packages.each do |package|
472
+ xml.Product do |xml|
473
+ xml.Description(package.options[:description])
474
+ xml.CommodityCode(package.options[:commodity_code])
475
+ xml.OriginCountryCode(origin.country_code(:alpha2))
476
+ xml.Unit do |xml|
477
+ xml.Value(package.value / (package.options[:item_count] || 1))
478
+ xml.Number((package.options[:item_count] || 1))
479
+ xml.UnitOfMeasurement do |xml|
480
+ # NMB = number. You can specify units in barrels, boxes, etc. Codes are in the api docs.
481
+ xml.Code(package.options[:unit_of_item_count] || 'NMB')
482
+ end
483
+ end
484
+ end
485
+ end
486
+ end
487
+ end
488
+ end
489
+
346
490
  def build_accept_request(digest, options = {})
347
491
  xml_builder = Nokogiri::XML::Builder.new do |xml|
348
492
  xml.ShipmentAcceptRequest do
@@ -374,8 +518,7 @@ module ActiveShipping
374
518
  # * Shipment/(Shipper|ShipTo|ShipFrom)/AttentionName element
375
519
  # * Shipment/(Shipper|ShipTo|ShipFrom)/TaxIdentificationNumber element
376
520
  xml.public_send(name) do
377
- # You must specify the shipper name when creating labels.
378
- if shipper_name = (options[:origin_name] || @options[:origin_name])
521
+ if shipper_name = (location.name || location.company_name || options[:origin_name])
379
522
  xml.Name(shipper_name)
380
523
  end
381
524
  xml.PhoneNumber(location.phone.gsub(/[^\d]/, '')) unless location.phone.blank?
@@ -387,7 +530,7 @@ module ActiveShipping
387
530
  xml.ShipperAssignedIdentificationNumber(destination_account)
388
531
  end
389
532
 
390
- if name = location.company_name || location.name
533
+ if name = (location.company_name || location.name || options[:origin_name])
391
534
  xml.CompanyName(name)
392
535
  end
393
536
 
@@ -414,6 +557,18 @@ module ActiveShipping
414
557
  end
415
558
  end
416
559
 
560
+ def build_address_artifact_format_location(xml, name, location)
561
+ xml.public_send(name) do
562
+ xml.AddressArtifactFormat do
563
+ xml.PoliticalDivision2(location.city)
564
+ xml.PoliticalDivision1(location.province)
565
+ xml.CountryCode(location.country_code(:alpha2))
566
+ xml.PostcodePrimaryLow(location.postal_code)
567
+ xml.ResidentialAddressIndicator(true) unless location.commercial?
568
+ end
569
+ end
570
+ end
571
+
417
572
  def build_package_node(xml, package, options = {})
418
573
  xml.Package do
419
574
 
@@ -443,15 +598,23 @@ module ActiveShipping
443
598
  xml.Weight([value, 0.1].max)
444
599
  end
445
600
 
446
- if options[:package] && options[:package][:reference_number]
601
+
602
+ Array(package.options[:reference_numbers]).each do |reference_number_info|
447
603
  xml.ReferenceNumber do
448
- xml.Code(options[:package][:reference_number][:code] || "")
449
- xml.Value(options[:package][:reference_number][:value])
604
+ xml.Code(reference_number_info[:code] || "")
605
+ xml.Value(reference_number_info[:value])
606
+ end
607
+ end
608
+
609
+ xml.PackageServiceOptions do
610
+ if delivery_confirmation = package.options[:delivery_confirmation]
611
+ xml.DeliveryConfirmation do
612
+ xml.DCISType(PACKAGE_DELIVERY_CONFIRMATION_CODES[delivery_confirmation])
613
+ end
450
614
  end
451
615
  end
452
616
 
453
617
  # not implemented: * Shipment/Package/LargePackageIndicator element
454
- # * Shipment/Package/PackageServiceOptions element
455
618
  # * Shipment/Package/AdditionalHandling element
456
619
  end
457
620
  end
@@ -526,10 +689,15 @@ module ActiveShipping
526
689
 
527
690
  # Get scheduled delivery date
528
691
  unless status == :delivered
529
- scheduled_delivery_date = parse_ups_datetime(
530
- :date => first_shipment.at('ScheduledDeliveryDate'),
531
- :time => nil
532
- )
692
+ scheduled_delivery_date_node = first_shipment.at('ScheduledDeliveryDate')
693
+ scheduled_delivery_date_node ||= first_shipment.at('RescheduledDeliveryDate')
694
+
695
+ if scheduled_delivery_date_node
696
+ scheduled_delivery_date = parse_ups_datetime(
697
+ :date => scheduled_delivery_date_node,
698
+ :time => nil
699
+ )
700
+ end
533
701
  end
534
702
 
535
703
  activities = first_package.css('> Activity')
@@ -591,6 +759,30 @@ module ActiveShipping
591
759
  :tracking_number => tracking_number)
592
760
  end
593
761
 
762
+ def parse_delivery_dates_response(origin, destination, packages, response, options={})
763
+ xml = build_document(response, 'TimeInTransitResponse')
764
+ success = response_success?(xml)
765
+ message = response_message(xml)
766
+ delivery_estimates = []
767
+
768
+ if success
769
+ xml.css('ServiceSummary').each do |service_summary|
770
+ # Translate the Time in Transit Codes to the service codes used elsewhere
771
+ service_name = service_summary.at('Service/Description').text
772
+ service_code = UPS::DEFAULT_SERVICE_NAME_TO_CODE[service_name]
773
+ date = Date.strptime(service_summary.at('EstimatedArrival/Date').text, '%Y-%m-%d')
774
+ business_transit_days = service_summary.at('EstimatedArrival/BusinessTransitDays').text.to_i
775
+ delivery_estimates << DeliveryDateEstimate.new(origin, destination, self.class.class_variable_get(:@@name),
776
+ service_name,
777
+ :service_code => service_code,
778
+ :guaranteed => service_summary.at('Guaranteed/Code').text == 'Y',
779
+ :date => date,
780
+ :business_transit_days => business_transit_days)
781
+ end
782
+ end
783
+ response = DeliveryDateEstimatesResponse.new(success, message, Hash.from_xml(response).values.first, :delivery_estimates => delivery_estimates, :xml => response, :request => last_request)
784
+ end
785
+
594
786
  def location_from_address_node(address)
595
787
  return nil unless address
596
788
  Location.new(
@@ -621,7 +813,9 @@ module ActiveShipping
621
813
  end
622
814
 
623
815
  def response_message(document)
624
- document.root.at_xpath('Response/Error/ErrorDescription | Response/ResponseStatusDescription').text
816
+ status = document.root.at_xpath('Response/ResponseStatusDescription').try(:text)
817
+ desc = document.root.at_xpath('Response/Error/ErrorDescription').try(:text)
818
+ [status, desc].select(&:present?).join(": ").presence || "UPS could not process the request."
625
819
  end
626
820
 
627
821
  def response_digest(xml)
@@ -663,5 +857,50 @@ module ActiveShipping
663
857
  name ||= OTHER_NON_US_ORIGIN_SERVICES[code] unless name == 'US'
664
858
  name || DEFAULT_SERVICES[code]
665
859
  end
860
+
861
+ def allow_package_level_reference_numbers(origin, destination)
862
+ # if the package is US -> US or PR -> PR the only type of reference numbers that are allowed are package-level
863
+ # Otherwise the only type of reference numbers that are allowed are shipment-level
864
+ [['US','US'],['PR', 'PR']].include?([origin,destination].map(&:country_code))
865
+ end
866
+
867
+ def handle_delivery_confirmation_options(origin, destination, packages, options)
868
+ if package_level_delivery_confirmation?(origin, destination)
869
+ handle_package_level_delivery_confirmation(origin, destination, packages, options)
870
+ else
871
+ handle_shipment_level_delivery_confirmation(origin, destination, packages, options)
872
+ end
873
+ end
874
+
875
+ def handle_package_level_delivery_confirmation(origin, destination, packages, options)
876
+ packages.each do |package|
877
+ # Transfer shipment-level option to package with no specified delivery_confirmation
878
+ package.options[:delivery_confirmation] = options[:delivery_confirmation] unless package.options[:delivery_confirmation]
879
+
880
+ # Assert that option is valid
881
+ if package.options[:delivery_confirmation] && !PACKAGE_DELIVERY_CONFIRMATION_CODES[package.options[:delivery_confirmation]]
882
+ raise "Invalid delivery_confirmation option on package: '#{package.options[:delivery_confirmation]}'. Use a key from PACKAGE_DELIVERY_CONFIRMATION_CODES"
883
+ end
884
+ end
885
+ options.delete(:delivery_confirmation)
886
+ end
887
+
888
+ def handle_shipment_level_delivery_confirmation(origin, destination, packages, options)
889
+ if packages.any? { |p| p.options[:delivery_confirmation] }
890
+ raise "origin/destination pair does not support package level delivery_confirmation options"
891
+ end
892
+
893
+ if options[:delivery_confirmation] && !SHIPMENT_DELIVERY_CONFIRMATION_CODES[options[:delivery_confirmation]]
894
+ raise "Invalid delivery_confirmation option: '#{options[:delivery_confirmation]}'. Use a key from SHIPMENT_DELIVERY_CONFIRMATION_CODES"
895
+ end
896
+ end
897
+
898
+ # For certain origin/destination pairs, UPS allows each package in a shipment to have a specified delivery_confirmation option
899
+ # otherwise the delivery_confirmation option must be specified on the entire shipment.
900
+ # See Appendix P of UPS Shipping Package XML Developers Guide for the rules on which the logic below is based.
901
+ def package_level_delivery_confirmation?(origin, destination)
902
+ origin.country_code == destination.country_code ||
903
+ [['US','PR'], ['PR','US']].include?([origin,destination].map(&:country_code))
904
+ end
666
905
  end
667
906
  end