friendly_shipping 0.10.1 → 0.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +1 -0
  3. data/README.md +16 -16
  4. data/lib/friendly_shipping/services/rl/parse_rate_quote_response.rb +3 -3
  5. data/lib/friendly_shipping/services/ups_json/parse_money_hash.rb +1 -0
  6. data/lib/friendly_shipping/services/usps_international/parse_rate_response.rb +3 -3
  7. data/lib/friendly_shipping/services/{usps → usps_international}/parse_xml_response.rb +1 -1
  8. data/lib/friendly_shipping/services/usps_international/serialize_rate_request.rb +1 -1
  9. data/lib/friendly_shipping/services/usps_ship/parse_rate_estimates_response.rb +1 -1
  10. data/lib/friendly_shipping/version.rb +1 -1
  11. metadata +3 -51
  12. data/lib/friendly_shipping/services/ups/label.rb +0 -20
  13. data/lib/friendly_shipping/services/ups/label_billing_options.rb +0 -41
  14. data/lib/friendly_shipping/services/ups/label_item_options.rb +0 -75
  15. data/lib/friendly_shipping/services/ups/label_options.rb +0 -174
  16. data/lib/friendly_shipping/services/ups/label_package_options.rb +0 -49
  17. data/lib/friendly_shipping/services/ups/parse_address_classification_response.rb +0 -29
  18. data/lib/friendly_shipping/services/ups/parse_address_validation_response.rb +0 -53
  19. data/lib/friendly_shipping/services/ups/parse_city_state_lookup_response.rb +0 -33
  20. data/lib/friendly_shipping/services/ups/parse_modifier_element.rb +0 -29
  21. data/lib/friendly_shipping/services/ups/parse_money_element.rb +0 -128
  22. data/lib/friendly_shipping/services/ups/parse_rate_response.rb +0 -101
  23. data/lib/friendly_shipping/services/ups/parse_shipment_accept_response.rb +0 -77
  24. data/lib/friendly_shipping/services/ups/parse_shipment_confirm_response.rb +0 -24
  25. data/lib/friendly_shipping/services/ups/parse_time_in_transit_response.rb +0 -56
  26. data/lib/friendly_shipping/services/ups/parse_void_shipment_response.rb +0 -24
  27. data/lib/friendly_shipping/services/ups/parse_xml_response.rb +0 -50
  28. data/lib/friendly_shipping/services/ups/rate_estimate_options.rb +0 -111
  29. data/lib/friendly_shipping/services/ups/rate_estimate_package_options.rb +0 -22
  30. data/lib/friendly_shipping/services/ups/serialize_access_request.rb +0 -20
  31. data/lib/friendly_shipping/services/ups/serialize_address_snippet.rb +0 -60
  32. data/lib/friendly_shipping/services/ups/serialize_address_validation_request.rb +0 -40
  33. data/lib/friendly_shipping/services/ups/serialize_city_state_lookup_request.rb +0 -26
  34. data/lib/friendly_shipping/services/ups/serialize_package_node.rb +0 -75
  35. data/lib/friendly_shipping/services/ups/serialize_rating_service_selection_request.rb +0 -98
  36. data/lib/friendly_shipping/services/ups/serialize_shipment_accept_request.rb +0 -27
  37. data/lib/friendly_shipping/services/ups/serialize_shipment_address_snippet.rb +0 -21
  38. data/lib/friendly_shipping/services/ups/serialize_shipment_confirm_request.rb +0 -285
  39. data/lib/friendly_shipping/services/ups/serialize_time_in_transit_request.rb +0 -56
  40. data/lib/friendly_shipping/services/ups/serialize_void_shipment_request.rb +0 -21
  41. data/lib/friendly_shipping/services/ups/shipping_methods.rb +0 -111
  42. data/lib/friendly_shipping/services/ups/timing_options.rb +0 -33
  43. data/lib/friendly_shipping/services/ups.rb +0 -218
  44. data/lib/friendly_shipping/services/usps/choose_package_rate.rb +0 -40
  45. data/lib/friendly_shipping/services/usps/machinable_package.rb +0 -50
  46. data/lib/friendly_shipping/services/usps/parse_address_validation_response.rb +0 -43
  47. data/lib/friendly_shipping/services/usps/parse_city_state_lookup_response.rb +0 -41
  48. data/lib/friendly_shipping/services/usps/parse_package_rate.rb +0 -159
  49. data/lib/friendly_shipping/services/usps/parse_rate_response.rb +0 -86
  50. data/lib/friendly_shipping/services/usps/parse_time_in_transit_response.rb +0 -240
  51. data/lib/friendly_shipping/services/usps/rate_estimate_options.rb +0 -26
  52. data/lib/friendly_shipping/services/usps/rate_estimate_package_options.rb +0 -66
  53. data/lib/friendly_shipping/services/usps/serialize_address_validation_request.rb +0 -25
  54. data/lib/friendly_shipping/services/usps/serialize_city_state_lookup_request.rb +0 -20
  55. data/lib/friendly_shipping/services/usps/serialize_rate_request.rb +0 -83
  56. data/lib/friendly_shipping/services/usps/serialize_time_in_transit_request.rb +0 -22
  57. data/lib/friendly_shipping/services/usps/shipping_methods.rb +0 -66
  58. data/lib/friendly_shipping/services/usps/timing_options.rb +0 -19
  59. data/lib/friendly_shipping/services/usps.rb +0 -115
@@ -1,218 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'nokogiri'
4
-
5
- module FriendlyShipping
6
- module Services
7
- class Ups
8
- include Dry::Monads::Result::Mixin
9
-
10
- attr_reader :test, :key, :login, :password, :client
11
-
12
- CARRIER = FriendlyShipping::Carrier.new(
13
- id: 'ups',
14
- name: 'United Parcel Service',
15
- code: 'ups',
16
- shipping_methods: SHIPPING_METHODS
17
- )
18
-
19
- TEST_URL = 'https://wwwcie.ups.com'
20
- LIVE_URL = 'https://onlinetools.ups.com'
21
-
22
- RESOURCES = {
23
- address_validation: '/ups.app/xml/XAV',
24
- city_state_lookup: '/ups.app/xml/AV',
25
- rates: '/ups.app/xml/Rate',
26
- ship_confirm: '/ups.app/xml/ShipConfirm',
27
- ship_accept: '/ups.app/xml/ShipAccept',
28
- timings: '/ups.app/xml/TimeInTransit',
29
- void: '/ups.app/xml/Void',
30
- }.freeze
31
-
32
- def initialize(key:, login:, password:, test: true, client: HttpClient.new)
33
- @key = key
34
- @login = login
35
- @password = password
36
- @test = test
37
- @client = client
38
- end
39
-
40
- def carriers
41
- Success([CARRIER])
42
- end
43
-
44
- # Get rates for a shipment
45
- # @param [Physical::Shipment] shipment The shipment we want to get rates for
46
- # @param [FriendlyShipping::Services::Ups::RateEstimateOptions] options What options
47
- # to use for this rate estimate call
48
- # @return [Result<ApiResult<Array<Rate>>>] The rates returned from UPS encoded in a
49
- # `FriendlyShipping::ApiResult` object.
50
- def rate_estimates(shipment, options:, debug: false)
51
- rate_request_xml = SerializeRatingServiceSelectionRequest.call(shipment: shipment, options: options)
52
- url = base_url + RESOURCES[:rates]
53
- request = FriendlyShipping::Request.new(
54
- url: url,
55
- http_method: "POST",
56
- body: access_request_xml + rate_request_xml,
57
- readable_body: rate_request_xml,
58
- debug: debug
59
- )
60
-
61
- client.post(request).bind do |response|
62
- ParseRateResponse.call(response: response, request: request, shipment: shipment)
63
- end
64
- end
65
-
66
- # Get timing information for a shipment
67
- # @param [Physical::Shipment] shipment The shipment we want to estimate timings for
68
- # @param [FriendlyShipping::Services::Ups::TimingOptions] options Options for this call
69
- def timings(shipment, options:, debug: false)
70
- time_in_transit_request_xml = SerializeTimeInTransitRequest.call(
71
- shipment: shipment,
72
- options: options
73
- )
74
- time_in_transit_url = base_url + RESOURCES[:timings]
75
-
76
- request = FriendlyShipping::Request.new(
77
- url: time_in_transit_url,
78
- http_method: "POST",
79
- body: access_request_xml + time_in_transit_request_xml,
80
- readable_body: time_in_transit_request_xml,
81
- debug: debug
82
- )
83
-
84
- client.post(request).bind do |response|
85
- ParseTimeInTransitResponse.call(response: response, request: request)
86
- end
87
- end
88
-
89
- def labels(shipment, options:, debug: false)
90
- ## Method body starts
91
- ship_confirm_request_xml = SerializeShipmentConfirmRequest.call(
92
- shipment: shipment,
93
- options: options
94
- )
95
- ship_confirm_url = base_url + RESOURCES[:ship_confirm]
96
-
97
- ship_confirm_request = FriendlyShipping::Request.new(
98
- url: ship_confirm_url,
99
- http_method: "POST",
100
- body: access_request_xml + ship_confirm_request_xml,
101
- readable_body: ship_confirm_request_xml,
102
- debug: debug
103
- )
104
-
105
- client.post(ship_confirm_request).bind do |ship_confirm_response|
106
- ParseShipmentConfirmResponse.call(
107
- request: ship_confirm_request,
108
- response: ship_confirm_response
109
- )
110
- end.bind do |ship_confirm_result|
111
- ship_accept_url = base_url + RESOURCES[:ship_accept]
112
- ship_accept_request_xml = SerializeShipmentAcceptRequest.call(
113
- digest: ship_confirm_result.data,
114
- options: options
115
- )
116
-
117
- ship_accept_request = FriendlyShipping::Request.new(
118
- url: ship_accept_url,
119
- http_method: "POST",
120
- body: access_request_xml + ship_accept_request_xml,
121
- readable_body: ship_accept_request_xml,
122
- debug: debug
123
- )
124
-
125
- client.post(ship_accept_request).bind do |ship_accept_response|
126
- ParseShipmentAcceptResponse.call(request: ship_accept_request, response: ship_accept_response)
127
- end
128
- end
129
- end
130
-
131
- # Validate an address.
132
- # @param [Physical::Location] location The address we want to verify
133
- # @return [Result<ApiResult<Array<Physical::Location>>>] The response data from UPS encoded in a
134
- # `Physical::Location` object. Name and Company name are always nil, the
135
- # address lines will be made conformant to what UPS considers right. The returned location will
136
- # have the address_type set if possible.
137
- def address_validation(location, debug: false)
138
- address_validation_request_xml = SerializeAddressValidationRequest.call(location: location)
139
- url = base_url + RESOURCES[:address_validation]
140
- request = FriendlyShipping::Request.new(
141
- url: url,
142
- http_method: "POST",
143
- body: access_request_xml + address_validation_request_xml,
144
- readable_body: address_validation_request_xml,
145
- debug: debug
146
- )
147
-
148
- client.post(request).bind do |response|
149
- ParseAddressValidationResponse.call(response: response, request: request)
150
- end
151
- end
152
-
153
- # Classify an address.
154
- # @param [Physical::Location] location The address we want to classify
155
- # @return [Result<ApiResult<String>>] Either `"commercial"`, `"residential"`, or `"unknown"`
156
- def address_classification(location, debug: false)
157
- address_validation_request_xml = SerializeAddressValidationRequest.call(location: location)
158
- url = base_url + RESOURCES[:address_validation]
159
- request = FriendlyShipping::Request.new(
160
- url: url,
161
- http_method: "POST",
162
- body: access_request_xml + address_validation_request_xml,
163
- readable_body: address_validation_request_xml,
164
- debug: debug
165
- )
166
-
167
- client.post(request).bind do |response|
168
- ParseAddressClassificationResponse.call(response: response, request: request)
169
- end
170
- end
171
-
172
- # Find city and state for a given ZIP code
173
- # @param [Physical::Location] location A location object with country and ZIP code set
174
- # @return [Result<ApiResult<Array<Physical::Location>>>] The response data from UPS encoded in a
175
- # `Physical::Location` object. Country, City and ZIP code will be set, everything else nil.
176
- def city_state_lookup(location, debug: false)
177
- city_state_lookup_request_xml = SerializeCityStateLookupRequest.call(location: location)
178
- url = base_url + RESOURCES[:city_state_lookup]
179
- request = FriendlyShipping::Request.new(
180
- url: url,
181
- http_method: "POST",
182
- body: access_request_xml + city_state_lookup_request_xml,
183
- readable_body: city_state_lookup_request_xml,
184
- debug: debug
185
- )
186
-
187
- client.post(request).bind do |response|
188
- ParseCityStateLookupResponse.call(response: response, request: request, location: location)
189
- end
190
- end
191
-
192
- def void(label, debug: false)
193
- url = base_url + RESOURCES[:void]
194
- void_request_xml = SerializeVoidShipmentRequest.call(label: label)
195
- request = FriendlyShipping::Request.new(
196
- url: url,
197
- http_method: "POST",
198
- body: access_request_xml + void_request_xml,
199
- readable_body: void_request_xml,
200
- debug: debug
201
- )
202
- client.post(request).bind do |response|
203
- ParseVoidShipmentResponse.call(request: request, response: response)
204
- end
205
- end
206
-
207
- private
208
-
209
- def access_request_xml
210
- SerializeAccessRequest.call(key: key, login: login, password: password)
211
- end
212
-
213
- def base_url
214
- test ? TEST_URL : LIVE_URL
215
- end
216
- end
217
- end
218
- end
@@ -1,40 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module FriendlyShipping
4
- module Services
5
- class Usps
6
- class ChoosePackageRate
7
- class CannotDetermineRate < StandardError; end
8
- # Some shipping rates use 'Flat Rate Boxes', indicating that
9
- # they are available for ALL flat rate boxes.
10
- FLAT_RATE_BOX = /Flat Rate Box/i
11
-
12
- # Select the corresponding rate for a package from all the rates USPS returns to us
13
- #
14
- # @param [FriendlyShipping::ShippingMethod] shipping_method The shipping method we want to filter by
15
- # @param [Array<FriendlyShipping::Rate>] rates The rates we select from
16
- # @param [FriendlyShipping::PackageOptions] package_options The package options we want to match with a rate
17
- #
18
- # @return [FriendlyShipping::Rate] The rate that most closely matches our package
19
- def self.call(shipping_method, rates, package_options)
20
- # Keep all rates with the requested shipping method
21
- rates_with_this_shipping_method = rates.select { |r| r.shipping_method == shipping_method }
22
-
23
- # Keep only rates with the package type of this package
24
- rates_with_this_package_type = rates_with_this_shipping_method.select do |r|
25
- r.data[:box_name] == package_options.box_name
26
- end
27
-
28
- # Filter by our package's `hold_for_pickup` option
29
- rates_with_this_hold_for_pickup_option = rates_with_this_package_type.select do |r|
30
- r.data[:hold_for_pickup] == package_options.hold_for_pickup
31
- end
32
-
33
- # At this point, we have one or two rates left, and they're similar enough.
34
- # Once this poses an actual problem, we'll fix it.
35
- rates_with_this_hold_for_pickup_option.first
36
- end
37
- end
38
- end
39
- end
40
- end
@@ -1,50 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module FriendlyShipping
4
- module Services
5
- class Usps
6
- # USPS has certain size and weight requirements for packages to
7
- # be considered machinable. Machinable packages are generally
8
- # less expensive to ship. For more information see:
9
- # https://pe.usps.com/BusinessMail101?ViewName=Parcels
10
- #
11
- class MachinablePackage
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 [Physical::Package] package
25
- def initialize(package)
26
- @package = package
27
- end
28
-
29
- def machinable?
30
- at_least_minimum && at_most_maximum
31
- end
32
-
33
- private
34
-
35
- def at_least_minimum
36
- package.length >= MIN_LENGTH &&
37
- package.width >= MIN_WIDTH &&
38
- package.height >= MIN_HEIGHT
39
- end
40
-
41
- def at_most_maximum
42
- package.length <= MAX_LENGTH &&
43
- package.width <= MAX_WIDTH &&
44
- package.height <= MAX_HEIGHT &&
45
- package.weight <= MAX_WEIGHT
46
- end
47
- end
48
- end
49
- end
50
- end
@@ -1,43 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module FriendlyShipping
4
- module Services
5
- class Usps
6
- class ParseAddressValidationResponse
7
- class << self
8
- # Parse a response from USPS' address validation API
9
- #
10
- # @param [FriendlyShipping::Request] request The request that was used to obtain this Response
11
- # @param [FriendlyShipping::Response] response The response that USPS returned
12
- # @return [Result<FriendlyShipping::AddressValidationResult>]
13
- def call(request:, response:)
14
- # Filter out error responses and directly return a failure
15
- parsing_result = ParseXMLResponse.call(
16
- request: request,
17
- response: response,
18
- expected_root_tag: 'AddressValidateResponse'
19
- )
20
- parsing_result.fmap do |xml|
21
- address = xml.root.at('Address')
22
- suggestions = [
23
- Physical::Location.new(
24
- address1: address&.at('Address2')&.text, # USPS swaps Address1 and Address2 in the response
25
- address2: address&.at('Address1')&.text,
26
- city: address&.at('City')&.text,
27
- region: address&.at('State')&.text,
28
- zip: address&.at('Zip5')&.text,
29
- country: 'US'
30
- )
31
- ]
32
- FriendlyShipping::ApiResult.new(
33
- suggestions,
34
- original_request: request,
35
- original_response: response
36
- )
37
- end
38
- end
39
- end
40
- end
41
- end
42
- end
43
- end
@@ -1,41 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module FriendlyShipping
4
- module Services
5
- class Usps
6
- class ParseCityStateLookupResponse
7
- class << self
8
- # Parse a response from USPS' city/state lookup API
9
- #
10
- # @param [FriendlyShipping::Request] request The request that was used to obtain this Response
11
- # @param [FriendlyShipping::Response] response The response that USPS returned
12
- # @return [Result<FriendlyShipping::AddressValidationResult>]
13
- def call(request:, response:)
14
- # Filter out error responses and directly return a failure
15
- parsing_result = ParseXMLResponse.call(
16
- request: request,
17
- response: response,
18
- expected_root_tag: 'CityStateLookupResponse'
19
- )
20
- parsing_result.fmap do |xml|
21
- address = xml.root.at('ZipCode')
22
- suggestions = [
23
- Physical::Location.new(
24
- city: address&.at('City')&.text,
25
- region: address&.at('State')&.text,
26
- zip: address&.at('Zip5')&.text,
27
- country: 'US'
28
- )
29
- ]
30
- FriendlyShipping::ApiResult.new(
31
- suggestions,
32
- original_request: request,
33
- original_response: response,
34
- )
35
- end
36
- end
37
- end
38
- end
39
- end
40
- end
41
- end
@@ -1,159 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module FriendlyShipping
4
- module Services
5
- class Usps
6
- class ParsePackageRate
7
- # USPS returns all the info about a rate in a long string with a bit of gibberish.
8
- ESCAPING_AND_SYMBOLS = /&lt;\S*&gt;/
9
-
10
- # At the beginning of the long String, USPS keeps a copy of its own name. We know we're dealing with
11
- # them though, so we can filter that out, too.
12
- LEADING_USPS = /^USPS /
13
-
14
- # This combines all the things we want to filter out.
15
- SERVICE_NAME_SUBSTITUTIONS = /#{ESCAPING_AND_SYMBOLS}|#{LEADING_USPS}/
16
-
17
- # Often we get a multitude of rates for the same service given some combination of
18
- # Box type and (see below) and "Hold for Pickup" service. This creates a regular expression
19
- # with groups named after the keys from the `Usps::CONTAINERS` constant.
20
- # Unfortunately, the keys don't correspond directly to the codes we use when serializing the
21
- # request.
22
- BOX_REGEX = {
23
- flat_rate_boxes: 'Flat Rate Boxes',
24
- large_flat_rate_box: 'Large Flat Rate Box',
25
- medium_flat_rate_box: 'Medium Flat Rate Box',
26
- small_flat_rate_box: 'Small Flat Rate Box',
27
- regional_rate_box_a: 'Regional Rate Box A',
28
- regional_rate_box_b: 'Regional Rate Box B',
29
- regional_rate_box_c: 'Regional Rate Box C',
30
- flat_rate_envelope: 'Flat Rate Envelope',
31
- legal_flat_rate_envelope: 'Legal Flat Rate Envelope',
32
- padded_flat_rate_envelope: 'Padded Flat Rate Envelope',
33
- gift_card_flat_rate_envelope: 'Gift Card Flat Rate Envelope',
34
- window_flat_rate_envelope: 'Window Flat Rate Envelope',
35
- small_flat_rate_envelope: 'Small Flat Rate Envelope',
36
- large_envelope: 'Large Envelope',
37
- parcel: 'Parcel',
38
- postcards: 'Postcards'
39
- }.map { |k, v| "(?<#{k}>#{v})" }.join("|").freeze
40
-
41
- # We use this for identifying rates that use the Hold for Pickup service.
42
- HOLD_FOR_PICKUP = /Hold for Pickup/i
43
-
44
- # For most rate options, USPS will return how many business days it takes to deliver this
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/
47
-
48
- # When delivering to military ZIP codes, we don't actually get a timing estimate, but instead the string
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
51
-
52
- # The tags used in the rate node that we get information from.
53
- SERVICE_CODE_TAG = 'CLASSID'
54
- SERVICE_NAME_TAG = 'MailService'
55
- RATE_TAG = 'Rate'
56
- COMMERCIAL_RATE_TAG = 'CommercialRate'
57
- COMMERCIAL_PLUS_RATE_TAG = 'CommercialPlusRate'
58
- DIMENSIONAL_WEIGHT_RATE = 'DimensionalWeightRate'
59
- FEES = './/Fees/Fee'
60
- CURRENCY = Money::Currency.new('USD').freeze
61
-
62
- class << self
63
- def call(rate_node, package, package_options)
64
- # "A mail class identifier for the postage returned. Not necessarily unique within a <Package/>."
65
- # (from the USPS docs). We save this on the data Hash, but do not use it for identifying shipping methods.
66
- service_code = rate_node.attributes[SERVICE_CODE_TAG].value
67
-
68
- # The long string discussed above.
69
- service_name = rate_node.at(SERVICE_NAME_TAG).text
70
-
71
- # Does this rate assume Hold for Pickup service?
72
- hold_for_pickup = service_name.match?(HOLD_FOR_PICKUP)
73
-
74
- # Is the destination a military ZIP code?
75
- military = service_name.match?(MILITARY)
76
-
77
- # If we get a days-to-delivery indication, save it in the `days_to_delivery` variable.
78
- days_to_delivery_match = service_name.match(DAYS_TO_DELIVERY)
79
- days_to_delivery = if days_to_delivery_match
80
- days_to_delivery_match.named_captures.values.first.to_i
81
- end
82
-
83
- # Clean up the long string
84
- service_name.gsub!(SERVICE_NAME_SUBSTITUTIONS, '')
85
-
86
- # Some USPS services only offer commercial pricing. Unfortunately, USPS then returns a retail rate of 0.
87
- # In these cases, return the commercial rate instead of the normal rate.
88
- #
89
- # Some rates are available in both commercial and retail pricing - if we want the commercial pricing here,
90
- # we need to specify the commercial_pricing property on the `Physical::Package`.
91
- #
92
- commercial_rate_requested_or_rate_is_zero = package_options.commercial_pricing || rate_node.at(RATE_TAG).text.to_d.zero?
93
- commercial_rate_available = rate_node.at(COMMERCIAL_RATE_TAG) || rate_node.at(COMMERCIAL_PLUS_RATE_TAG)
94
-
95
- rate_value =
96
- if commercial_rate_requested_or_rate_is_zero && commercial_rate_available
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
99
- else
100
- rate_node.at(RATE_TAG).text.to_d
101
- end
102
-
103
- # The rate expressed as a RubyMoney objext
104
- rate = Money.new(rate_value * CURRENCY.subunit_to_unit, CURRENCY)
105
-
106
- # Which shipping method does this rate belong to? We first try to match a rate to a shipping method
107
- # by class ID (the CLASSID attribute in the USPS API rate response). Not every shipping method
108
- # has a class ID defined, and a shipping method can have multiple class IDs (for example, Priority
109
- # Express has different class IDs for standard, hold for pickup, and Sunday/Holiday delivery).
110
- #
111
- # If we don't find a match for class ID, we next try to match a rate to a shipping method using the
112
- # shipping method's service code. The USPS API rate response includes a name for each rate in the
113
- # MailService element. We match to see if the name starts with the given value. For example:
114
- # `Priority Mail Express 2-day™`
115
- #
116
- shipping_method =
117
- SHIPPING_METHODS.detect { |sm| sm.data[:class_ids]&.include?(service_code) } ||
118
- SHIPPING_METHODS.detect { |sm| service_name.tr('-', ' ').upcase.starts_with?(sm.service_code) }
119
-
120
- # We find out the box name using a bit of Regex magic using named captures. See the `BOX_REGEX`
121
- # constant above.
122
- box_name_match = service_name.match(/#{BOX_REGEX}/)
123
- box_name = box_name_match ? box_name_match.named_captures.compact.keys.last.to_sym : :variable
124
-
125
- dimensional_weight_rate = rate_node.at(DIMENSIONAL_WEIGHT_RATE)&.text&.to_i
126
-
127
- fees = rate_node.xpath(FEES).map do |fee_node|
128
- type = fee_node.at('FeeType').text
129
- price = fee_node.at('FeePrice').text.to_d
130
- {
131
- type: type,
132
- price: Money.new(price * CURRENCY.subunit_to_unit, CURRENCY)
133
- }
134
- end
135
-
136
- # Combine all the gathered information in a FriendlyShipping::Rate object.
137
- # Careful: This rate is only for one package within the shipment, and we get multiple
138
- # rates per package for the different shipping method/box/hold for pickup combinations.
139
- FriendlyShipping::Rate.new(
140
- shipping_method: shipping_method,
141
- amounts: { package.id => rate },
142
- data: {
143
- package: package,
144
- box_name: box_name,
145
- hold_for_pickup: hold_for_pickup,
146
- days_to_delivery: days_to_delivery,
147
- military: military,
148
- full_mail_service: service_name,
149
- service_code: service_code,
150
- dimensional_weight_rate: dimensional_weight_rate,
151
- fees: fees
152
- }
153
- )
154
- end
155
- end
156
- end
157
- end
158
- end
159
- end
@@ -1,86 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module FriendlyShipping
4
- module Services
5
- class Usps
6
- class ParseRateResponse
7
- class BoxNotFoundError < StandardError; end
8
-
9
- class << self
10
- # Parse a response from USPS' rating API
11
- #
12
- # @param [FriendlyShipping::Request] request The request that was used to obtain this Response
13
- # @param [FriendlyShipping::Response] response The response that USPS returned
14
- # @param [Physical::Shipment] shipment The shipment object we're trying to get results for
15
- # @param [FriendlyShipping::Services::Usps::RateEstimateOptions] options The options we sent with this request
16
- # @return [Result<ApiResult<Array<FriendlyShipping::Rate>>>] When successfully parsing, an array of rates in a Success Monad.
17
- def call(request:, response:, shipment:, options:)
18
- # Filter out error responses and directly return a failure
19
- parsing_result = ParseXMLResponse.call(
20
- request: request,
21
- response: response,
22
- expected_root_tag: 'RateV4Response'
23
- )
24
- parsing_result.fmap do |xml|
25
- # Get all the possible rates for each package
26
- rates_by_package = rates_from_response_node(xml, shipment, options)
27
-
28
- rates = SHIPPING_METHODS.map do |shipping_method|
29
- # For every package ...
30
- matching_rates = rates_by_package.map do |package, package_rates|
31
- # ... choose the rate that fits this package best.
32
-
33
- package_options = options.options_for_package(package)
34
-
35
- ChoosePackageRate.call(shipping_method, package_rates, package_options)
36
- end.compact # Some shipping rates are not available for every shipping method.
37
-
38
- # in this case, go to the next shipping method.
39
- next if matching_rates.empty?
40
-
41
- # return one rate for all packages with the amount keys being the package IDs.
42
- FriendlyShipping::Rate.new(
43
- amounts: matching_rates.map(&:amounts).reduce({}, :merge),
44
- shipping_method: shipping_method,
45
- data: matching_rates.first.data
46
- )
47
- end.compact
48
-
49
- ApiResult.new(
50
- rates,
51
- original_request: request,
52
- original_response: response
53
- )
54
- end
55
- end
56
-
57
- private
58
-
59
- PACKAGE_NODE_XPATH = '//Package'
60
- SERVICE_NODE_NAME = 'Postage'
61
-
62
- # Iterate over all packages and parse the rates for each package
63
- #
64
- # @param [Nokogiri::XML::Node] xml The XML document containing packages and rates
65
- # @param [Physical::Shipment] shipment The shipment we're trying to get rates for
66
- #
67
- # @return [Hash<Physical::Package => Array<FriendlyShipping::Rate>>]
68
- def rates_from_response_node(xml, shipment, options)
69
- xml.xpath(PACKAGE_NODE_XPATH).each_with_object({}) do |package_node, result|
70
- package_id = package_node['ID']
71
- corresponding_package = shipment.packages[package_id.to_i]
72
- package_options = options.options_for_package(corresponding_package)
73
- # There should always be a package in the original shipment that corresponds to the package ID
74
- # in the USPS response.
75
- raise BoxNotFoundError if corresponding_package.nil?
76
-
77
- result[corresponding_package] = package_node.xpath(SERVICE_NODE_NAME).map do |service_node|
78
- ParsePackageRate.call(service_node, corresponding_package, package_options)
79
- end
80
- end
81
- end
82
- end
83
- end
84
- end
85
- end
86
- end