friendly_shipping 0.8.0 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.circleci/config.yml +1 -1
- data/.env.template +7 -0
- data/.env.test +4 -0
- data/.env.test.local.template +6 -0
- data/.gitignore +1 -0
- data/.rubocop-relaxed.yml +7 -23
- data/.rubocop.yml +11 -2
- data/.rubocop_todo.yml +21 -0
- data/.yardopts +6 -0
- data/CHANGELOG.md +82 -0
- data/Gemfile +17 -0
- data/friendly_shipping.gemspec +4 -13
- data/lib/friendly_shipping/api_error.rb +6 -4
- data/lib/friendly_shipping/api_error_handler.rb +10 -6
- data/lib/friendly_shipping/api_failure.rb +4 -0
- data/lib/friendly_shipping/api_result.rb +16 -4
- data/lib/friendly_shipping/carrier.rb +28 -8
- data/lib/friendly_shipping/http_client.rb +25 -2
- data/lib/friendly_shipping/inflections.rb +10 -0
- data/lib/friendly_shipping/item_options.rb +3 -0
- data/lib/friendly_shipping/label.rb +41 -20
- data/lib/friendly_shipping/package_options.rb +21 -2
- data/lib/friendly_shipping/rate.rb +50 -15
- data/lib/friendly_shipping/request.rb +23 -7
- data/lib/friendly_shipping/response.rb +21 -6
- data/lib/friendly_shipping/services/rl/api_error.rb +33 -0
- data/lib/friendly_shipping/services/rl/bol_options.rb +107 -0
- data/lib/friendly_shipping/services/rl/bol_packages_serializer.rb +32 -0
- data/lib/friendly_shipping/services/rl/bol_structures_serializer.rb +31 -0
- data/lib/friendly_shipping/services/rl/item_options.rb +38 -0
- data/lib/friendly_shipping/services/rl/package_options.rb +40 -0
- data/lib/friendly_shipping/services/rl/parse_create_bol_response.rb +46 -0
- data/lib/friendly_shipping/services/rl/parse_invoice_response.rb +50 -0
- data/lib/friendly_shipping/services/rl/parse_print_bol_response.rb +47 -0
- data/lib/friendly_shipping/services/rl/parse_print_shipping_labels_response.rb +47 -0
- data/lib/friendly_shipping/services/rl/parse_rate_quote_response.rb +66 -0
- data/lib/friendly_shipping/services/rl/parse_transit_times_response.rb +76 -0
- data/lib/friendly_shipping/services/rl/rate_quote_options.rb +86 -0
- data/lib/friendly_shipping/services/rl/rate_quote_packages_serializer.rb +54 -0
- data/lib/friendly_shipping/services/rl/rate_quote_structures_serializer.rb +53 -0
- data/lib/friendly_shipping/services/rl/serialize_create_bol_request.rb +86 -0
- data/lib/friendly_shipping/services/rl/serialize_location.rb +46 -0
- data/lib/friendly_shipping/services/rl/serialize_rate_quote_request.rb +69 -0
- data/lib/friendly_shipping/services/rl/serialize_transit_times_request.rb +38 -0
- data/lib/friendly_shipping/services/rl/shipment_document.rb +40 -0
- data/lib/friendly_shipping/services/rl/shipment_information.rb +41 -0
- data/lib/friendly_shipping/services/rl/shipment_options.rb +50 -0
- data/lib/friendly_shipping/services/rl/shipping_methods.rb +28 -0
- data/lib/friendly_shipping/services/rl/structure_options.rb +13 -0
- data/lib/friendly_shipping/services/rl.rb +204 -0
- data/lib/friendly_shipping/services/ship_engine/api_error.rb +33 -0
- data/lib/friendly_shipping/services/ship_engine/customs_items_serializer.rb +36 -0
- data/lib/friendly_shipping/services/ship_engine/label_customs_options.rb +10 -7
- data/lib/friendly_shipping/services/ship_engine/label_item_options.rb +10 -5
- data/lib/friendly_shipping/services/ship_engine/label_options.rb +31 -14
- data/lib/friendly_shipping/services/ship_engine/label_package_options.rb +18 -11
- data/lib/friendly_shipping/services/ship_engine/parse_address_validation_response.rb +77 -0
- data/lib/friendly_shipping/services/ship_engine/parse_carrier_response.rb +9 -0
- data/lib/friendly_shipping/services/ship_engine/parse_label_response.rb +4 -0
- data/lib/friendly_shipping/services/ship_engine/{parse_rate_estimate_response.rb → parse_rate_estimates_response.rb} +26 -6
- data/lib/friendly_shipping/services/ship_engine/parse_rates_response.rb +101 -0
- data/lib/friendly_shipping/services/ship_engine/parse_void_response.rb +4 -0
- data/lib/friendly_shipping/services/ship_engine/rate_estimates_options.rb +17 -4
- data/lib/friendly_shipping/services/ship_engine/rates_item_options.rb +28 -0
- data/lib/friendly_shipping/services/ship_engine/rates_options.rb +61 -0
- data/lib/friendly_shipping/services/ship_engine/rates_package_options.rb +20 -0
- data/lib/friendly_shipping/services/ship_engine/serialize_address_residential_indicator.rb +27 -0
- data/lib/friendly_shipping/services/ship_engine/serialize_address_validation_request.rb +31 -0
- data/lib/friendly_shipping/services/ship_engine/serialize_label_shipment.rb +22 -27
- data/lib/friendly_shipping/services/ship_engine/serialize_rate_estimate_request.rb +41 -16
- data/lib/friendly_shipping/services/ship_engine/serialize_rates_request.rb +126 -0
- data/lib/friendly_shipping/services/ship_engine.rb +94 -21
- data/lib/friendly_shipping/services/ship_engine_ltl/api_error.rb +12 -0
- data/lib/friendly_shipping/services/ship_engine_ltl/item_options.rb +50 -0
- data/lib/friendly_shipping/services/ship_engine_ltl/package_options.rb +50 -0
- data/lib/friendly_shipping/services/ship_engine_ltl/parse_carrier_response.rb +53 -0
- data/lib/friendly_shipping/services/ship_engine_ltl/parse_quote_response.rb +82 -0
- data/lib/friendly_shipping/services/ship_engine_ltl/quote_options.rb +52 -0
- data/lib/friendly_shipping/services/ship_engine_ltl/serialize_packages.rb +46 -0
- data/lib/friendly_shipping/services/ship_engine_ltl/serialize_quote_request.rb +143 -0
- data/lib/friendly_shipping/services/ship_engine_ltl/serialize_structures.rb +42 -0
- data/lib/friendly_shipping/services/ship_engine_ltl/shipment_options.rb +47 -0
- data/lib/friendly_shipping/services/ship_engine_ltl/structure_options.rb +17 -0
- data/lib/friendly_shipping/services/ship_engine_ltl.rb +153 -0
- data/lib/friendly_shipping/services/tforce_freight/access_token.rb +43 -0
- data/lib/friendly_shipping/services/tforce_freight/api_error.rb +42 -0
- data/lib/friendly_shipping/services/tforce_freight/bol_options.rb +180 -0
- data/lib/friendly_shipping/services/tforce_freight/document_options.rb +100 -0
- data/lib/friendly_shipping/services/tforce_freight/generate_commodity_information.rb +92 -0
- data/lib/friendly_shipping/services/tforce_freight/generate_create_bol_request_hash.rb +165 -0
- data/lib/friendly_shipping/services/tforce_freight/generate_document_options_hash.rb +36 -0
- data/lib/friendly_shipping/services/tforce_freight/generate_handling_units_hash.rb +51 -0
- data/lib/friendly_shipping/services/tforce_freight/generate_location_hash.rb +25 -0
- data/lib/friendly_shipping/services/tforce_freight/generate_pickup_request_hash.rb +113 -0
- data/lib/friendly_shipping/services/tforce_freight/generate_rates_request_hash.rb +65 -0
- data/lib/friendly_shipping/services/tforce_freight/generate_reference_hash.rb +28 -0
- data/lib/friendly_shipping/services/tforce_freight/item_options.rb +93 -0
- data/lib/friendly_shipping/services/tforce_freight/package_options.rb +121 -0
- data/lib/friendly_shipping/services/tforce_freight/parse_create_bol_response.rb +94 -0
- data/lib/friendly_shipping/services/tforce_freight/parse_pickup_response.rb +45 -0
- data/lib/friendly_shipping/services/tforce_freight/parse_rates_response.rb +58 -0
- data/lib/friendly_shipping/services/tforce_freight/parse_shipment_document.rb +31 -0
- data/lib/friendly_shipping/services/tforce_freight/pickup_options.rb +82 -0
- data/lib/friendly_shipping/services/tforce_freight/rates_item_options.rb +12 -0
- data/lib/friendly_shipping/services/tforce_freight/rates_options.rb +162 -0
- data/lib/friendly_shipping/services/tforce_freight/rates_package_options.rb +12 -0
- data/lib/friendly_shipping/services/tforce_freight/shipment_document.rb +38 -0
- data/lib/friendly_shipping/services/tforce_freight/shipment_information.rb +104 -0
- data/lib/friendly_shipping/services/tforce_freight/shipment_options.rb +49 -0
- data/lib/friendly_shipping/services/tforce_freight/shipping_methods.rb +25 -0
- data/lib/friendly_shipping/services/tforce_freight/structure_options.rb +44 -0
- data/lib/friendly_shipping/services/tforce_freight.rb +202 -0
- data/lib/friendly_shipping/services/ups/label_options.rb +14 -2
- data/lib/friendly_shipping/services/ups/label_package_options.rb +8 -4
- data/lib/friendly_shipping/services/ups/parse_modifier_element.rb +29 -0
- data/lib/friendly_shipping/services/ups/parse_rate_response.rb +12 -0
- data/lib/friendly_shipping/services/ups/parse_time_in_transit_response.rb +1 -1
- data/lib/friendly_shipping/services/ups/rate_estimate_options.rb +14 -1
- data/lib/friendly_shipping/services/ups/serialize_package_node.rb +11 -1
- data/lib/friendly_shipping/services/ups/serialize_rating_service_selection_request.rb +10 -1
- data/lib/friendly_shipping/services/ups/serialize_shipment_accept_request.rb +1 -1
- data/lib/friendly_shipping/services/ups/serialize_shipment_confirm_request.rb +8 -4
- data/lib/friendly_shipping/services/ups/shipping_methods.rb +1 -1
- data/lib/friendly_shipping/services/ups.rb +1 -1
- data/lib/friendly_shipping/services/ups_freight/api_error.rb +8 -5
- data/lib/friendly_shipping/services/ups_freight/generate_commodity_information.rb +65 -19
- data/lib/friendly_shipping/services/ups_freight/generate_freight_rate_request_hash.rb +3 -20
- data/lib/friendly_shipping/services/ups_freight/generate_freight_ship_request_hash.rb +2 -20
- data/lib/friendly_shipping/services/ups_freight/generate_handling_units_hash.rb +54 -0
- data/lib/friendly_shipping/services/ups_freight/generate_location_hash.rb +10 -6
- data/lib/friendly_shipping/services/ups_freight/label_options.rb +36 -10
- data/lib/friendly_shipping/services/ups_freight/label_structure_options.rb +13 -0
- data/lib/friendly_shipping/services/ups_freight/parse_freight_label_response.rb +2 -2
- data/lib/friendly_shipping/services/ups_freight/parse_shipment_document.rb +1 -1
- data/lib/friendly_shipping/services/ups_freight/rates_item_options.rb +18 -9
- data/lib/friendly_shipping/services/ups_freight/rates_options.rb +31 -21
- data/lib/friendly_shipping/services/ups_freight/rates_package_options.rb +79 -8
- data/lib/friendly_shipping/services/ups_freight/rates_structure_options.rb +46 -0
- data/lib/friendly_shipping/services/ups_freight/shipment_information.rb +7 -3
- data/lib/friendly_shipping/services/ups_freight/shipment_options.rb +49 -0
- data/lib/friendly_shipping/services/ups_freight.rb +3 -1
- data/lib/friendly_shipping/services/ups_json/access_token.rb +29 -0
- data/lib/friendly_shipping/services/ups_json/api_error.rb +29 -0
- data/lib/friendly_shipping/services/ups_json/generate_address_classification_payload.rb +29 -0
- data/lib/friendly_shipping/services/ups_json/generate_address_hash.rb +30 -0
- data/lib/friendly_shipping/services/ups_json/generate_labels_payload.rb +211 -0
- data/lib/friendly_shipping/services/ups_json/generate_package_hash.rb +76 -0
- data/lib/friendly_shipping/services/ups_json/generate_rates_payload.rb +86 -0
- data/lib/friendly_shipping/services/ups_json/generate_timings_payload.rb +44 -0
- data/lib/friendly_shipping/services/ups_json/label.rb +20 -0
- data/lib/friendly_shipping/services/ups_json/label_billing_options.rb +41 -0
- data/lib/friendly_shipping/services/ups_json/label_item_options.rb +77 -0
- data/lib/friendly_shipping/services/ups_json/label_options.rb +177 -0
- data/lib/friendly_shipping/services/ups_json/label_package_options.rb +51 -0
- data/lib/friendly_shipping/services/ups_json/parse_address_classification_response.rb +31 -0
- data/lib/friendly_shipping/services/ups_json/parse_json_response.rb +44 -0
- data/lib/friendly_shipping/services/ups_json/parse_labels_response.rb +71 -0
- data/lib/friendly_shipping/services/ups_json/parse_money_hash.rb +128 -0
- data/lib/friendly_shipping/services/ups_json/parse_rate_modifier_hash.rb +28 -0
- data/lib/friendly_shipping/services/ups_json/parse_rates_response.rb +105 -0
- data/lib/friendly_shipping/services/ups_json/parse_timings_response.rb +56 -0
- data/lib/friendly_shipping/services/ups_json/parse_void_response.rb +32 -0
- data/lib/friendly_shipping/services/ups_json/rates_item_options.rb +22 -0
- data/lib/friendly_shipping/services/ups_json/rates_options.rb +113 -0
- data/lib/friendly_shipping/services/ups_json/rates_package_options.rb +17 -0
- data/lib/friendly_shipping/services/ups_json/shipping_methods.rb +111 -0
- data/lib/friendly_shipping/services/ups_json/timings_options.rb +33 -0
- data/lib/friendly_shipping/services/ups_json.rb +216 -0
- data/lib/friendly_shipping/services/usps/choose_package_rate.rb +3 -3
- data/lib/friendly_shipping/services/usps/machinable_package.rb +1 -1
- data/lib/friendly_shipping/services/usps/parse_package_rate.rb +8 -7
- data/lib/friendly_shipping/services/usps/parse_time_in_transit_response.rb +12 -8
- data/lib/friendly_shipping/services/usps/rate_estimate_options.rb +1 -1
- data/lib/friendly_shipping/services/usps/rate_estimate_package_options.rb +6 -1
- data/lib/friendly_shipping/services/usps/serialize_rate_request.rb +2 -2
- data/lib/friendly_shipping/services/usps/shipping_methods.rb +8 -5
- data/lib/friendly_shipping/services/usps.rb +1 -1
- data/lib/friendly_shipping/services/usps_international/parse_package_rate.rb +5 -4
- data/lib/friendly_shipping/services/usps_international/rate_estimate_options.rb +1 -1
- data/lib/friendly_shipping/services/usps_international/serialize_rate_request.rb +3 -3
- data/lib/friendly_shipping/services/usps_international.rb +1 -1
- data/lib/friendly_shipping/services/usps_ship/access_token.rb +37 -0
- data/lib/friendly_shipping/services/usps_ship/api_error.rb +29 -0
- data/lib/friendly_shipping/services/usps_ship/machinable_package.rb +53 -0
- data/lib/friendly_shipping/services/usps_ship/parse_rate_estimates_response.rb +80 -0
- data/lib/friendly_shipping/services/usps_ship/parse_timings_response.rb +80 -0
- data/lib/friendly_shipping/services/usps_ship/rate_estimate_options.rb +45 -0
- data/lib/friendly_shipping/services/usps_ship/rate_estimate_package_options.rb +124 -0
- data/lib/friendly_shipping/services/usps_ship/serialize_rate_estimates_request.rb +55 -0
- data/lib/friendly_shipping/services/usps_ship/shipping_methods.rb +38 -0
- data/lib/friendly_shipping/services/usps_ship/timing_options.rb +9 -0
- data/lib/friendly_shipping/services/usps_ship.rb +199 -0
- data/lib/friendly_shipping/shipment_options.rb +13 -1
- data/lib/friendly_shipping/shipping_method.rb +38 -11
- data/lib/friendly_shipping/structure_options.rb +38 -0
- data/lib/friendly_shipping/timing.rb +42 -7
- data/lib/friendly_shipping/version.rb +1 -1
- data/lib/friendly_shipping.rb +7 -0
- metadata +149 -172
- data/lib/friendly_shipping/services/ship_engine/bad_request.rb +0 -29
- data/lib/friendly_shipping/services/ship_engine/bad_request_handler.rb +0 -33
@@ -5,14 +5,14 @@ module FriendlyShipping
|
|
5
5
|
class Usps
|
6
6
|
class ParsePackageRate
|
7
7
|
# USPS returns all the info about a rate in a long string with a bit of gibberish.
|
8
|
-
ESCAPING_AND_SYMBOLS = /<\S*>
|
8
|
+
ESCAPING_AND_SYMBOLS = /<\S*>/
|
9
9
|
|
10
10
|
# At the beginning of the long String, USPS keeps a copy of its own name. We know we're dealing with
|
11
11
|
# them though, so we can filter that out, too.
|
12
|
-
LEADING_USPS = /^USPS
|
12
|
+
LEADING_USPS = /^USPS /
|
13
13
|
|
14
14
|
# This combines all the things we want to filter out.
|
15
|
-
SERVICE_NAME_SUBSTITUTIONS = /#{ESCAPING_AND_SYMBOLS}|#{LEADING_USPS}
|
15
|
+
SERVICE_NAME_SUBSTITUTIONS = /#{ESCAPING_AND_SYMBOLS}|#{LEADING_USPS}/
|
16
16
|
|
17
17
|
# Often we get a multitude of rates for the same service given some combination of
|
18
18
|
# Box type and (see below) and "Hold for Pickup" service. This creates a regular expression
|
@@ -39,15 +39,15 @@ module FriendlyShipping
|
|
39
39
|
}.map { |k, v| "(?<#{k}>#{v})" }.join("|").freeze
|
40
40
|
|
41
41
|
# We use this for identifying rates that use the Hold for Pickup service.
|
42
|
-
HOLD_FOR_PICKUP = /Hold for Pickup/i
|
42
|
+
HOLD_FOR_PICKUP = /Hold for Pickup/i
|
43
43
|
|
44
44
|
# For most rate options, USPS will return how many business days it takes to deliver this
|
45
45
|
# package in the format "{1,2,3}-Day". We can filter this out using the below Regex.
|
46
|
-
DAYS_TO_DELIVERY = /(?<days>\d)-Day
|
46
|
+
DAYS_TO_DELIVERY = /(?<days>\d)-Day/
|
47
47
|
|
48
48
|
# When delivering to military ZIP codes, we don't actually get a timing estimate, but instead the string
|
49
49
|
# "Military". We use this to indicate that this rate is for a military zip code in the rates' data Hash.
|
50
|
-
MILITARY = /MILITARY/i
|
50
|
+
MILITARY = /MILITARY/i
|
51
51
|
|
52
52
|
# The tags used in the rate node that we get information from.
|
53
53
|
SERVICE_CODE_TAG = 'CLASSID'
|
@@ -94,7 +94,8 @@ module FriendlyShipping
|
|
94
94
|
|
95
95
|
rate_value =
|
96
96
|
if commercial_rate_requested_or_rate_is_zero && commercial_rate_available
|
97
|
-
rate_node.at(COMMERCIAL_RATE_TAG)&.text
|
97
|
+
commercial_rate = rate_node.at(COMMERCIAL_RATE_TAG)&.text.to_d
|
98
|
+
commercial_rate.zero? ? rate_node.at(COMMERCIAL_PLUS_RATE_TAG).text.to_d : commercial_rate
|
98
99
|
else
|
99
100
|
rate_node.at(RATE_TAG).text.to_d
|
100
101
|
end
|
@@ -51,10 +51,10 @@ module FriendlyShipping
|
|
51
51
|
potential_shipping_method.name == MAIL_CLASSES[commitment_node.at('MailClass').text]
|
52
52
|
end
|
53
53
|
commitment_sequence = commitment_node.at('CommitmentSeq').text
|
54
|
-
|
55
|
-
next unless
|
54
|
+
data = COMMITMENT_SEQUENCES[commitment_sequence]
|
55
|
+
next unless data # Sometimes USPS returns an invalid CommitmentSeq
|
56
56
|
|
57
|
-
scheduled_delivery_time =
|
57
|
+
scheduled_delivery_time = data.delete(:commitment_time)
|
58
58
|
scheduled_delivery_date = commitment_node.at('SDD').text
|
59
59
|
parsed_delivery_time = Time.parse("#{scheduled_delivery_date} #{scheduled_delivery_time}")
|
60
60
|
guaranteed = commitment_node.at('IsGuaranteed').text == '1'
|
@@ -64,7 +64,7 @@ module FriendlyShipping
|
|
64
64
|
pickup: effective_acceptance_date,
|
65
65
|
delivery: parsed_delivery_time,
|
66
66
|
guaranteed: guaranteed,
|
67
|
-
|
67
|
+
data: data
|
68
68
|
)
|
69
69
|
end.compact
|
70
70
|
end
|
@@ -80,7 +80,7 @@ module FriendlyShipping
|
|
80
80
|
warning_text = commitment_node.xpath('HFPU//NonExpeditedTransMsg/Msg')&.text
|
81
81
|
warning = warning_text unless warning_text.empty?
|
82
82
|
|
83
|
-
|
83
|
+
data = {
|
84
84
|
commitment: commitment_node.at('SvcStdMsg')&.text,
|
85
85
|
destination_type: NON_EXPEDITED_DESTINATION_TYPES[commitment_node.at('NonExpeditedDestType').text],
|
86
86
|
warning: warning
|
@@ -95,7 +95,7 @@ module FriendlyShipping
|
|
95
95
|
pickup: effective_acceptance_date,
|
96
96
|
delivery: parsed_delivery_time,
|
97
97
|
guaranteed: false,
|
98
|
-
|
98
|
+
data: data
|
99
99
|
)
|
100
100
|
end.compact
|
101
101
|
end
|
@@ -106,10 +106,12 @@ module FriendlyShipping
|
|
106
106
|
# “0” = All Mail Classes
|
107
107
|
# “1” = Priority Mail Express
|
108
108
|
# “2” = Priority Mail
|
109
|
-
# “3” = First
|
109
|
+
# “3” = First-Class - replaced by Ground Advantage (up to 15.999 oz)
|
110
110
|
# “4” = Marketing Mail
|
111
111
|
# “5” = Periodicals
|
112
112
|
# “6” = Package Services
|
113
|
+
# “7” = Parcel Select Ground - replaced by Ground Advantage (1-70 lbs)
|
114
|
+
# “9” = Ground Advantage (1-70 lbs)
|
113
115
|
#
|
114
116
|
# However, no shipping methods really map to "Marketing Mail" or "Periodicals".
|
115
117
|
# This will likely be somewhat more work in the future.
|
@@ -117,7 +119,9 @@ module FriendlyShipping
|
|
117
119
|
'1' => 'Priority Mail Express',
|
118
120
|
'2' => 'Priority Mail',
|
119
121
|
'3' => 'First-Class',
|
120
|
-
'6' => 'Package Services'
|
122
|
+
'6' => 'Package Services',
|
123
|
+
'7' => 'Parcel Select Ground',
|
124
|
+
'9' => 'Ground Advantage'
|
121
125
|
}.freeze
|
122
126
|
|
123
127
|
# This code carries a few details about the shipment:
|
@@ -20,7 +20,7 @@ module FriendlyShipping
|
|
20
20
|
package_options_class: FriendlyShipping::Services::Usps::RateEstimatePackageOptions,
|
21
21
|
**kwargs
|
22
22
|
)
|
23
|
-
super(**kwargs.
|
23
|
+
super(**kwargs.reverse_merge(package_options_class: package_options_class))
|
24
24
|
end
|
25
25
|
end
|
26
26
|
end
|
@@ -42,8 +42,13 @@ module FriendlyShipping
|
|
42
42
|
CONTAINERS.fetch(box_name)
|
43
43
|
end
|
44
44
|
|
45
|
+
# @return [String, nil]
|
45
46
|
def first_class_mail_type_code
|
46
|
-
|
47
|
+
if %i[parcel package_service package_service_retail].include?(first_class_mail_type)
|
48
|
+
warn "[DEPRECATION] First Class `:#{first_class_mail_type}` has been replaced by Ground Advantage."
|
49
|
+
else
|
50
|
+
FIRST_CLASS_MAIL_TYPES[first_class_mail_type]
|
51
|
+
end
|
47
52
|
end
|
48
53
|
|
49
54
|
def service_code
|
@@ -15,7 +15,7 @@ module FriendlyShipping
|
|
15
15
|
# @param [String] login The USPS login code
|
16
16
|
# @param [FriendlyShipping::Services::Usps::RateEstimateOptions] options The options
|
17
17
|
# object to use with this request.
|
18
|
-
# @return Array<
|
18
|
+
# @return [Array<FriendlyShipping::Rate>] A set of Rates that this package may be sent with
|
19
19
|
def call(shipment:, login:, options:)
|
20
20
|
xml_builder = Nokogiri::XML::Builder.new do |xml|
|
21
21
|
xml.RateV4Request('USERID' => login) do
|
@@ -23,7 +23,7 @@ module FriendlyShipping
|
|
23
23
|
package_options = options.options_for_package(package)
|
24
24
|
xml.Package('ID' => index) do
|
25
25
|
xml.Service(package_options.service_code)
|
26
|
-
if package_options.first_class_mail_type
|
26
|
+
if package_options.first_class_mail_type && package_options.first_class_mail_type_code
|
27
27
|
xml.FirstClassMailType(package_options.first_class_mail_type_code)
|
28
28
|
end
|
29
29
|
xml.ZipOrigination(shipment.origin.zip)
|
@@ -23,10 +23,11 @@ module FriendlyShipping
|
|
23
23
|
FIRST_CLASS_MAIL_TYPES = {
|
24
24
|
letter: 'LETTER',
|
25
25
|
flat: 'FLAT',
|
26
|
-
parcel: 'PARCEL',
|
26
|
+
parcel: 'PARCEL', # @deprecated
|
27
27
|
post_card: 'POSTCARD',
|
28
|
-
|
29
|
-
|
28
|
+
large_post_card: 'LARGE POSTCARD',
|
29
|
+
package_service: 'PACKAGE SERVICE', # @deprecated
|
30
|
+
package_service_retail: 'PACKAGE SERVICE RETAIL' # @deprecated
|
30
31
|
}.freeze
|
31
32
|
|
32
33
|
CLASS_IDS = {
|
@@ -35,11 +36,13 @@ module FriendlyShipping
|
|
35
36
|
hold_for_pickup: '2',
|
36
37
|
sunday_holiday_delivery: '23'
|
37
38
|
},
|
38
|
-
priority_mail_cubic: '999'
|
39
|
+
priority_mail_cubic: '999',
|
40
|
+
ground_advantage: '1058'
|
39
41
|
}.freeze
|
40
42
|
|
41
43
|
SHIPPING_METHODS = [
|
42
44
|
['FIRST CLASS', 'First-Class'],
|
45
|
+
['GROUND ADVANTAGE', 'Ground Advantage', CLASS_IDS[:ground_advantage]],
|
43
46
|
['PACKAGE SERVICES', 'Package Services'],
|
44
47
|
['PRIORITY', 'Priority Mail'],
|
45
48
|
['PRIORITY MAIL EXPRESS', 'Priority Mail Express', CLASS_IDS[:priority_mail_express].values],
|
@@ -55,7 +58,7 @@ module FriendlyShipping
|
|
55
58
|
service_code: code,
|
56
59
|
domestic: true,
|
57
60
|
international: false,
|
58
|
-
data: { class_ids: class_ids }
|
61
|
+
data: { class_ids: Array(class_ids) }
|
59
62
|
)
|
60
63
|
end.freeze
|
61
64
|
end
|
@@ -5,14 +5,14 @@ module FriendlyShipping
|
|
5
5
|
class UspsInternational
|
6
6
|
class ParsePackageRate
|
7
7
|
# USPS returns all the info about a rate in a long string with a bit of gibberish.
|
8
|
-
ESCAPING_AND_SYMBOLS = /<\S*>
|
8
|
+
ESCAPING_AND_SYMBOLS = /<\S*>/
|
9
9
|
|
10
10
|
# At the beginning of the long String, USPS keeps a copy of its own name. We know we're dealing with
|
11
11
|
# them though, so we can filter that out, too.
|
12
|
-
LEADING_USPS = /^USPS
|
12
|
+
LEADING_USPS = /^USPS /
|
13
13
|
|
14
14
|
# This combines all the things we want to filter out.
|
15
|
-
SERVICE_NAME_SUBSTITUTIONS = /#{ESCAPING_AND_SYMBOLS}|#{LEADING_USPS}
|
15
|
+
SERVICE_NAME_SUBSTITUTIONS = /#{ESCAPING_AND_SYMBOLS}|#{LEADING_USPS}/
|
16
16
|
|
17
17
|
# Often we get a multitude of rates for the same service given some combination of
|
18
18
|
# Box type and (see below) and "Hold for Pickup" service. This creates a regular expression
|
@@ -47,7 +47,8 @@ module FriendlyShipping
|
|
47
47
|
|
48
48
|
rate_value =
|
49
49
|
if commercial_rate_requested_or_rate_is_zero && commercial_rate_available
|
50
|
-
rate_node.at(COMMERCIAL_RATE_TAG)&.text
|
50
|
+
commercial_rate = rate_node.at(COMMERCIAL_RATE_TAG)&.text.to_d
|
51
|
+
commercial_rate.zero? ? rate_node.at(COMMERCIAL_PLUS_RATE_TAG).text.to_d : commercial_rate
|
51
52
|
else
|
52
53
|
rate_node.at(RATE_TAG).text.to_d
|
53
54
|
end
|
@@ -20,7 +20,7 @@ module FriendlyShipping
|
|
20
20
|
package_options_class: FriendlyShipping::Services::UspsInternational::RateEstimatePackageOptions,
|
21
21
|
**kwargs
|
22
22
|
)
|
23
|
-
super(**kwargs.
|
23
|
+
super(**kwargs.reverse_merge(package_options_class: package_options_class))
|
24
24
|
end
|
25
25
|
end
|
26
26
|
end
|
@@ -13,7 +13,7 @@ module FriendlyShipping
|
|
13
13
|
# @param [String] login The USPS login code
|
14
14
|
# @param [FriendlyShipping::Services::UspsInternational::RateEstimateOptions] options The options
|
15
15
|
# object to use with this request.
|
16
|
-
# @return Array<
|
16
|
+
# @return [Array<FriendlyShipping::Rate>] A set of Rates that this package may be sent with
|
17
17
|
def call(shipment:, login:, options:)
|
18
18
|
xml_builder = Nokogiri::XML::Builder.new do |xml|
|
19
19
|
xml.IntlRateV2Request('USERID' => login) do
|
@@ -56,12 +56,12 @@ module FriendlyShipping
|
|
56
56
|
end
|
57
57
|
|
58
58
|
def ounces_for(package)
|
59
|
-
ounces = package.weight.convert_to(:ounces).value.to_f.round(2).ceil
|
59
|
+
ounces = (package.weight.convert_to(:ounces).value.to_f % 16).round(2).ceil
|
60
60
|
ounces == 16 ? 15.999 : [ounces, 1].max
|
61
61
|
end
|
62
62
|
|
63
63
|
def pounds_for(package)
|
64
|
-
package.weight.convert_to(:pounds).value.to_f.
|
64
|
+
package.weight.convert_to(:pounds).value.to_f.floor
|
65
65
|
end
|
66
66
|
|
67
67
|
def girth(package)
|
@@ -9,7 +9,7 @@ require 'friendly_shipping/services/usps_international/rate_estimate_options'
|
|
9
9
|
module FriendlyShipping
|
10
10
|
module Services
|
11
11
|
class UspsInternational
|
12
|
-
include Dry::Monads
|
12
|
+
include Dry::Monads::Result::Mixin
|
13
13
|
|
14
14
|
attr_reader :test, :login, :client
|
15
15
|
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'jwt'
|
4
|
+
|
5
|
+
module FriendlyShipping
|
6
|
+
module Services
|
7
|
+
class USPSShip
|
8
|
+
# Represents an access token returned by USPS Ship. The access token can be
|
9
|
+
# used to make API requests. Once it expires, a new token must be created.
|
10
|
+
class AccessToken
|
11
|
+
# @return [String] the token's type
|
12
|
+
attr_reader :token_type
|
13
|
+
|
14
|
+
# @return [Integer] the token's expiration
|
15
|
+
attr_reader :expires_in
|
16
|
+
|
17
|
+
# @return [String] the raw JWT token
|
18
|
+
attr_reader :raw_token
|
19
|
+
|
20
|
+
# @param token_type [String] the token's type (typically "Bearer")
|
21
|
+
# @param expires_in [Integer] the token's expiration
|
22
|
+
# @param raw_token [String] the raw JWT token
|
23
|
+
def initialize(token_type:, expires_in:, raw_token:)
|
24
|
+
@token_type = token_type
|
25
|
+
@expires_in = expires_in
|
26
|
+
@raw_token = raw_token
|
27
|
+
end
|
28
|
+
|
29
|
+
# Decodes and returns the raw JWT token.
|
30
|
+
# @return [Array<Hash>] the decoded token
|
31
|
+
def decoded_token
|
32
|
+
@_decoded_token = JWT.decode(raw_token, nil, false)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'friendly_shipping/api_error'
|
4
|
+
|
5
|
+
module FriendlyShipping
|
6
|
+
module Services
|
7
|
+
class USPSShip
|
8
|
+
class ApiError < FriendlyShipping::ApiError
|
9
|
+
# @param [RestClient::Exception] cause
|
10
|
+
def initialize(cause)
|
11
|
+
super(cause, parse_message(cause))
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
# @param [RestClient::Exception] error
|
17
|
+
# @return [String]
|
18
|
+
def parse_message(error)
|
19
|
+
return error.message unless error.response
|
20
|
+
|
21
|
+
parsed_json = JSON.parse(error.response.body)
|
22
|
+
parsed_json.dig("error", "message")
|
23
|
+
rescue JSON::ParserError, KeyError => _e
|
24
|
+
nil
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FriendlyShipping
|
4
|
+
module Services
|
5
|
+
class USPSShip
|
6
|
+
# USPS has certain size and weight requirements for packages to be considered
|
7
|
+
# machinable. Machinable packages are generally less expensive to ship.
|
8
|
+
# @see https://pe.usps.com/BusinessMail101?ViewName=Parcels
|
9
|
+
#
|
10
|
+
class MachinablePackage
|
11
|
+
# @return [Physical::Package]
|
12
|
+
attr_reader :package
|
13
|
+
|
14
|
+
MIN_LENGTH = Measured::Length(6, :inches)
|
15
|
+
MIN_WIDTH = Measured::Length(3, :inches)
|
16
|
+
MIN_HEIGHT = Measured::Length(0.25, :inches)
|
17
|
+
|
18
|
+
MAX_LENGTH = Measured::Length(27, :inches)
|
19
|
+
MAX_WIDTH = Measured::Length(17, :inches)
|
20
|
+
MAX_HEIGHT = Measured::Length(17, :inches)
|
21
|
+
|
22
|
+
MAX_WEIGHT = Measured::Weight(25, :pounds)
|
23
|
+
|
24
|
+
# @param package [Physical::Package]
|
25
|
+
def initialize(package)
|
26
|
+
@package = package
|
27
|
+
end
|
28
|
+
|
29
|
+
# @return [Boolean]
|
30
|
+
def machinable?
|
31
|
+
at_least_minimum && at_most_maximum
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
# @return [Boolean]
|
37
|
+
def at_least_minimum
|
38
|
+
package.length >= MIN_LENGTH &&
|
39
|
+
package.width >= MIN_WIDTH &&
|
40
|
+
package.height >= MIN_HEIGHT
|
41
|
+
end
|
42
|
+
|
43
|
+
# @return [Boolean]
|
44
|
+
def at_most_maximum
|
45
|
+
package.length <= MAX_LENGTH &&
|
46
|
+
package.width <= MAX_WIDTH &&
|
47
|
+
package.height <= MAX_HEIGHT &&
|
48
|
+
package.weight <= MAX_WEIGHT
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FriendlyShipping
|
4
|
+
module Services
|
5
|
+
class USPSShip
|
6
|
+
class ParseRateEstimatesResponse
|
7
|
+
extend Dry::Monads::Result::Mixin
|
8
|
+
CURRENCY = Money::Currency.new('USD').freeze
|
9
|
+
|
10
|
+
class << self
|
11
|
+
# Parse a rate estimates response.
|
12
|
+
#
|
13
|
+
# @param request [Request] the request that was used to obtain this response
|
14
|
+
# @param response [Response] the response that USPS returned
|
15
|
+
# @return [Success<ApiResult<Array<Rate>>>, Failure<ApiFailure<String>>]
|
16
|
+
def call(request:, response:)
|
17
|
+
rates = JSON.parse(response.body)['rates'].map do |rate|
|
18
|
+
shipping_method = SHIPPING_METHODS.detect { |sm| sm.service_code == rate['mailClass'] }
|
19
|
+
amounts = { price: money(rate['price']) }
|
20
|
+
|
21
|
+
# Add any additional fees to the amounts hash
|
22
|
+
rate['fees'].each_with_object(amounts) do |fee, result|
|
23
|
+
result[fee['name']] = money(fee['price'])
|
24
|
+
end
|
25
|
+
|
26
|
+
FriendlyShipping::Rate.new(
|
27
|
+
amounts: amounts,
|
28
|
+
shipping_method: shipping_method,
|
29
|
+
data: {
|
30
|
+
description: rate['description'],
|
31
|
+
zone: rate['zone']
|
32
|
+
}
|
33
|
+
)
|
34
|
+
end
|
35
|
+
|
36
|
+
success(rates, request, response)
|
37
|
+
rescue JSON::ParserError, KeyError => e
|
38
|
+
failure(e.message, request, response)
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
# @param value [Numeric]
|
44
|
+
# @return [Money]
|
45
|
+
def money(value)
|
46
|
+
Money.new(value * CURRENCY.subunit_to_unit, CURRENCY)
|
47
|
+
end
|
48
|
+
|
49
|
+
# @param rates [Array<Rate>]
|
50
|
+
# @param request [Request]
|
51
|
+
# @param response [Response]
|
52
|
+
# @return [Success<ApiResult<Array<Rate>>]
|
53
|
+
def success(rates, request, response)
|
54
|
+
Success(
|
55
|
+
ApiResult.new(
|
56
|
+
rates,
|
57
|
+
original_request: request,
|
58
|
+
original_response: response
|
59
|
+
)
|
60
|
+
)
|
61
|
+
end
|
62
|
+
|
63
|
+
# @param message [String]
|
64
|
+
# @param request [Request]
|
65
|
+
# @param response [Response]
|
66
|
+
# @return [Failure<ApiFailure<String>>]
|
67
|
+
def failure(message, request, response)
|
68
|
+
Failure(
|
69
|
+
ApiFailure.new(
|
70
|
+
message,
|
71
|
+
original_request: request,
|
72
|
+
original_response: response
|
73
|
+
)
|
74
|
+
)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'friendly_shipping/timing'
|
4
|
+
|
5
|
+
module FriendlyShipping
|
6
|
+
module Services
|
7
|
+
class USPSShip
|
8
|
+
class ParseTimingsResponse
|
9
|
+
extend Dry::Monads::Result::Mixin
|
10
|
+
|
11
|
+
class << self
|
12
|
+
# Parse a timings response.
|
13
|
+
#
|
14
|
+
# @param request [Request] the request that was used to obtain this response
|
15
|
+
# @param response [Response] the response that USPS returned
|
16
|
+
# @return [Success<ApiResult<Array<Timing>>>, Failure<ApiFailure<String>>]
|
17
|
+
def call(request:, response:)
|
18
|
+
timings = JSON.parse(response.body).map do |timing|
|
19
|
+
shipping_method = SHIPPING_METHODS.detect { |sm| sm.service_code == timing['mailClass'] }
|
20
|
+
|
21
|
+
# The delivery estimate is blank if an invalid destination zip code was used
|
22
|
+
delivery = timing.dig('delivery', 'scheduledDeliveryDateTime')
|
23
|
+
next unless delivery
|
24
|
+
|
25
|
+
FriendlyShipping::Timing.new(
|
26
|
+
shipping_method: shipping_method,
|
27
|
+
pickup: Time.parse(timing['acceptanceDateTime']),
|
28
|
+
delivery: Time.parse(delivery),
|
29
|
+
guaranteed: timing['delivery']['guaranteedDelivery'],
|
30
|
+
data: {
|
31
|
+
notes: timing['notes'],
|
32
|
+
service_standard: timing['serviceStandard'],
|
33
|
+
service_standard_message: timing['serviceStandardMessage']
|
34
|
+
}
|
35
|
+
)
|
36
|
+
end.compact
|
37
|
+
|
38
|
+
if timings.empty?
|
39
|
+
failure("No timings were returned. Is the destination zip correct?", request, response)
|
40
|
+
else
|
41
|
+
success(timings, request, response)
|
42
|
+
end
|
43
|
+
rescue JSON::ParserError => e
|
44
|
+
failure(e.message, request, response)
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
# @param timings [Array<Timing>]
|
50
|
+
# @param request [Request]
|
51
|
+
# @param response [Response]
|
52
|
+
# @return [Success<ApiResult<Array<Timing>>]
|
53
|
+
def success(timings, request, response)
|
54
|
+
Success(
|
55
|
+
ApiResult.new(
|
56
|
+
timings,
|
57
|
+
original_request: request,
|
58
|
+
original_response: response
|
59
|
+
)
|
60
|
+
)
|
61
|
+
end
|
62
|
+
|
63
|
+
# @param message [String]
|
64
|
+
# @param request [Request]
|
65
|
+
# @param response [Response]
|
66
|
+
# @return [Failure<ApiFailure<String>>]
|
67
|
+
def failure(message, request, response)
|
68
|
+
Failure(
|
69
|
+
ApiFailure.new(
|
70
|
+
message,
|
71
|
+
original_request: request,
|
72
|
+
original_response: response
|
73
|
+
)
|
74
|
+
)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FriendlyShipping
|
4
|
+
module Services
|
5
|
+
class USPSShip
|
6
|
+
class RateEstimateOptions < FriendlyShipping::ShipmentOptions
|
7
|
+
DESTINATION_ENTRY_FACILITY_TYPES = {
|
8
|
+
none: "NONE",
|
9
|
+
destination_network_distribution_center: "DESTINATION_NETWORK_DISTRIBUTION_CENTER",
|
10
|
+
destination_sectional_center_facility: "DESTINATION_SECTIONAL_CENTER_FACILITY",
|
11
|
+
destination_delivery_unit: "DESTINATION_DELIVERY_UNIT",
|
12
|
+
destination_service_hub: "DESTINATION_SERVICE_HUB"
|
13
|
+
}.freeze
|
14
|
+
|
15
|
+
# @return [ShippingMethod]
|
16
|
+
attr_reader :shipping_method
|
17
|
+
|
18
|
+
# @return [String]
|
19
|
+
attr_reader :destination_entry_facility_type
|
20
|
+
|
21
|
+
# @return [#strftime]
|
22
|
+
attr_reader :mailing_date
|
23
|
+
|
24
|
+
# @param shipping_method [ShippingMethod] the shipping method for which we want a rate
|
25
|
+
# @param destination_entry_facility_type [Symbol] one of {DESTINATION_ENTRY_FACILITY_TYPES}
|
26
|
+
# @param mailing_date [#strftime] the date on which we want to ship
|
27
|
+
# @param package_options_class [Class] the class to use for package options
|
28
|
+
# @param kwargs [Hash]
|
29
|
+
# @option kwargs [Array<PackageOptions>] :package_options the options for packages in this shipment
|
30
|
+
def initialize(
|
31
|
+
shipping_method:,
|
32
|
+
destination_entry_facility_type: :none,
|
33
|
+
mailing_date: Date.today,
|
34
|
+
package_options_class: FriendlyShipping::Services::USPSShip::RateEstimatePackageOptions,
|
35
|
+
**kwargs
|
36
|
+
)
|
37
|
+
@shipping_method = shipping_method
|
38
|
+
@destination_entry_facility_type = DESTINATION_ENTRY_FACILITY_TYPES.fetch(destination_entry_facility_type)
|
39
|
+
@mailing_date = mailing_date
|
40
|
+
super(**kwargs.reverse_merge(package_options_class: package_options_class))
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|