reactive_shipping 3.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.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/.travis.yml +33 -0
- data/.yardopts +13 -0
- data/CHANGELOG.md +225 -0
- data/CONTRIBUTING.md +23 -0
- data/Gemfile +3 -0
- data/MIT-LICENSE +21 -0
- data/README.md +158 -0
- data/Rakefile +35 -0
- data/dev.yml +17 -0
- data/gemfiles/activesupport42.gemfile +5 -0
- data/gemfiles/activesupport50.gemfile +6 -0
- data/gemfiles/activesupport51.gemfile +5 -0
- data/gemfiles/activesupport52.gemfile +5 -0
- data/gemfiles/activesupport_master.gemfile +5 -0
- data/lib/certs/eParcel.dtd +111 -0
- data/lib/reactive_shipping.rb +26 -0
- data/lib/reactive_shipping/address_validation_response.rb +30 -0
- data/lib/reactive_shipping/carrier.rb +184 -0
- data/lib/reactive_shipping/carriers.rb +35 -0
- data/lib/reactive_shipping/carriers/australia_post.rb +248 -0
- data/lib/reactive_shipping/carriers/benchmark_carrier.rb +31 -0
- data/lib/reactive_shipping/carriers/bogus_carrier.rb +12 -0
- data/lib/reactive_shipping/carriers/canada_post.rb +263 -0
- data/lib/reactive_shipping/carriers/canada_post_pws.rb +908 -0
- data/lib/reactive_shipping/carriers/fedex.rb +797 -0
- data/lib/reactive_shipping/carriers/kunaki.rb +155 -0
- data/lib/reactive_shipping/carriers/new_zealand_post.rb +260 -0
- data/lib/reactive_shipping/carriers/shipwire.rb +178 -0
- data/lib/reactive_shipping/carriers/stamps.rb +860 -0
- data/lib/reactive_shipping/carriers/ups.rb +1060 -0
- data/lib/reactive_shipping/carriers/usps.rb +708 -0
- data/lib/reactive_shipping/carriers/usps_returns.rb +86 -0
- data/lib/reactive_shipping/delivery_date_estimate.rb +20 -0
- data/lib/reactive_shipping/delivery_date_estimates_response.rb +11 -0
- data/lib/reactive_shipping/errors.rb +35 -0
- data/lib/reactive_shipping/external_return_label_request.rb +417 -0
- data/lib/reactive_shipping/external_return_label_response.rb +26 -0
- data/lib/reactive_shipping/label.rb +10 -0
- data/lib/reactive_shipping/label_response.rb +10 -0
- data/lib/reactive_shipping/location.rb +166 -0
- data/lib/reactive_shipping/package.rb +165 -0
- data/lib/reactive_shipping/package_item.rb +60 -0
- data/lib/reactive_shipping/rate_estimate.rb +197 -0
- data/lib/reactive_shipping/rate_response.rb +33 -0
- data/lib/reactive_shipping/response.rb +44 -0
- data/lib/reactive_shipping/shipment_event.rb +22 -0
- data/lib/reactive_shipping/shipment_packer.rb +108 -0
- data/lib/reactive_shipping/shipping_response.rb +34 -0
- data/lib/reactive_shipping/tracking_response.rb +120 -0
- data/lib/reactive_shipping/version.rb +3 -0
- data/reactive_shipping.gemspec +38 -0
- data/shipit.rubygems.yml +1 -0
- data/test/console.rb +39 -0
- data/test/credentials.yml +76 -0
- data/test/fixtures/files/label1.pdf +0 -0
- data/test/fixtures/files/ups-shipping-label.gif +0 -0
- data/test/fixtures/json/australia_post/calculate_domestic.json +13 -0
- data/test/fixtures/json/australia_post/calculate_domestic_2.json +19 -0
- data/test/fixtures/json/australia_post/calculate_international.json +12 -0
- data/test/fixtures/json/australia_post/calculate_international_2.json +15 -0
- data/test/fixtures/json/australia_post/error_message.json +5 -0
- data/test/fixtures/json/australia_post/service_domestic.json +117 -0
- data/test/fixtures/json/australia_post/service_domestic_2.json +117 -0
- data/test/fixtures/json/australia_post/service_international.json +76 -0
- data/test/fixtures/json/australia_post/service_international_2.json +59 -0
- data/test/fixtures/json/newzealandpost/domestic_book.json +1 -0
- data/test/fixtures/json/newzealandpost/domestic_default.json +1 -0
- data/test/fixtures/json/newzealandpost/domestic_error.json +1 -0
- data/test/fixtures/json/newzealandpost/domestic_poster.json +1 -0
- data/test/fixtures/json/newzealandpost/domestic_small_half_pound.json +1 -0
- data/test/fixtures/json/newzealandpost/international_book.json +1 -0
- data/test/fixtures/json/newzealandpost/international_new_zealand_wii.json +1 -0
- data/test/fixtures/json/newzealandpost/international_small_half_pound.json +1 -0
- data/test/fixtures/json/newzealandpost/international_wii.json +1 -0
- data/test/fixtures/xml/canadapost/example_request.xml +25 -0
- data/test/fixtures/xml/canadapost/example_response.xml +130 -0
- data/test/fixtures/xml/canadapost/example_response_error.xml +16 -0
- data/test/fixtures/xml/canadapost/example_response_french.xml +122 -0
- data/test/fixtures/xml/canadapost/example_response_with_nil_value.xml +164 -0
- data/test/fixtures/xml/canadapost/example_response_with_postal_outlet.xml +155 -0
- data/test/fixtures/xml/canadapost/example_response_with_postal_outlet_french.xml +274 -0
- data/test/fixtures/xml/canadapost/example_response_with_strange_delivery_date.xml +130 -0
- data/test/fixtures/xml/canadapost_pws/dnc_tracking_details_en.xml +112 -0
- data/test/fixtures/xml/canadapost_pws/merchant_details_error.xml +7 -0
- data/test/fixtures/xml/canadapost_pws/merchant_details_response.xml +7 -0
- data/test/fixtures/xml/canadapost_pws/option_response.xml +13 -0
- data/test/fixtures/xml/canadapost_pws/option_response_no_conflicts.xml +7 -0
- data/test/fixtures/xml/canadapost_pws/rates_info.xml +190 -0
- data/test/fixtures/xml/canadapost_pws/rates_info_error.xml +7 -0
- data/test/fixtures/xml/canadapost_pws/receipt_response.xml +42 -0
- data/test/fixtures/xml/canadapost_pws/receipt_response_no_priced_options.xml +36 -0
- data/test/fixtures/xml/canadapost_pws/register_token_error.xml +7 -0
- data/test/fixtures/xml/canadapost_pws/register_token_response.xml +3 -0
- data/test/fixtures/xml/canadapost_pws/service_options_response.xml +42 -0
- data/test/fixtures/xml/canadapost_pws/services_error.xml +6 -0
- data/test/fixtures/xml/canadapost_pws/services_response.xml +32 -0
- data/test/fixtures/xml/canadapost_pws/shipment_domestic.xml +69 -0
- data/test/fixtures/xml/canadapost_pws/shipment_response.xml +20 -0
- data/test/fixtures/xml/canadapost_pws/shipment_us.xml +69 -0
- data/test/fixtures/xml/canadapost_pws/tracking_details_en.xml +152 -0
- data/test/fixtures/xml/canadapost_pws/tracking_details_en_error.xml +7 -0
- data/test/fixtures/xml/canadapost_pws/tracking_details_en_undelivered.xml +116 -0
- data/test/fixtures/xml/canadapost_pws/tracking_details_fr.xml +156 -0
- data/test/fixtures/xml/canadapost_pws/tracking_details_no_expected_delivery_date.xml +40 -0
- data/test/fixtures/xml/fedex/create_shipment_response.xml +2 -0
- data/test/fixtures/xml/fedex/freight_rate_request.xml +82 -0
- data/test/fixtures/xml/fedex/freight_rate_response.xml +506 -0
- data/test/fixtures/xml/fedex/invalid_fedex_reply.xml +27 -0
- data/test/fixtures/xml/fedex/ottawa_to_beverly_hills_commercial_rate_request.xml +79 -0
- data/test/fixtures/xml/fedex/ottawa_to_beverly_hills_no_saturday_rate_request.xml +79 -0
- data/test/fixtures/xml/fedex/ottawa_to_beverly_hills_rate_request.xml +80 -0
- data/test/fixtures/xml/fedex/ottawa_to_beverly_hills_rate_response.xml +214 -0
- data/test/fixtures/xml/fedex/raterequest_reply.xml +213 -0
- data/test/fixtures/xml/fedex/raterequest_response_with_ground_home_delivery.xml +206 -0
- data/test/fixtures/xml/fedex/reply_without_notifications.xml +185 -0
- data/test/fixtures/xml/fedex/tracking_request.xml +29 -0
- data/test/fixtures/xml/fedex/tracking_response_bad_tracking_number.xml +20 -0
- data/test/fixtures/xml/fedex/tracking_response_delivered_at_door.xml +254 -0
- data/test/fixtures/xml/fedex/tracking_response_delivered_at_facility.xml +403 -0
- data/test/fixtures/xml/fedex/tracking_response_delivered_with_signature.xml +269 -0
- data/test/fixtures/xml/fedex/tracking_response_empty_status_detail.xml +84 -0
- data/test/fixtures/xml/fedex/tracking_response_failure_code_9045.xml +52 -0
- data/test/fixtures/xml/fedex/tracking_response_failure_code_9080.xml +51 -0
- data/test/fixtures/xml/fedex/tracking_response_in_transit.xml +127 -0
- data/test/fixtures/xml/fedex/tracking_response_invalid_tracking_number.xml +52 -0
- data/test/fixtures/xml/fedex/tracking_response_missing_status_code.xml +89 -0
- data/test/fixtures/xml/fedex/tracking_response_multiple_results.xml +100 -0
- data/test/fixtures/xml/fedex/tracking_response_not_found.xml +52 -0
- data/test/fixtures/xml/fedex/tracking_response_shipment_exception.xml +209 -0
- data/test/fixtures/xml/fedex/tracking_response_unable_to_process.xml +32 -0
- data/test/fixtures/xml/fedex/tracking_response_with_blank_state.xml +107 -0
- data/test/fixtures/xml/fedex/unknown_fedex_document_reply.xml +3 -0
- data/test/fixtures/xml/kunaki/invalid_state_response.xml +3 -0
- data/test/fixtures/xml/kunaki/no_valid_items_response.xml +3 -0
- data/test/fixtures/xml/kunaki/successful_rates_response.xml +3 -0
- data/test/fixtures/xml/kunaki/unsuccessful_rates_response.xml +9 -0
- data/test/fixtures/xml/shipwire/international_rates_response.xml +17 -0
- data/test/fixtures/xml/shipwire/new_carrier_rate_response.xml +18 -0
- data/test/fixtures/xml/shipwire/no_rates_response.xml +7 -0
- data/test/fixtures/xml/shipwire/rates_response.xml +36 -0
- data/test/fixtures/xml/shipwire/rates_response_no_estimate.xml +14 -0
- data/test/fixtures/xml/stamps/authenticate_user_request.xml +15 -0
- data/test/fixtures/xml/stamps/authenticate_user_response.xml +10 -0
- data/test/fixtures/xml/stamps/cleanse_address_request.xml +19 -0
- data/test/fixtures/xml/stamps/cleanse_address_response.xml +27 -0
- data/test/fixtures/xml/stamps/create_indicium_request.xml +69 -0
- data/test/fixtures/xml/stamps/create_indicium_response.xml +40 -0
- data/test/fixtures/xml/stamps/expired_authenticator_response.xml +15 -0
- data/test/fixtures/xml/stamps/get_account_info_request.xml +11 -0
- data/test/fixtures/xml/stamps/get_account_info_response.xml +36 -0
- data/test/fixtures/xml/stamps/get_purchase_status_request.xml +12 -0
- data/test/fixtures/xml/stamps/get_purchase_status_response.xml +16 -0
- data/test/fixtures/xml/stamps/get_rates_request.xml +19 -0
- data/test/fixtures/xml/stamps/get_rates_response.xml +351 -0
- data/test/fixtures/xml/stamps/purchase_postage_request.xml +13 -0
- data/test/fixtures/xml/stamps/purchase_postage_response.xml +17 -0
- data/test/fixtures/xml/stamps/track_shipment_request.xml +12 -0
- data/test/fixtures/xml/stamps/track_shipment_response.xml +45 -0
- data/test/fixtures/xml/ups/access_request.xml +6 -0
- data/test/fixtures/xml/ups/delivered_shipment_with_refund.xml +290 -0
- data/test/fixtures/xml/ups/delivered_shipment_without_events_tracking_response.xml +62 -0
- data/test/fixtures/xml/ups/delivery_dates_response.xml +140 -0
- data/test/fixtures/xml/ups/example_tracking_response.xml +53 -0
- data/test/fixtures/xml/ups/in_transit_shipment.xml +183 -0
- data/test/fixtures/xml/ups/out_for_delivery_shipment.xml +165 -0
- data/test/fixtures/xml/ups/package_exceeds_maximum_length.xml +12 -0
- data/test/fixtures/xml/ups/rate_single_service.xml +54 -0
- data/test/fixtures/xml/ups/rescheduled_shipment.xml +204 -0
- data/test/fixtures/xml/ups/shipment_accept_response.xml +42 -0
- data/test/fixtures/xml/ups/shipment_confirm_response.xml +33 -0
- data/test/fixtures/xml/ups/shipment_from_tiger_direct.xml +222 -0
- data/test/fixtures/xml/ups/test_real_home_as_residential_destination_response.xml +290 -0
- data/test/fixtures/xml/ups/test_real_home_as_residential_destination_response_with_insured.xml +289 -0
- data/test/fixtures/xml/ups/test_real_home_as_residential_destination_with_origin_account_response.xml +311 -0
- data/test/fixtures/xml/ups/tracking_request.xml +9 -0
- data/test/fixtures/xml/ups/triple_accept_response.xml +72 -0
- data/test/fixtures/xml/ups/triple_confirm_response.xml +32 -0
- data/test/fixtures/xml/ups/void_shipment_response.xml +11 -0
- data/test/fixtures/xml/usps/api_error_rate_response.xml +53 -0
- data/test/fixtures/xml/usps/beverly_hills_to_new_york_book_commercial_base_rate_response.xml +2 -0
- data/test/fixtures/xml/usps/beverly_hills_to_new_york_book_commercial_plus_rate_response.xml +258 -0
- data/test/fixtures/xml/usps/beverly_hills_to_new_york_book_rate_response.xml +108 -0
- data/test/fixtures/xml/usps/beverly_hills_to_ottawa_american_wii_commercial_base_rate_response.xml +84 -0
- data/test/fixtures/xml/usps/beverly_hills_to_ottawa_american_wii_commercial_plus_rate_response.xml +212 -0
- data/test/fixtures/xml/usps/beverly_hills_to_ottawa_american_wii_rate_response.xml +230 -0
- data/test/fixtures/xml/usps/first_class_packages_with_invalid_mail_type_response.xml +12 -0
- data/test/fixtures/xml/usps/first_class_packages_with_mail_type_response.xml +16 -0
- data/test/fixtures/xml/usps/first_class_packages_without_mail_type_response.xml +12 -0
- data/test/fixtures/xml/usps/invalid_xml_response.xml +10 -0
- data/test/fixtures/xml/usps/invalid_xml_tracking_response_error.xml +2 -0
- data/test/fixtures/xml/usps/tracking_request.xml +10 -0
- data/test/fixtures/xml/usps/tracking_request_batch.xml +12 -0
- data/test/fixtures/xml/usps/tracking_response.xml +162 -0
- data/test/fixtures/xml/usps/tracking_response_alt.xml +53 -0
- data/test/fixtures/xml/usps/tracking_response_batch.xml +231 -0
- data/test/fixtures/xml/usps/tracking_response_failure.xml +11 -0
- data/test/fixtures/xml/usps/tracking_response_not_available.xml +12 -0
- data/test/fixtures/xml/usps/tracking_response_test_error.xml +8 -0
- data/test/fixtures/xml/usps/us_rate_request.xml +18 -0
- data/test/fixtures/xml/usps/us_rate_request_large.xml +18 -0
- data/test/fixtures/xml/usps/world_rate_request_only_country.xml +22 -0
- data/test/fixtures/xml/usps/world_rate_request_with_value.xml +24 -0
- data/test/fixtures/xml/usps/world_rate_request_without_value.xml +24 -0
- data/test/fixtures/xml/usps_returns/external_return_label_response.xml +2 -0
- data/test/fixtures/xml/usps_returns/external_return_label_response_failure.xml +10 -0
- data/test/remote/australia_post_test.rb +140 -0
- data/test/remote/canada_post_pws_platform_test.rb +259 -0
- data/test/remote/canada_post_pws_test.rb +169 -0
- data/test/remote/canada_post_test.rb +55 -0
- data/test/remote/fedex_test.rb +400 -0
- data/test/remote/kunaki_test.rb +37 -0
- data/test/remote/new_zealand_post_test.rb +149 -0
- data/test/remote/shipwire_test.rb +84 -0
- data/test/remote/stamps_test.rb +396 -0
- data/test/remote/usps_returns_test.rb +72 -0
- data/test/remote/usps_test.rb +243 -0
- data/test/test_helper.rb +296 -0
- data/test/unit/carrier_test.rb +130 -0
- data/test/unit/carriers/australia_post_test.rb +181 -0
- data/test/unit/carriers/benchmark_test.rb +18 -0
- data/test/unit/carriers/canada_post_pws_rating_test.rb +379 -0
- data/test/unit/carriers/canada_post_pws_register_test.rb +76 -0
- data/test/unit/carriers/canada_post_pws_shipping_test.rb +258 -0
- data/test/unit/carriers/canada_post_pws_test.rb +59 -0
- data/test/unit/carriers/canada_post_pws_tracking_test.rb +154 -0
- data/test/unit/carriers/canada_post_test.rb +148 -0
- data/test/unit/carriers/fedex_test.rb +693 -0
- data/test/unit/carriers/kunaki_test.rb +56 -0
- data/test/unit/carriers/new_zealand_post_test.rb +177 -0
- data/test/unit/carriers/shipwire_test.rb +188 -0
- data/test/unit/carriers/stamps_test.rb +245 -0
- data/test/unit/carriers/ups_test.rb +580 -0
- data/test/unit/carriers/usps_returns_test.rb +45 -0
- data/test/unit/carriers/usps_test.rb +633 -0
- data/test/unit/carriers_test.rb +16 -0
- data/test/unit/external_return_label_request_test.rb +258 -0
- data/test/unit/location_test.rb +234 -0
- data/test/unit/package_item_test.rb +232 -0
- data/test/unit/package_test.rb +404 -0
- data/test/unit/rate_estimate_test.rb +93 -0
- data/test/unit/response_test.rb +38 -0
- data/test/unit/shipment_event_test.rb +20 -0
- data/test/unit/shipment_packer_test.rb +212 -0
- data/test/unit/tracking_response_test.rb +41 -0
- metadata +684 -0
|
@@ -0,0 +1,1060 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
module ReactiveShipping
|
|
4
|
+
class UPS < Carrier
|
|
5
|
+
self.retry_safe = true
|
|
6
|
+
|
|
7
|
+
cattr_accessor :default_options
|
|
8
|
+
cattr_reader :name
|
|
9
|
+
@@name = "UPS"
|
|
10
|
+
|
|
11
|
+
TEST_URL = 'https://wwwcie.ups.com'
|
|
12
|
+
LIVE_URL = 'https://onlinetools.ups.com'
|
|
13
|
+
|
|
14
|
+
RESOURCES = {
|
|
15
|
+
:rates => 'ups.app/xml/Rate',
|
|
16
|
+
:track => 'ups.app/xml/Track',
|
|
17
|
+
:ship_confirm => 'ups.app/xml/ShipConfirm',
|
|
18
|
+
:ship_accept => 'ups.app/xml/ShipAccept',
|
|
19
|
+
:delivery_dates => 'ups.app/xml/TimeInTransit',
|
|
20
|
+
:void => 'ups.app/xml/Void'
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
PICKUP_CODES = HashWithIndifferentAccess.new(
|
|
24
|
+
:daily_pickup => "01",
|
|
25
|
+
:customer_counter => "03",
|
|
26
|
+
:one_time_pickup => "06",
|
|
27
|
+
:on_call_air => "07",
|
|
28
|
+
:suggested_retail_rates => "11",
|
|
29
|
+
:letter_center => "19",
|
|
30
|
+
:air_service_center => "20"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
CUSTOMER_CLASSIFICATIONS = HashWithIndifferentAccess.new(
|
|
34
|
+
:wholesale => "01",
|
|
35
|
+
:occasional => "03",
|
|
36
|
+
:retail => "04"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# these are the defaults described in the UPS API docs,
|
|
40
|
+
# but they don't seem to apply them under all circumstances,
|
|
41
|
+
# so we need to take matters into our own hands
|
|
42
|
+
DEFAULT_CUSTOMER_CLASSIFICATIONS = Hash.new do |hash, key|
|
|
43
|
+
hash[key] = case key.to_sym
|
|
44
|
+
when :daily_pickup then :wholesale
|
|
45
|
+
when :customer_counter then :retail
|
|
46
|
+
else
|
|
47
|
+
:occasional
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
DEFAULT_SERVICES = {
|
|
52
|
+
"01" => "UPS Next Day Air",
|
|
53
|
+
"02" => "UPS Second Day Air",
|
|
54
|
+
"03" => "UPS Ground",
|
|
55
|
+
"07" => "UPS Worldwide Express",
|
|
56
|
+
"08" => "UPS Worldwide Expedited",
|
|
57
|
+
"11" => "UPS Standard",
|
|
58
|
+
"12" => "UPS Three-Day Select",
|
|
59
|
+
"13" => "UPS Next Day Air Saver",
|
|
60
|
+
"14" => "UPS Next Day Air Early A.M.",
|
|
61
|
+
"54" => "UPS Worldwide Express Plus",
|
|
62
|
+
"59" => "UPS Second Day Air A.M.",
|
|
63
|
+
"65" => "UPS Saver",
|
|
64
|
+
"82" => "UPS Today Standard",
|
|
65
|
+
"83" => "UPS Today Dedicated Courier",
|
|
66
|
+
"84" => "UPS Today Intercity",
|
|
67
|
+
"85" => "UPS Today Express",
|
|
68
|
+
"86" => "UPS Today Express Saver",
|
|
69
|
+
"92" => "UPS SurePost (USPS) < 1lb",
|
|
70
|
+
"93" => "UPS SurePost (USPS) > 1lb",
|
|
71
|
+
"94" => "UPS SurePost (USPS) BPM",
|
|
72
|
+
"95" => "UPS SurePost (USPS) Media",
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
CANADA_ORIGIN_SERVICES = {
|
|
76
|
+
"01" => "UPS Express",
|
|
77
|
+
"02" => "UPS Expedited",
|
|
78
|
+
"14" => "UPS Express Early A.M."
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
MEXICO_ORIGIN_SERVICES = {
|
|
82
|
+
"07" => "UPS Express",
|
|
83
|
+
"08" => "UPS Expedited",
|
|
84
|
+
"54" => "UPS Express Plus"
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
EU_ORIGIN_SERVICES = {
|
|
88
|
+
"07" => "UPS Express",
|
|
89
|
+
"08" => "UPS Expedited"
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
OTHER_NON_US_ORIGIN_SERVICES = {
|
|
93
|
+
"07" => "UPS Express"
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
RETURN_SERVICE_CODES = {
|
|
97
|
+
"2" => "UPS Print and Mail (PNM)",
|
|
98
|
+
"3" => "UPS Return Service 1-Attempt (RS1)",
|
|
99
|
+
"5" => "UPS Return Service 3-Attempt (RS3)",
|
|
100
|
+
"8" => "UPS Electronic Return Label (ERL)",
|
|
101
|
+
"9" => "UPS Print Return Label (PRL)",
|
|
102
|
+
"10" => "UPS Exchange Print Return Label",
|
|
103
|
+
"11" => "UPS Pack & Collect Service 1-Attempt Box 1",
|
|
104
|
+
"12" => "UPS Pack & Collect Service 1-Attempt Box 2",
|
|
105
|
+
"13" => "UPS Pack & Collect Service 1-Attempt Box 3",
|
|
106
|
+
"14" => "UPS Pack & Collect Service 1-Attempt Box 4",
|
|
107
|
+
"15" => "UPS Pack & Collect Service 1-Attempt Box 5",
|
|
108
|
+
"16" => "UPS Pack & Collect Service 3-Attempt Box 1",
|
|
109
|
+
"17" => "UPS Pack & Collect Service 3-Attempt Box 2",
|
|
110
|
+
"18" => "UPS Pack & Collect Service 3-Attempt Box 3",
|
|
111
|
+
"19" => "UPS Pack & Collect Service 3-Attempt Box 4",
|
|
112
|
+
"20" => "UPS Pack & Collect Service 3-Attempt Box 5",
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
TRACKING_STATUS_CODES = HashWithIndifferentAccess.new(
|
|
116
|
+
'I' => :in_transit,
|
|
117
|
+
'D' => :delivered,
|
|
118
|
+
'X' => :exception,
|
|
119
|
+
'P' => :pickup,
|
|
120
|
+
'M' => :manifest_pickup
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# From http://en.wikipedia.org/w/index.php?title=European_Union&oldid=174718707 (Current as of November 30, 2007)
|
|
124
|
+
EU_COUNTRY_CODES = %w(GB AT BE BG CY CZ DK EE FI FR DE GR HU IE IT LV LT LU MT NL PL PT RO SK SI ES SE)
|
|
125
|
+
|
|
126
|
+
US_TERRITORIES_TREATED_AS_COUNTRIES = %w(AS FM GU MH MP PW PR VI)
|
|
127
|
+
|
|
128
|
+
IMPERIAL_COUNTRIES = %w(US LR MM)
|
|
129
|
+
|
|
130
|
+
DEFAULT_SERVICE_NAME_TO_CODE = Hash[UPS::DEFAULT_SERVICES.to_a.map(&:reverse)]
|
|
131
|
+
DEFAULT_SERVICE_NAME_TO_CODE['UPS 2nd Day Air'] = "02"
|
|
132
|
+
DEFAULT_SERVICE_NAME_TO_CODE['UPS 3 Day Select'] = "12"
|
|
133
|
+
DEFAULT_SERVICE_NAME_TO_CODE['UPS Next Day Air Early'] = "14"
|
|
134
|
+
|
|
135
|
+
SHIPMENT_DELIVERY_CONFIRMATION_CODES = {
|
|
136
|
+
delivery_confirmation_signature_required: 1,
|
|
137
|
+
delivery_confirmation_adult_signature_required: 2
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
PACKAGE_DELIVERY_CONFIRMATION_CODES = {
|
|
141
|
+
delivery_confirmation: 1,
|
|
142
|
+
delivery_confirmation_signature_required: 2,
|
|
143
|
+
delivery_confirmation_adult_signature_required: 3,
|
|
144
|
+
usps_delivery_confirmation: 4
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
def requirements
|
|
148
|
+
[:key, :login, :password]
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def find_rates(origin, destination, packages, options = {})
|
|
152
|
+
origin, destination = upsified_location(origin), upsified_location(destination)
|
|
153
|
+
options = @options.merge(options)
|
|
154
|
+
packages = Array(packages)
|
|
155
|
+
access_request = build_access_request
|
|
156
|
+
rate_request = build_rate_request(origin, destination, packages, options)
|
|
157
|
+
response = commit(:rates, save_request(access_request + rate_request), options[:test])
|
|
158
|
+
parse_rate_response(origin, destination, packages, response, options)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Retrieves tracking information for a previous shipment
|
|
162
|
+
#
|
|
163
|
+
# @note Override with whatever you need to get a shipping label
|
|
164
|
+
#
|
|
165
|
+
# @param tracking_number [String] The unique identifier of the shipment to track.
|
|
166
|
+
# @param options [Hash] Carrier-specific parameters.
|
|
167
|
+
# @option options [Boolean] :mail_innovations Set this to true to track a Mail Innovations Package
|
|
168
|
+
# @return [ReactiveShipping::TrackingResponse] The response from the carrier. This
|
|
169
|
+
# response should a list of shipment tracking events if successful.
|
|
170
|
+
def find_tracking_info(tracking_number, options = {})
|
|
171
|
+
options = @options.merge(options)
|
|
172
|
+
access_request = build_access_request
|
|
173
|
+
tracking_request = build_tracking_request(tracking_number, options)
|
|
174
|
+
response = commit(:track, save_request(access_request + tracking_request), options[:test])
|
|
175
|
+
parse_tracking_response(response, options)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def create_shipment(origin, destination, packages, options = {})
|
|
179
|
+
options = @options.merge(options)
|
|
180
|
+
packages = Array(packages)
|
|
181
|
+
access_request = build_access_request
|
|
182
|
+
|
|
183
|
+
# STEP 1: Confirm. Validation step, important for verifying price.
|
|
184
|
+
confirm_request = build_shipment_request(origin, destination, packages, options)
|
|
185
|
+
logger.debug(confirm_request) if logger
|
|
186
|
+
|
|
187
|
+
confirm_response = commit(:ship_confirm, save_request(access_request + confirm_request), (options[:test] || false))
|
|
188
|
+
logger.debug(confirm_response) if logger
|
|
189
|
+
|
|
190
|
+
# ... now, get the digest, it's needed to get the label. In theory,
|
|
191
|
+
# one could make decisions based on the price or some such to avoid
|
|
192
|
+
# surprises. This also has *no* error handling yet.
|
|
193
|
+
xml = parse_ship_confirm(confirm_response)
|
|
194
|
+
success = response_success?(xml)
|
|
195
|
+
message = response_message(xml)
|
|
196
|
+
raise message unless success
|
|
197
|
+
digest = response_digest(xml)
|
|
198
|
+
|
|
199
|
+
# STEP 2: Accept. Use shipment digest in first response to get the actual label.
|
|
200
|
+
accept_request = build_accept_request(digest, options)
|
|
201
|
+
logger.debug(accept_request) if logger
|
|
202
|
+
|
|
203
|
+
accept_response = commit(:ship_accept, save_request(access_request + accept_request), (options[:test] || false))
|
|
204
|
+
logger.debug(accept_response) if logger
|
|
205
|
+
|
|
206
|
+
# ...finally, build a map from the response that contains
|
|
207
|
+
# the label data and tracking information.
|
|
208
|
+
parse_ship_accept(accept_response)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def get_delivery_date_estimates(origin, destination, packages, pickup_date=Date.current, options = {})
|
|
212
|
+
origin, destination = upsified_location(origin), upsified_location(destination)
|
|
213
|
+
options = @options.merge(options)
|
|
214
|
+
packages = Array(packages)
|
|
215
|
+
access_request = build_access_request
|
|
216
|
+
dates_request = build_delivery_dates_request(origin, destination, packages, pickup_date, options)
|
|
217
|
+
response = commit(:delivery_dates, save_request(access_request + dates_request), (options[:test] || false))
|
|
218
|
+
parse_delivery_dates_response(origin, destination, packages, response, options)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def void_shipment(tracking, options={})
|
|
222
|
+
options = @options.merge(options)
|
|
223
|
+
access_request = build_access_request
|
|
224
|
+
void_request = build_void_request(tracking)
|
|
225
|
+
response = commit(:void, save_request(access_request + void_request), (options[:test] || false))
|
|
226
|
+
parse_void_response(response, options)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def maximum_address_field_length
|
|
230
|
+
# http://www.ups.com/worldshiphelp/WS12/ENU/AppHelp/CONNECT/Shipment_Data_Field_Descriptions.htm
|
|
231
|
+
35
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
protected
|
|
235
|
+
|
|
236
|
+
def upsified_location(location)
|
|
237
|
+
if location.country_code == 'US' && US_TERRITORIES_TREATED_AS_COUNTRIES.include?(location.state)
|
|
238
|
+
atts = {:country => location.state}
|
|
239
|
+
[:zip, :city, :address1, :address2, :address3, :phone, :fax, :address_type].each do |att|
|
|
240
|
+
atts[att] = location.send(att)
|
|
241
|
+
end
|
|
242
|
+
Location.new(atts)
|
|
243
|
+
else
|
|
244
|
+
location
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def build_access_request
|
|
249
|
+
xml_builder = Nokogiri::XML::Builder.new do |xml|
|
|
250
|
+
xml.AccessRequest do
|
|
251
|
+
xml.AccessLicenseNumber(@options[:key])
|
|
252
|
+
xml.UserId(@options[:login])
|
|
253
|
+
xml.Password(@options[:password])
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
xml_builder.to_xml
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def build_rate_request(origin, destination, packages, options = {})
|
|
260
|
+
xml_builder = Nokogiri::XML::Builder.new do |xml|
|
|
261
|
+
xml.RatingServiceSelectionRequest do
|
|
262
|
+
xml.Request do
|
|
263
|
+
xml.RequestAction('Rate')
|
|
264
|
+
xml.RequestOption((options[:service].nil?) ? 'Shop' : 'Rate')
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
pickup_type = options[:pickup_type] || :daily_pickup
|
|
268
|
+
|
|
269
|
+
xml.PickupType do
|
|
270
|
+
xml.Code(PICKUP_CODES[pickup_type])
|
|
271
|
+
# not implemented: PickupType/PickupDetails element
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
cc = options[:customer_classification] || DEFAULT_CUSTOMER_CLASSIFICATIONS[pickup_type]
|
|
275
|
+
xml.CustomerClassification do
|
|
276
|
+
xml.Code(CUSTOMER_CLASSIFICATIONS[cc])
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
xml.Shipment do
|
|
280
|
+
# not implemented: Shipment/Description element
|
|
281
|
+
build_location_node(xml, 'Shipper', (options[:shipper] || origin), options)
|
|
282
|
+
build_location_node(xml, 'ShipTo', destination, options)
|
|
283
|
+
build_location_node(xml, 'ShipFrom', origin, options) if options[:shipper] && options[:shipper] != origin
|
|
284
|
+
|
|
285
|
+
# not implemented: * Shipment/ShipmentWeight element
|
|
286
|
+
# * Shipment/ReferenceNumber element
|
|
287
|
+
# * Shipment/Service element
|
|
288
|
+
# * Shipment/PickupDate element
|
|
289
|
+
# * Shipment/ScheduledDeliveryDate element
|
|
290
|
+
# * Shipment/ScheduledDeliveryTime element
|
|
291
|
+
# * Shipment/AlternateDeliveryTime element
|
|
292
|
+
# * Shipment/DocumentsOnly element
|
|
293
|
+
|
|
294
|
+
unless options[:service].nil?
|
|
295
|
+
xml.Service do
|
|
296
|
+
xml.Code(options[:service])
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
Array(packages).each do |package|
|
|
301
|
+
options[:imperial] ||= IMPERIAL_COUNTRIES.include?(origin.country_code(:alpha2))
|
|
302
|
+
build_package_node(xml, package, options)
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# not implemented: * Shipment/ShipmentServiceOptions element
|
|
306
|
+
if options[:negotiated_rates]
|
|
307
|
+
xml.RateInformation do
|
|
308
|
+
xml.NegotiatedRatesIndicator
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
xml_builder.to_xml
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# Build XML node to request a shipping label for the given packages.
|
|
318
|
+
#
|
|
319
|
+
# options:
|
|
320
|
+
# * origin_account: who will pay for the shipping label
|
|
321
|
+
# * customer_context: a "guid like substance" -- according to UPS
|
|
322
|
+
# * shipper: who is sending the package and where it should be returned
|
|
323
|
+
# if it is undeliverable.
|
|
324
|
+
# * ship_from: where the package is picked up.
|
|
325
|
+
# * service_code: default to '03'
|
|
326
|
+
# * saturday_delivery: any truthy value causes this element to exist
|
|
327
|
+
# * optional_processing: 'validate' (blank) or 'nonvalidate' or blank
|
|
328
|
+
# * paperless_invoice: set to truthy if using paperless invoice to ship internationally
|
|
329
|
+
# * terms_of_shipment: used with paperless invoice to specify who pays duties and taxes
|
|
330
|
+
# * reference_numbers: Array of hashes with :value => a reference number value and optionally :code => reference number type
|
|
331
|
+
# * prepay: if truthy the shipper will be bill immediatly. Otherwise the shipper is billed when the label is used.
|
|
332
|
+
# * negotiated_rates: if truthy negotiated rates will be requested from ups. Only valid if shipper account has negotiated rates.
|
|
333
|
+
# * delivery_confirmation: Can be set to any key from SHIPMENT_DELIVERY_CONFIRMATION_CODES. Can also be set on package level via package.options
|
|
334
|
+
def build_shipment_request(origin, destination, packages, options={})
|
|
335
|
+
packages = Array(packages)
|
|
336
|
+
shipper = options[:shipper] || origin
|
|
337
|
+
options[:international] = origin.country.name != destination.country.name
|
|
338
|
+
options[:imperial] ||= IMPERIAL_COUNTRIES.include?(shipper.country_code(:alpha2))
|
|
339
|
+
options[:return] = options[:return_service_code].present?
|
|
340
|
+
options[:reason_for_export] ||= ("RETURN" if options[:return])
|
|
341
|
+
|
|
342
|
+
if allow_package_level_reference_numbers(origin, destination)
|
|
343
|
+
if options[:reference_numbers]
|
|
344
|
+
packages.each do |package|
|
|
345
|
+
package.options[:reference_numbers] = options[:reference_numbers]
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
options[:reference_numbers] = []
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
handle_delivery_confirmation_options(origin, destination, packages, options)
|
|
352
|
+
|
|
353
|
+
xml_builder = Nokogiri::XML::Builder.new do |xml|
|
|
354
|
+
xml.ShipmentConfirmRequest do
|
|
355
|
+
xml.Request do
|
|
356
|
+
xml.RequestAction('ShipConfirm')
|
|
357
|
+
# Required element cotnrols level of address validation.
|
|
358
|
+
xml.RequestOption(options[:optional_processing] || 'validate')
|
|
359
|
+
# Optional element to identify transactions between client and server.
|
|
360
|
+
if options[:customer_context]
|
|
361
|
+
xml.TransactionReference do
|
|
362
|
+
xml.CustomerContext(options[:customer_context])
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
xml.Shipment do
|
|
368
|
+
xml.Service do
|
|
369
|
+
xml.Code(options[:service_code] || '03')
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
build_location_node(xml, 'ShipTo', destination, options)
|
|
373
|
+
build_location_node(xml, 'ShipFrom', origin, options)
|
|
374
|
+
# Required element. The company whose account is responsible for the label(s).
|
|
375
|
+
build_location_node(xml, 'Shipper', shipper, options)
|
|
376
|
+
|
|
377
|
+
if options[:saturday_delivery]
|
|
378
|
+
xml.ShipmentServiceOptions do
|
|
379
|
+
xml.SaturdayDelivery
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
if options[:origin_account]
|
|
384
|
+
xml.RateInformation do
|
|
385
|
+
xml.NegotiatedRatesIndicator
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
Array(options[:reference_numbers]).each do |reference_num_info|
|
|
390
|
+
xml.ReferenceNumber do
|
|
391
|
+
xml.Code(reference_num_info[:code] || "")
|
|
392
|
+
xml.Value(reference_num_info[:value])
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
if options[:prepay]
|
|
397
|
+
xml.PaymentInformation do
|
|
398
|
+
xml.Prepaid do
|
|
399
|
+
xml.BillShipper do
|
|
400
|
+
xml.AccountNumber(options[:origin_account])
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
elsif options[:bill_third_party]
|
|
405
|
+
xml.PaymentInformation do
|
|
406
|
+
xml.BillThirdParty do
|
|
407
|
+
xml.BillThirdPartyShipper do
|
|
408
|
+
xml.AccountNumber(options[:billing_account])
|
|
409
|
+
xml.ThirdParty do
|
|
410
|
+
xml.Address do
|
|
411
|
+
xml.PostalCode(options[:billing_zip])
|
|
412
|
+
xml.CountryCode(options[:billing_country])
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
end
|
|
418
|
+
else
|
|
419
|
+
xml.ItemizedPaymentInformation do
|
|
420
|
+
xml.ShipmentCharge do
|
|
421
|
+
# Type '01' means 'Transportation'
|
|
422
|
+
# This node specifies who will be billed for transportation.
|
|
423
|
+
xml.Type('01')
|
|
424
|
+
xml.BillShipper do
|
|
425
|
+
xml.AccountNumber(options[:origin_account])
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
if options[:terms_of_shipment] == 'DDP'
|
|
429
|
+
# DDP stands for delivery duty paid and means the shipper will cover duties and taxes
|
|
430
|
+
# Otherwise UPS will charge the receiver
|
|
431
|
+
xml.ShipmentCharge do
|
|
432
|
+
xml.Type('02') # Type '02' means 'Duties and Taxes'
|
|
433
|
+
xml.BillShipper do
|
|
434
|
+
xml.AccountNumber(options[:origin_account])
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
if options[:international]
|
|
442
|
+
unless options[:return]
|
|
443
|
+
build_location_node(xml, 'SoldTo', options[:sold_to] || destination, options)
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
if origin.country_code(:alpha2) == 'US' && ['CA', 'PR'].include?(destination.country_code(:alpha2))
|
|
447
|
+
# Required for shipments from the US to Puerto Rico or Canada
|
|
448
|
+
xml.InvoiceLineTotal do
|
|
449
|
+
total_value = packages.inject(0) {|sum, package| sum + (package.value || 0)}
|
|
450
|
+
xml.MonetaryValue(total_value)
|
|
451
|
+
end
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
contents_description = packages.map {|p| p.options[:description]}.compact.join(',')
|
|
455
|
+
unless contents_description.empty?
|
|
456
|
+
xml.Description(contents_description)
|
|
457
|
+
end
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
if options[:return]
|
|
461
|
+
xml.ReturnService do
|
|
462
|
+
xml.Code(options[:return_service_code])
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
xml.ShipmentServiceOptions do
|
|
467
|
+
if delivery_confirmation = options[:delivery_confirmation]
|
|
468
|
+
xml.DeliveryConfirmation do
|
|
469
|
+
xml.DCISType(SHIPMENT_DELIVERY_CONFIRMATION_CODES[delivery_confirmation])
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
if options[:international]
|
|
474
|
+
build_international_forms(xml, origin, destination, packages, options)
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
# A request may specify multiple packages.
|
|
479
|
+
packages.each do |package|
|
|
480
|
+
build_package_node(xml, package, options)
|
|
481
|
+
end
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
# Supported label formats:
|
|
485
|
+
# GIF, EPL, ZPL, STARPL and SPL
|
|
486
|
+
label_format = options[:label_format] ? options[:label_format].upcase : 'GIF'
|
|
487
|
+
label_size = options[:label_size] ? options[:label_size] : [4, 6]
|
|
488
|
+
|
|
489
|
+
xml.LabelSpecification do
|
|
490
|
+
xml.LabelStockSize do
|
|
491
|
+
xml.Height(label_size[0])
|
|
492
|
+
xml.Width(label_size[1])
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
xml.LabelPrintMethod do
|
|
496
|
+
xml.Code(label_format)
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
# API requires these only if returning a GIF formated label
|
|
500
|
+
if label_format == 'GIF'
|
|
501
|
+
xml.HTTPUserAgent('Mozilla/4.5')
|
|
502
|
+
xml.LabelImageFormat(label_format) do
|
|
503
|
+
xml.Code(label_format)
|
|
504
|
+
end
|
|
505
|
+
end
|
|
506
|
+
end
|
|
507
|
+
end
|
|
508
|
+
end
|
|
509
|
+
xml_builder.to_xml
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
def build_delivery_dates_request(origin, destination, packages, pickup_date, options={})
|
|
513
|
+
xml_builder = Nokogiri::XML::Builder.new do |xml|
|
|
514
|
+
|
|
515
|
+
xml.TimeInTransitRequest do
|
|
516
|
+
xml.Request do
|
|
517
|
+
xml.RequestAction('TimeInTransit')
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
build_address_artifact_format_location(xml, 'TransitFrom', origin)
|
|
521
|
+
build_address_artifact_format_location(xml, 'TransitTo', destination)
|
|
522
|
+
|
|
523
|
+
xml.ShipmentWeight do
|
|
524
|
+
xml.UnitOfMeasurement do
|
|
525
|
+
xml.Code(options[:imperial] ? 'LBS' : 'KGS')
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
value = packages.inject(0) do |sum, package|
|
|
529
|
+
sum + (options[:imperial] ? package.lbs.to_f : package.kgs.to_f )
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
xml.Weight([value.round(3), 0.1].max)
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
xml.InvoiceLineTotal do
|
|
536
|
+
xml.CurrencyCode('USD')
|
|
537
|
+
total_value = packages.inject(0) {|sum, package| sum + package.value}
|
|
538
|
+
xml.MonetaryValue(total_value)
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
xml.PickupDate(pickup_date.strftime('%Y%m%d'))
|
|
542
|
+
end
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
xml_builder.to_xml
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
def build_void_request(tracking)
|
|
549
|
+
xml_builder = Nokogiri::XML::Builder.new do |xml|
|
|
550
|
+
xml.VoidShipmentRequest do
|
|
551
|
+
xml.Request do
|
|
552
|
+
xml.RequestAction('Void')
|
|
553
|
+
end
|
|
554
|
+
xml.ShipmentIdentificationNumber(tracking)
|
|
555
|
+
end
|
|
556
|
+
end
|
|
557
|
+
xml_builder.to_xml
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
def build_international_forms(xml, origin, destination, packages, options)
|
|
561
|
+
if options[:paperless_invoice]
|
|
562
|
+
xml.InternationalForms do
|
|
563
|
+
xml.FormType('01') # 01 is "Invoice"
|
|
564
|
+
xml.InvoiceDate(options[:invoice_date] || Date.today.strftime('%Y%m%d'))
|
|
565
|
+
xml.ReasonForExport(options[:reason_for_export] || 'SALE')
|
|
566
|
+
xml.CurrencyCode(options[:currency_code] || 'USD')
|
|
567
|
+
|
|
568
|
+
if options[:terms_of_shipment]
|
|
569
|
+
xml.TermsOfShipment(options[:terms_of_shipment])
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
packages.each do |package|
|
|
573
|
+
xml.Product do |xml|
|
|
574
|
+
xml.Description(package.options[:description])
|
|
575
|
+
xml.CommodityCode(package.options[:commodity_code])
|
|
576
|
+
xml.OriginCountryCode(origin.country_code(:alpha2))
|
|
577
|
+
xml.Unit do |xml|
|
|
578
|
+
xml.Value(package.value / (package.options[:item_count] || 1))
|
|
579
|
+
xml.Number((package.options[:item_count] || 1))
|
|
580
|
+
xml.UnitOfMeasurement do |xml|
|
|
581
|
+
# NMB = number. You can specify units in barrels, boxes, etc. Codes are in the api docs.
|
|
582
|
+
xml.Code(package.options[:unit_of_item_count] || 'NMB')
|
|
583
|
+
end
|
|
584
|
+
end
|
|
585
|
+
end
|
|
586
|
+
end
|
|
587
|
+
end
|
|
588
|
+
end
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
def build_accept_request(digest, options = {})
|
|
592
|
+
xml_builder = Nokogiri::XML::Builder.new do |xml|
|
|
593
|
+
xml.ShipmentAcceptRequest do
|
|
594
|
+
xml.Request do
|
|
595
|
+
xml.RequestAction('ShipAccept')
|
|
596
|
+
end
|
|
597
|
+
xml.ShipmentDigest(digest)
|
|
598
|
+
end
|
|
599
|
+
end
|
|
600
|
+
xml_builder.to_xml
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
def build_tracking_request(tracking_number, options = {})
|
|
604
|
+
xml_builder = Nokogiri::XML::Builder.new do |xml|
|
|
605
|
+
xml.TrackRequest do
|
|
606
|
+
xml.TrackingOption(options[:tracking_option]) if options[:tracking_option]
|
|
607
|
+
xml.Request do
|
|
608
|
+
xml.RequestAction('Track')
|
|
609
|
+
xml.RequestOption('1')
|
|
610
|
+
end
|
|
611
|
+
xml.TrackingNumber(tracking_number.to_s)
|
|
612
|
+
xml.TrackingOption('03') if options[:mail_innovations]
|
|
613
|
+
end
|
|
614
|
+
end
|
|
615
|
+
xml_builder.to_xml
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
def build_location_node(xml, name, location, options = {})
|
|
619
|
+
# not implemented: * Shipment/Shipper/Name element
|
|
620
|
+
# * Shipment/(ShipTo|ShipFrom)/CompanyName element
|
|
621
|
+
# * Shipment/(Shipper|ShipTo|ShipFrom)/AttentionName element
|
|
622
|
+
# * Shipment/(Shipper|ShipTo|ShipFrom)/TaxIdentificationNumber element
|
|
623
|
+
xml.public_send(name) do
|
|
624
|
+
if shipper_name = (location.name || location.company_name || options[:origin_name])
|
|
625
|
+
xml.Name(shipper_name)
|
|
626
|
+
end
|
|
627
|
+
xml.PhoneNumber(location.phone.gsub(/[^\d]/, '')) unless location.phone.blank?
|
|
628
|
+
xml.FaxNumber(location.fax.gsub(/[^\d]/, '')) unless location.fax.blank?
|
|
629
|
+
|
|
630
|
+
if name == 'Shipper' and (origin_account = options[:origin_account] || @options[:origin_account])
|
|
631
|
+
xml.ShipperNumber(origin_account)
|
|
632
|
+
elsif name == 'ShipTo' and (destination_account = options[:destination_account] || @options[:destination_account])
|
|
633
|
+
xml.ShipperAssignedIdentificationNumber(destination_account)
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
if name = (location.company_name || location.name || options[:origin_name])
|
|
637
|
+
xml.CompanyName(name)
|
|
638
|
+
end
|
|
639
|
+
|
|
640
|
+
if phone = location.phone
|
|
641
|
+
xml.PhoneNumber(phone)
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
if attn = location.name
|
|
645
|
+
xml.AttentionName(attn)
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
xml.Address do
|
|
649
|
+
xml.AddressLine1(location.address1) unless location.address1.blank?
|
|
650
|
+
xml.AddressLine2(location.address2) unless location.address2.blank?
|
|
651
|
+
xml.AddressLine3(location.address3) unless location.address3.blank?
|
|
652
|
+
xml.City(location.city) unless location.city.blank?
|
|
653
|
+
xml.StateProvinceCode(location.province) unless location.province.blank?
|
|
654
|
+
# StateProvinceCode required for negotiated rates but not otherwise, for some reason
|
|
655
|
+
xml.PostalCode(location.postal_code) unless location.postal_code.blank?
|
|
656
|
+
xml.CountryCode(location.country_code(:alpha2)) unless location.country_code(:alpha2).blank?
|
|
657
|
+
xml.ResidentialAddressIndicator(true) unless location.commercial? # the default should be that UPS returns residential rates for destinations that it doesn't know about
|
|
658
|
+
# not implemented: Shipment/(Shipper|ShipTo|ShipFrom)/Address/ResidentialAddressIndicator element
|
|
659
|
+
end
|
|
660
|
+
end
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
def build_address_artifact_format_location(xml, name, location)
|
|
664
|
+
xml.public_send(name) do
|
|
665
|
+
xml.AddressArtifactFormat do
|
|
666
|
+
xml.PoliticalDivision2(location.city)
|
|
667
|
+
xml.PoliticalDivision1(location.province)
|
|
668
|
+
xml.CountryCode(location.country_code(:alpha2))
|
|
669
|
+
xml.PostcodePrimaryLow(location.postal_code)
|
|
670
|
+
xml.ResidentialAddressIndicator(true) unless location.commercial?
|
|
671
|
+
end
|
|
672
|
+
end
|
|
673
|
+
end
|
|
674
|
+
|
|
675
|
+
def build_package_node(xml, package, options = {})
|
|
676
|
+
xml.Package do
|
|
677
|
+
# not implemented: * Shipment/Package/PackagingType element
|
|
678
|
+
|
|
679
|
+
#return requires description
|
|
680
|
+
if options[:return]
|
|
681
|
+
contents_description = package.options[:description]
|
|
682
|
+
xml.Description(contents_description) if contents_description
|
|
683
|
+
end
|
|
684
|
+
|
|
685
|
+
xml.PackagingType do
|
|
686
|
+
xml.Code('02')
|
|
687
|
+
end
|
|
688
|
+
|
|
689
|
+
xml.Dimensions do
|
|
690
|
+
xml.UnitOfMeasurement do
|
|
691
|
+
xml.Code(options[:imperial] ? 'IN' : 'CM')
|
|
692
|
+
end
|
|
693
|
+
[:length, :width, :height].each do |axis|
|
|
694
|
+
value = ((options[:imperial] ? package.inches(axis) : package.cm(axis)).to_f * 1000).round / 1000.0 # 3 decimals
|
|
695
|
+
xml.public_send(axis.to_s.capitalize, [value, 0.1].max)
|
|
696
|
+
end
|
|
697
|
+
end
|
|
698
|
+
|
|
699
|
+
xml.PackageWeight do
|
|
700
|
+
if (options[:service] || options[:service_code]) == DEFAULT_SERVICE_NAME_TO_CODE["UPS SurePost (USPS) < 1lb"]
|
|
701
|
+
# SurePost < 1lb uses OZS, not LBS
|
|
702
|
+
code = options[:imperial] ? 'OZS' : 'KGS'
|
|
703
|
+
weight = options[:imperial] ? package.oz : package.kgs
|
|
704
|
+
else
|
|
705
|
+
code = options[:imperial] ? 'LBS' : 'KGS'
|
|
706
|
+
weight = options[:imperial] ? package.lbs : package.kgs
|
|
707
|
+
end
|
|
708
|
+
xml.UnitOfMeasurement do
|
|
709
|
+
xml.Code(code)
|
|
710
|
+
end
|
|
711
|
+
|
|
712
|
+
value = ((weight).to_f * 1000).round / 1000.0 # 3 decimals
|
|
713
|
+
xml.Weight([value, 0.1].max)
|
|
714
|
+
end
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
Array(package.options[:reference_numbers]).each do |reference_number_info|
|
|
718
|
+
xml.ReferenceNumber do
|
|
719
|
+
xml.Code(reference_number_info[:code] || "")
|
|
720
|
+
xml.Value(reference_number_info[:value])
|
|
721
|
+
end
|
|
722
|
+
end
|
|
723
|
+
|
|
724
|
+
xml.PackageServiceOptions do
|
|
725
|
+
if delivery_confirmation = package.options[:delivery_confirmation]
|
|
726
|
+
xml.DeliveryConfirmation do
|
|
727
|
+
xml.DCISType(PACKAGE_DELIVERY_CONFIRMATION_CODES[delivery_confirmation])
|
|
728
|
+
end
|
|
729
|
+
end
|
|
730
|
+
|
|
731
|
+
if dry_ice = package.options[:dry_ice]
|
|
732
|
+
xml.DryIce do
|
|
733
|
+
xml.RegulationSet(dry_ice[:regulation_set] || 'CFR')
|
|
734
|
+
xml.DryIceWeight do
|
|
735
|
+
xml.UnitOfMeasurement do
|
|
736
|
+
xml.Code(options[:imperial] ? 'LBS' : 'KGS')
|
|
737
|
+
end
|
|
738
|
+
# Cannot be more than package weight.
|
|
739
|
+
# Should be more than 0.0.
|
|
740
|
+
# Valid characters are 0-9 and .(Decimal point).
|
|
741
|
+
# Limit to 1 digit after the decimal. The maximum length
|
|
742
|
+
# of the field is 5 including . and can hold up
|
|
743
|
+
# to 1 decimal place.
|
|
744
|
+
xml.Weight(dry_ice[:weight])
|
|
745
|
+
end
|
|
746
|
+
end
|
|
747
|
+
end
|
|
748
|
+
end
|
|
749
|
+
|
|
750
|
+
# not implemented: * Shipment/Package/LargePackageIndicator element
|
|
751
|
+
# * Shipment/Package/AdditionalHandling element
|
|
752
|
+
end
|
|
753
|
+
end
|
|
754
|
+
|
|
755
|
+
def build_document(xml, expected_root_tag)
|
|
756
|
+
document = Nokogiri.XML(xml)
|
|
757
|
+
if document.root.nil? || document.root.name != expected_root_tag
|
|
758
|
+
raise ReactiveShipping::ResponseContentError.new(StandardError.new('Invalid document'), xml)
|
|
759
|
+
end
|
|
760
|
+
document
|
|
761
|
+
rescue Nokogiri::XML::SyntaxError => e
|
|
762
|
+
raise ReactiveShipping::ResponseContentError.new(e, xml)
|
|
763
|
+
end
|
|
764
|
+
|
|
765
|
+
def parse_rate_response(origin, destination, packages, response, options = {})
|
|
766
|
+
xml = build_document(response, 'RatingServiceSelectionResponse')
|
|
767
|
+
success = response_success?(xml)
|
|
768
|
+
message = response_message(xml)
|
|
769
|
+
|
|
770
|
+
if success
|
|
771
|
+
rate_estimates = xml.root.css('> RatedShipment').map do |rated_shipment|
|
|
772
|
+
service_code = rated_shipment.at('Service/Code').text
|
|
773
|
+
days_to_delivery = rated_shipment.at('GuaranteedDaysToDelivery').text.to_i
|
|
774
|
+
days_to_delivery = nil if days_to_delivery == 0
|
|
775
|
+
RateEstimate.new(origin, destination, @@name, service_name_for(origin, service_code),
|
|
776
|
+
:total_price => rated_shipment.at('TotalCharges/MonetaryValue').text.to_f,
|
|
777
|
+
:insurance_price => rated_shipment.at('ServiceOptionsCharges/MonetaryValue').text.to_f,
|
|
778
|
+
:currency => rated_shipment.at('TotalCharges/CurrencyCode').text,
|
|
779
|
+
:service_code => service_code,
|
|
780
|
+
:packages => packages,
|
|
781
|
+
:delivery_range => [timestamp_from_business_day(days_to_delivery)],
|
|
782
|
+
:negotiated_rate => rated_shipment.at('NegotiatedRates/NetSummaryCharges/GrandTotal/MonetaryValue').try(:text).to_f
|
|
783
|
+
)
|
|
784
|
+
end
|
|
785
|
+
end
|
|
786
|
+
RateResponse.new(success, message, Hash.from_xml(response).values.first, :rates => rate_estimates, :xml => response, :request => last_request)
|
|
787
|
+
end
|
|
788
|
+
|
|
789
|
+
def parse_tracking_response(response, options = {})
|
|
790
|
+
xml = build_document(response, 'TrackResponse')
|
|
791
|
+
success = response_success?(xml)
|
|
792
|
+
message = response_message(xml)
|
|
793
|
+
|
|
794
|
+
if success
|
|
795
|
+
delivery_signature = nil
|
|
796
|
+
exception_event, scheduled_delivery_date, actual_delivery_date = nil
|
|
797
|
+
delivered, exception = false
|
|
798
|
+
shipment_events = []
|
|
799
|
+
|
|
800
|
+
first_shipment = xml.root.at('Shipment')
|
|
801
|
+
first_package = first_shipment.at('Package')
|
|
802
|
+
tracking_number = first_shipment.at_xpath('ShipmentIdentificationNumber | Package/TrackingNumber').text
|
|
803
|
+
|
|
804
|
+
# Build status hash
|
|
805
|
+
status_nodes = first_package.css('Activity > Status > StatusType')
|
|
806
|
+
|
|
807
|
+
# Prefer a delivery node
|
|
808
|
+
status_node = status_nodes.detect { |x| x.at('Code').text == 'D' }
|
|
809
|
+
status_node ||= status_nodes.first
|
|
810
|
+
|
|
811
|
+
status_code = status_node.at('Code').text
|
|
812
|
+
status_description = status_node.at('Description').text
|
|
813
|
+
status = TRACKING_STATUS_CODES[status_code]
|
|
814
|
+
|
|
815
|
+
if status_description =~ /out.*delivery/i
|
|
816
|
+
status = :out_for_delivery
|
|
817
|
+
end
|
|
818
|
+
|
|
819
|
+
origin, destination = %w(Shipper ShipTo).map do |location|
|
|
820
|
+
location_from_address_node(first_shipment.at("#{location}/Address"))
|
|
821
|
+
end
|
|
822
|
+
|
|
823
|
+
# Get scheduled delivery date
|
|
824
|
+
unless status == :delivered
|
|
825
|
+
scheduled_delivery_date_node = first_shipment.at('ScheduledDeliveryDate')
|
|
826
|
+
scheduled_delivery_date_node ||= first_shipment.at('RescheduledDeliveryDate')
|
|
827
|
+
|
|
828
|
+
if scheduled_delivery_date_node
|
|
829
|
+
scheduled_delivery_date = parse_ups_datetime(
|
|
830
|
+
:date => scheduled_delivery_date_node,
|
|
831
|
+
:time => nil
|
|
832
|
+
)
|
|
833
|
+
end
|
|
834
|
+
end
|
|
835
|
+
|
|
836
|
+
activities = first_package.css('> Activity')
|
|
837
|
+
unless activities.empty?
|
|
838
|
+
shipment_events = activities.map do |activity|
|
|
839
|
+
description = activity.at('Status/StatusType/Description').text
|
|
840
|
+
type_code = activity.at('Status/StatusType/Code').text
|
|
841
|
+
zoneless_time = parse_ups_datetime(:time => activity.at('Time'), :date => activity.at('Date'))
|
|
842
|
+
location = location_from_address_node(activity.at('ActivityLocation/Address'))
|
|
843
|
+
ShipmentEvent.new(description, zoneless_time, location, nil, type_code)
|
|
844
|
+
end
|
|
845
|
+
|
|
846
|
+
shipment_events = shipment_events.sort_by(&:time)
|
|
847
|
+
|
|
848
|
+
# UPS will sometimes archive a shipment, stripping all shipment activity except for the delivery
|
|
849
|
+
# event (see test/fixtures/xml/delivered_shipment_without_events_tracking_response.xml for an example).
|
|
850
|
+
# This adds an origin event to the shipment activity in such cases.
|
|
851
|
+
if origin && !(shipment_events.count == 1 && status == :delivered)
|
|
852
|
+
first_event = shipment_events[0]
|
|
853
|
+
origin_event = ShipmentEvent.new(first_event.name, first_event.time, origin, first_event.message, first_event.type_code)
|
|
854
|
+
|
|
855
|
+
if within_same_area?(origin, first_event.location)
|
|
856
|
+
shipment_events[0] = origin_event
|
|
857
|
+
else
|
|
858
|
+
shipment_events.unshift(origin_event)
|
|
859
|
+
end
|
|
860
|
+
end
|
|
861
|
+
|
|
862
|
+
# Has the shipment been delivered?
|
|
863
|
+
if status == :delivered
|
|
864
|
+
delivered_activity = activities.first
|
|
865
|
+
delivery_signature = delivered_activity.at('ActivityLocation/SignedForByName').try(:text)
|
|
866
|
+
if delivered_activity.at('Status/StatusType/Code').text == 'D'
|
|
867
|
+
actual_delivery_date = parse_ups_datetime(:date => delivered_activity.at('Date'), :time => delivered_activity.at('Time'))
|
|
868
|
+
end
|
|
869
|
+
unless destination
|
|
870
|
+
destination = shipment_events[-1].location
|
|
871
|
+
end
|
|
872
|
+
shipment_events[-1] = ShipmentEvent.new(shipment_events.last.name, shipment_events.last.time, destination, shipment_events.last.message, shipment_events.last.type_code)
|
|
873
|
+
end
|
|
874
|
+
end
|
|
875
|
+
|
|
876
|
+
end
|
|
877
|
+
TrackingResponse.new(success, message, Hash.from_xml(response).values.first,
|
|
878
|
+
:carrier => @@name,
|
|
879
|
+
:xml => response,
|
|
880
|
+
:request => last_request,
|
|
881
|
+
:status => status,
|
|
882
|
+
:status_code => status_code,
|
|
883
|
+
:status_description => status_description,
|
|
884
|
+
:delivery_signature => delivery_signature,
|
|
885
|
+
:scheduled_delivery_date => scheduled_delivery_date,
|
|
886
|
+
:actual_delivery_date => actual_delivery_date,
|
|
887
|
+
:shipment_events => shipment_events,
|
|
888
|
+
:delivered => delivered,
|
|
889
|
+
:exception => exception,
|
|
890
|
+
:exception_event => exception_event,
|
|
891
|
+
:origin => origin,
|
|
892
|
+
:destination => destination,
|
|
893
|
+
:tracking_number => tracking_number)
|
|
894
|
+
end
|
|
895
|
+
|
|
896
|
+
def parse_delivery_dates_response(origin, destination, packages, response, options={})
|
|
897
|
+
xml = build_document(response, 'TimeInTransitResponse')
|
|
898
|
+
success = response_success?(xml)
|
|
899
|
+
message = response_message(xml)
|
|
900
|
+
delivery_estimates = []
|
|
901
|
+
|
|
902
|
+
if success
|
|
903
|
+
xml.css('ServiceSummary').each do |service_summary|
|
|
904
|
+
# Translate the Time in Transit Codes to the service codes used elsewhere
|
|
905
|
+
service_name = service_summary.at('Service/Description').text
|
|
906
|
+
service_code = UPS::DEFAULT_SERVICE_NAME_TO_CODE[service_name]
|
|
907
|
+
date = Date.strptime(service_summary.at('EstimatedArrival/Date').text, '%Y-%m-%d')
|
|
908
|
+
business_transit_days = service_summary.at('EstimatedArrival/BusinessTransitDays').text.to_i
|
|
909
|
+
delivery_estimates << DeliveryDateEstimate.new(origin, destination, self.class.class_variable_get(:@@name),
|
|
910
|
+
service_name,
|
|
911
|
+
:service_code => service_code,
|
|
912
|
+
:guaranteed => service_summary.at('Guaranteed/Code').text == 'Y',
|
|
913
|
+
:date => date,
|
|
914
|
+
:business_transit_days => business_transit_days)
|
|
915
|
+
end
|
|
916
|
+
end
|
|
917
|
+
response = DeliveryDateEstimatesResponse.new(success, message, Hash.from_xml(response).values.first, :delivery_estimates => delivery_estimates, :xml => response, :request => last_request)
|
|
918
|
+
end
|
|
919
|
+
|
|
920
|
+
def parse_void_response(response, options={})
|
|
921
|
+
xml = build_document(response, 'VoidShipmentResponse')
|
|
922
|
+
success = response_success?(xml)
|
|
923
|
+
message = response_message(xml)
|
|
924
|
+
if success
|
|
925
|
+
true
|
|
926
|
+
else
|
|
927
|
+
raise ResponseError.new("Void shipment failed with message: #{message}")
|
|
928
|
+
end
|
|
929
|
+
end
|
|
930
|
+
|
|
931
|
+
def location_from_address_node(address)
|
|
932
|
+
return nil unless address
|
|
933
|
+
country = address.at('CountryCode').try(:text)
|
|
934
|
+
country = 'US' if country == 'ZZ' # Sometimes returned by SUREPOST in the US
|
|
935
|
+
Location.new(
|
|
936
|
+
:country => country,
|
|
937
|
+
:postal_code => address.at('PostalCode').try(:text),
|
|
938
|
+
:province => address.at('StateProvinceCode').try(:text),
|
|
939
|
+
:city => address.at('City').try(:text),
|
|
940
|
+
:address1 => address.at('AddressLine1').try(:text),
|
|
941
|
+
:address2 => address.at('AddressLine2').try(:text),
|
|
942
|
+
:address3 => address.at('AddressLine3').try(:text)
|
|
943
|
+
)
|
|
944
|
+
end
|
|
945
|
+
|
|
946
|
+
def parse_ups_datetime(options = {})
|
|
947
|
+
time, date = options[:time].try(:text), options[:date].text
|
|
948
|
+
if time.nil?
|
|
949
|
+
hour, minute, second = 0
|
|
950
|
+
else
|
|
951
|
+
hour, minute, second = time.scan(/\d{2}/)
|
|
952
|
+
end
|
|
953
|
+
year, month, day = date[0..3], date[4..5], date[6..7]
|
|
954
|
+
|
|
955
|
+
Time.utc(year, month, day, hour, minute, second)
|
|
956
|
+
end
|
|
957
|
+
|
|
958
|
+
def response_success?(document)
|
|
959
|
+
document.root.at('Response/ResponseStatusCode').text == '1'
|
|
960
|
+
end
|
|
961
|
+
|
|
962
|
+
def response_message(document)
|
|
963
|
+
status = document.root.at_xpath('Response/ResponseStatusDescription').try(:text)
|
|
964
|
+
desc = document.root.at_xpath('Response/Error/ErrorDescription').try(:text)
|
|
965
|
+
[status, desc].select(&:present?).join(": ").presence || "UPS could not process the request."
|
|
966
|
+
end
|
|
967
|
+
|
|
968
|
+
def response_digest(xml)
|
|
969
|
+
xml.root.at('ShipmentDigest').text
|
|
970
|
+
end
|
|
971
|
+
|
|
972
|
+
def parse_ship_confirm(response)
|
|
973
|
+
build_document(response, 'ShipmentConfirmResponse')
|
|
974
|
+
end
|
|
975
|
+
|
|
976
|
+
def parse_ship_accept(response)
|
|
977
|
+
xml = build_document(response, 'ShipmentAcceptResponse')
|
|
978
|
+
success = response_success?(xml)
|
|
979
|
+
message = response_message(xml)
|
|
980
|
+
|
|
981
|
+
response_info = Hash.from_xml(response).values.first
|
|
982
|
+
packages = response_info["ShipmentResults"]["PackageResults"]
|
|
983
|
+
packages = [packages] if Hash === packages
|
|
984
|
+
labels = packages.map do |package|
|
|
985
|
+
Label.new(package["TrackingNumber"], Base64.decode64(package["LabelImage"]["GraphicImage"]))
|
|
986
|
+
end
|
|
987
|
+
|
|
988
|
+
LabelResponse.new(success, message, response_info, {labels: labels})
|
|
989
|
+
end
|
|
990
|
+
|
|
991
|
+
def commit(action, request, test = false)
|
|
992
|
+
ssl_post("#{test ? TEST_URL : LIVE_URL}/#{RESOURCES[action]}", request)
|
|
993
|
+
end
|
|
994
|
+
|
|
995
|
+
def within_same_area?(origin, location)
|
|
996
|
+
return false unless location
|
|
997
|
+
matching_country_codes = origin.country_code(:alpha2) == location.country_code(:alpha2)
|
|
998
|
+
matching_or_blank_city = location.city.blank? || location.city == origin.city
|
|
999
|
+
matching_country_codes && matching_or_blank_city
|
|
1000
|
+
end
|
|
1001
|
+
|
|
1002
|
+
def service_name_for(origin, code)
|
|
1003
|
+
origin = origin.country_code(:alpha2)
|
|
1004
|
+
|
|
1005
|
+
name = case origin
|
|
1006
|
+
when "CA" then CANADA_ORIGIN_SERVICES[code]
|
|
1007
|
+
when "MX" then MEXICO_ORIGIN_SERVICES[code]
|
|
1008
|
+
when *EU_COUNTRY_CODES then EU_ORIGIN_SERVICES[code]
|
|
1009
|
+
end
|
|
1010
|
+
|
|
1011
|
+
name ||= OTHER_NON_US_ORIGIN_SERVICES[code] unless name == 'US'
|
|
1012
|
+
name || DEFAULT_SERVICES[code]
|
|
1013
|
+
end
|
|
1014
|
+
|
|
1015
|
+
def allow_package_level_reference_numbers(origin, destination)
|
|
1016
|
+
# if the package is US -> US or PR -> PR the only type of reference numbers that are allowed are package-level
|
|
1017
|
+
# Otherwise the only type of reference numbers that are allowed are shipment-level
|
|
1018
|
+
[['US','US'],['PR', 'PR']].include?([origin,destination].map(&:country_code))
|
|
1019
|
+
end
|
|
1020
|
+
|
|
1021
|
+
def handle_delivery_confirmation_options(origin, destination, packages, options)
|
|
1022
|
+
if package_level_delivery_confirmation?(origin, destination)
|
|
1023
|
+
handle_package_level_delivery_confirmation(origin, destination, packages, options)
|
|
1024
|
+
else
|
|
1025
|
+
handle_shipment_level_delivery_confirmation(origin, destination, packages, options)
|
|
1026
|
+
end
|
|
1027
|
+
end
|
|
1028
|
+
|
|
1029
|
+
def handle_package_level_delivery_confirmation(origin, destination, packages, options)
|
|
1030
|
+
packages.each do |package|
|
|
1031
|
+
# Transfer shipment-level option to package with no specified delivery_confirmation
|
|
1032
|
+
package.options[:delivery_confirmation] = options[:delivery_confirmation] unless package.options[:delivery_confirmation]
|
|
1033
|
+
|
|
1034
|
+
# Assert that option is valid
|
|
1035
|
+
if package.options[:delivery_confirmation] && !PACKAGE_DELIVERY_CONFIRMATION_CODES[package.options[:delivery_confirmation]]
|
|
1036
|
+
raise "Invalid delivery_confirmation option on package: '#{package.options[:delivery_confirmation]}'. Use a key from PACKAGE_DELIVERY_CONFIRMATION_CODES"
|
|
1037
|
+
end
|
|
1038
|
+
end
|
|
1039
|
+
options.delete(:delivery_confirmation)
|
|
1040
|
+
end
|
|
1041
|
+
|
|
1042
|
+
def handle_shipment_level_delivery_confirmation(origin, destination, packages, options)
|
|
1043
|
+
if packages.any? { |p| p.options[:delivery_confirmation] }
|
|
1044
|
+
raise "origin/destination pair does not support package level delivery_confirmation options"
|
|
1045
|
+
end
|
|
1046
|
+
|
|
1047
|
+
if options[:delivery_confirmation] && !SHIPMENT_DELIVERY_CONFIRMATION_CODES[options[:delivery_confirmation]]
|
|
1048
|
+
raise "Invalid delivery_confirmation option: '#{options[:delivery_confirmation]}'. Use a key from SHIPMENT_DELIVERY_CONFIRMATION_CODES"
|
|
1049
|
+
end
|
|
1050
|
+
end
|
|
1051
|
+
|
|
1052
|
+
# For certain origin/destination pairs, UPS allows each package in a shipment to have a specified delivery_confirmation option
|
|
1053
|
+
# otherwise the delivery_confirmation option must be specified on the entire shipment.
|
|
1054
|
+
# See Appendix P of UPS Shipping Package XML Developers Guide for the rules on which the logic below is based.
|
|
1055
|
+
def package_level_delivery_confirmation?(origin, destination)
|
|
1056
|
+
origin.country_code == destination.country_code ||
|
|
1057
|
+
[['US','PR'], ['PR','US']].include?([origin,destination].map(&:country_code))
|
|
1058
|
+
end
|
|
1059
|
+
end
|
|
1060
|
+
end
|