active_shipping 1.0.0.pre4 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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