friendly_shipping 0.4.14 → 0.5
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 +4 -4
- data/.circleci/config.yml +1 -1
- data/.rubocop.yml +1 -1
- data/CHANGELOG.md +8 -0
- data/friendly_shipping.gemspec +1 -1
- data/lib/friendly_shipping/services/ups.rb +5 -2
- data/lib/friendly_shipping/services/ups/rate_estimate_options.rb +100 -0
- data/lib/friendly_shipping/services/ups/rate_estimate_package_options.rb +24 -0
- data/lib/friendly_shipping/services/ups/serialize_package_node.rb +3 -2
- data/lib/friendly_shipping/services/ups/serialize_rating_service_selection_request.rb +40 -34
- data/lib/friendly_shipping/services/usps.rb +7 -12
- data/lib/friendly_shipping/services/usps/choose_package_rate.rb +3 -4
- data/lib/friendly_shipping/services/usps/parse_package_rate.rb +3 -5
- data/lib/friendly_shipping/services/usps/parse_rate_response.rb +10 -6
- data/lib/friendly_shipping/services/usps/rate_estimate_options.rb +28 -0
- data/lib/friendly_shipping/services/usps/rate_estimate_package_options.rb +58 -0
- data/lib/friendly_shipping/services/usps/serialize_rate_request.rb +7 -17
- data/lib/friendly_shipping/version.rb +1 -1
- metadata +7 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a7033f8bf845a41a0cefa6899a465fd78a85077ff565c7c30f67d04f20708ff6
|
4
|
+
data.tar.gz: 2c5207e0139608d78b7c65b6f273b9dc222bc750bc47e587147e8f486c908b43
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 00d7ce8fe1d737b482d71a6ff96195a82dc70b94c7784291c9cc18eb2f71ca529dadf33900a7f5aef223a4f89a3b1297403d7bc352dd9e8255c139b11b7351c1
|
7
|
+
data.tar.gz: 7a7746f0c59dfcd03d2a330142f106cfe3c0fc7b82628859691d28c0dbd27e3d7584670c44f3509780271aaaf44fbd83551d0e1415ac14807d9ec2578891ab95
|
data/.circleci/config.yml
CHANGED
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.
|
|
4
4
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
5
5
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
6
6
|
|
7
|
+
## [0.5] - 2020-01-24
|
8
|
+
|
9
|
+
### Removed
|
10
|
+
- Drop support for Ruby 2.4 (#83)
|
11
|
+
|
12
|
+
### Changed
|
13
|
+
- UPS/USPS Services: Use options classes for rate estimates (#82)
|
14
|
+
|
7
15
|
## [0.4.14] - 2020-01-21
|
8
16
|
|
9
17
|
### Changed
|
data/friendly_shipping.gemspec
CHANGED
@@ -28,7 +28,7 @@ Gem::Specification.new do |spec|
|
|
28
28
|
spec.add_runtime_dependency "nokogiri", ">= 1.6"
|
29
29
|
spec.add_runtime_dependency "physical", ">= 0.4.4"
|
30
30
|
spec.add_runtime_dependency "rest-client", "~> 2.0"
|
31
|
-
spec.required_ruby_version = '>= 2.
|
31
|
+
spec.required_ruby_version = '>= 2.5'
|
32
32
|
|
33
33
|
spec.add_development_dependency "bundler"
|
34
34
|
spec.add_development_dependency "dotenv"
|
@@ -20,6 +20,7 @@ require 'friendly_shipping/services/ups/parse_time_in_transit_response'
|
|
20
20
|
require 'friendly_shipping/services/ups/parse_void_shipment_response'
|
21
21
|
require 'friendly_shipping/services/ups/shipping_methods'
|
22
22
|
require 'friendly_shipping/services/ups/label_options'
|
23
|
+
require 'friendly_shipping/services/ups/rate_estimate_options'
|
23
24
|
require 'friendly_shipping/services/ups/timing_options'
|
24
25
|
|
25
26
|
module FriendlyShipping
|
@@ -63,10 +64,12 @@ module FriendlyShipping
|
|
63
64
|
|
64
65
|
# Get rates for a shipment
|
65
66
|
# @param [Physical::Shipment] shipment The shipment we want to get rates for
|
67
|
+
# @param [FriendlyShipping::Services::Ups::RateEstimateOptions] options What options
|
68
|
+
# to use for this rate estimate call
|
66
69
|
# @return [Result<ApiResult<Array<Rate>>>] The rates returned from UPS encoded in a
|
67
70
|
# `FriendlyShipping::ApiResult` object.
|
68
|
-
def rate_estimates(shipment, debug: false)
|
69
|
-
rate_request_xml = SerializeRatingServiceSelectionRequest.call(shipment: shipment)
|
71
|
+
def rate_estimates(shipment, options:, debug: false)
|
72
|
+
rate_request_xml = SerializeRatingServiceSelectionRequest.call(shipment: shipment, options: options)
|
70
73
|
url = base_url + RESOURCES[:rates]
|
71
74
|
request = FriendlyShipping::Request.new(
|
72
75
|
url: url,
|
@@ -0,0 +1,100 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'friendly_shipping/services/ups/rate_estimate_package_options'
|
4
|
+
|
5
|
+
module FriendlyShipping
|
6
|
+
module Services
|
7
|
+
# Option container for rating a shipment via UPS
|
8
|
+
#
|
9
|
+
# Required:
|
10
|
+
#
|
11
|
+
# @option shipper_number [String] The shipper number of the origin of the shipment.
|
12
|
+
#
|
13
|
+
# Optional:
|
14
|
+
#
|
15
|
+
# @option carbon_neutral [Boolean] Ship with UPS' carbon neutral program
|
16
|
+
# @option customer_context [String ] Optional element to identify transactions between client and server
|
17
|
+
# @option customer_classification [Symbol] Which kind of rates to request. See UPS docs for more details. Default: `shipper_number`
|
18
|
+
# @option negotiated_rates [Boolean] if truthy negotiated rates will be requested from ups. Only valid if
|
19
|
+
# shipper account has negotiated rates. Default: false
|
20
|
+
# @option saturday_delivery [Boolean] should we request Saturday delivery?. Default: false
|
21
|
+
# @option saturday_pickup [Boolean] should we request Saturday pickup?. Default: false
|
22
|
+
# @option shipping_method [FriendlyShipping::ShippingMethod] Request rates for a particular shipping method only?
|
23
|
+
# Default is `nil`, which translates to 'All shipping methods' (The "Shop" option in UPS parlance)
|
24
|
+
# @option with_time_in_transit [Boolean] Whether to request timing information alongside the rates
|
25
|
+
# @option package_options_class [Class] See FriendlyShipping::ShipmentOptions
|
26
|
+
#
|
27
|
+
class Ups
|
28
|
+
class RateEstimateOptions < FriendlyShipping::ShipmentOptions
|
29
|
+
PICKUP_TYPE_CODES = {
|
30
|
+
daily_pickup: "01",
|
31
|
+
customer_counter: "03",
|
32
|
+
one_time_pickup: "06",
|
33
|
+
on_call_air: "07",
|
34
|
+
suggested_retail_rates: "11",
|
35
|
+
letter_center: "19",
|
36
|
+
air_service_center: "20"
|
37
|
+
}.freeze
|
38
|
+
|
39
|
+
CUSTOMER_CLASSIFICATION_CODES = {
|
40
|
+
shipper_number: "00",
|
41
|
+
daily_rates: "01",
|
42
|
+
retail_rates: "04",
|
43
|
+
regional_rates: "05",
|
44
|
+
general_rates: "06",
|
45
|
+
standard_rates: "53"
|
46
|
+
}.freeze
|
47
|
+
|
48
|
+
attr_reader :carbon_neutral,
|
49
|
+
:customer_context,
|
50
|
+
:destination_account,
|
51
|
+
:negotiated_rates,
|
52
|
+
:saturday_delivery,
|
53
|
+
:saturday_pickup,
|
54
|
+
:shipper,
|
55
|
+
:shipper_number,
|
56
|
+
:shipping_method,
|
57
|
+
:with_time_in_transit
|
58
|
+
|
59
|
+
def initialize(
|
60
|
+
shipper_number:,
|
61
|
+
carbon_neutral: true,
|
62
|
+
customer_context: nil,
|
63
|
+
customer_classification: :daily_rates,
|
64
|
+
destination_account: nil,
|
65
|
+
negotiated_rates: false,
|
66
|
+
pickup_type: :daily_pickup,
|
67
|
+
saturday_delivery: false,
|
68
|
+
saturday_pickup: false,
|
69
|
+
shipper: nil,
|
70
|
+
shipping_method: nil,
|
71
|
+
with_time_in_transit: false,
|
72
|
+
package_options_class: FriendlyShipping::Services::Ups::RateEstimatePackageOptions,
|
73
|
+
**kwargs
|
74
|
+
)
|
75
|
+
@carbon_neutral = carbon_neutral
|
76
|
+
@customer_context = customer_context
|
77
|
+
@customer_classification = customer_classification
|
78
|
+
@destination_account = destination_account
|
79
|
+
@negotiated_rates = negotiated_rates
|
80
|
+
@shipper_number = shipper_number
|
81
|
+
@pickup_type = pickup_type
|
82
|
+
@saturday_delivery = saturday_delivery
|
83
|
+
@saturday_pickup = saturday_pickup
|
84
|
+
@shipper = shipper
|
85
|
+
@shipping_method = shipping_method
|
86
|
+
@with_time_in_transit = with_time_in_transit
|
87
|
+
super kwargs.merge(package_options_class: package_options_class)
|
88
|
+
end
|
89
|
+
|
90
|
+
def pickup_type_code
|
91
|
+
PICKUP_TYPE_CODES[@pickup_type]
|
92
|
+
end
|
93
|
+
|
94
|
+
def customer_classification_code
|
95
|
+
CUSTOMER_CLASSIFICATION_CODES[@customer_classification]
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'friendly_shipping/services/ups/label_item_options'
|
4
|
+
|
5
|
+
module FriendlyShipping
|
6
|
+
module Services
|
7
|
+
class Ups
|
8
|
+
# Package properties relevant for quoting a UPS package
|
9
|
+
#
|
10
|
+
# @option transmit_dimensions [Boolean] Whether to transmit the dimensions of this package, or quote only based on weight.
|
11
|
+
class RateEstimatePackageOptions < FriendlyShipping::PackageOptions
|
12
|
+
attr_reader :transmit_dimensions
|
13
|
+
|
14
|
+
def initialize(
|
15
|
+
transmit_dimensions: true,
|
16
|
+
**kwargs
|
17
|
+
)
|
18
|
+
@transmit_dimensions = transmit_dimensions
|
19
|
+
super kwargs
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -9,7 +9,8 @@ module FriendlyShipping
|
|
9
9
|
package:,
|
10
10
|
reference_numbers: {},
|
11
11
|
delivery_confirmation_code: nil,
|
12
|
-
shipper_release: false
|
12
|
+
shipper_release: false,
|
13
|
+
transmit_dimensions: true
|
13
14
|
)
|
14
15
|
xml.Package do
|
15
16
|
xml.PackagingType do
|
@@ -20,7 +21,7 @@ module FriendlyShipping
|
|
20
21
|
xml.Description(package.items.map(&:description).compact.join(', '))
|
21
22
|
end
|
22
23
|
|
23
|
-
if package.dimensions.all? { |dim| !dim.value.zero? && !dim.value.infinite? }
|
24
|
+
if transmit_dimensions && package.dimensions.all? { |dim| !dim.value.zero? && !dim.value.infinite? }
|
24
25
|
xml.Dimensions do
|
25
26
|
xml.UnitOfMeasurement do
|
26
27
|
xml.Code('IN')
|
@@ -8,47 +8,30 @@ module FriendlyShipping
|
|
8
8
|
module Services
|
9
9
|
class Ups
|
10
10
|
class SerializeRatingServiceSelectionRequest
|
11
|
-
|
12
|
-
|
13
|
-
customer_counter: "03",
|
14
|
-
one_time_pickup: "06",
|
15
|
-
on_call_air: "07",
|
16
|
-
suggested_retail_rates: "11",
|
17
|
-
letter_center: "19",
|
18
|
-
air_service_center: "20"
|
19
|
-
)
|
20
|
-
|
21
|
-
CUSTOMER_CLASSIFICATIONS = HashWithIndifferentAccess.new(
|
22
|
-
shipper_number: "00",
|
23
|
-
daily_rates: "01",
|
24
|
-
retail_rates: "04",
|
25
|
-
regional_rates: "05",
|
26
|
-
general_rates: "06",
|
27
|
-
standard_rates: "53"
|
28
|
-
)
|
29
|
-
|
30
|
-
def self.call(shipment:)
|
31
|
-
shipper = shipment.options[:shipper] || shipment.origin
|
32
|
-
pickup_type = PICKUP_CODES[shipment.options[:pickup_type] || :daily_pickup]
|
33
|
-
customer_classification = CUSTOMER_CLASSIFICATIONS[
|
34
|
-
shipment.options[:customer_classification] || :daily_rates
|
35
|
-
]
|
36
|
-
origin_account = shipment.options[:origin_account]
|
37
|
-
destination_account = shipment.options[:destination_account]
|
11
|
+
def self.call(shipment:, options:)
|
12
|
+
shipper = options.shipper || shipment.origin
|
38
13
|
|
39
14
|
xml_builder = Nokogiri::XML::Builder.new do |xml|
|
40
15
|
xml.RatingServiceSelectionRequest do
|
41
16
|
xml.Request do
|
42
17
|
xml.RequestAction('Rate')
|
43
|
-
|
18
|
+
# If no shipping method is given, request all of them
|
19
|
+
# I one is given, omit the request option. It then becomes "Rate", the default.
|
20
|
+
xml.RequestOption('Shop') unless options.shipping_method
|
44
21
|
xml.SubVersion('1707')
|
22
|
+
# Optional element to identify transactions between client and server.
|
23
|
+
if options.customer_context
|
24
|
+
xml.TransactionReference do
|
25
|
+
xml.CustomerContext(options.customer_context)
|
26
|
+
end
|
27
|
+
end
|
45
28
|
end
|
46
29
|
|
47
30
|
xml.PickupType do
|
48
|
-
xml.Code(
|
31
|
+
xml.Code(options.pickup_type_code)
|
49
32
|
end
|
50
33
|
xml.CustomerClassification do
|
51
|
-
xml.Code(
|
34
|
+
xml.Code(options.customer_classification_code)
|
52
35
|
end
|
53
36
|
|
54
37
|
xml.Shipment do
|
@@ -56,14 +39,14 @@ module FriendlyShipping
|
|
56
39
|
xml.Shipper do
|
57
40
|
SerializeAddressSnippet.call(xml: xml, location: shipper)
|
58
41
|
|
59
|
-
xml.ShipperNumber(
|
42
|
+
xml.ShipperNumber(options.shipper_number)
|
60
43
|
end
|
61
44
|
|
62
45
|
xml.ShipTo do
|
63
46
|
SerializeAddressSnippet.call(xml: xml, location: shipment.destination)
|
64
47
|
|
65
|
-
if destination_account
|
66
|
-
xml.ShipperAssignedIdentificationNumber(destination_account)
|
48
|
+
if options.destination_account
|
49
|
+
xml.ShipperAssignedIdentificationNumber(options.destination_account)
|
67
50
|
end
|
68
51
|
end
|
69
52
|
|
@@ -74,7 +57,30 @@ module FriendlyShipping
|
|
74
57
|
end
|
75
58
|
|
76
59
|
shipment.packages.each do |package|
|
77
|
-
|
60
|
+
package_options = options.options_for_package(package)
|
61
|
+
SerializePackageNode.call(
|
62
|
+
xml: xml,
|
63
|
+
package: package,
|
64
|
+
transmit_dimensions: package_options.transmit_dimensions
|
65
|
+
)
|
66
|
+
end
|
67
|
+
|
68
|
+
if options.shipping_method
|
69
|
+
xml.Service do
|
70
|
+
xml.Code options.shipping_method.service_code
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
xml.ShipmentServiceOptions do
|
75
|
+
xml.UPScarbonneutralIndicator if options.carbon_neutral
|
76
|
+
xml.SaturdayDelivery if options.saturday_delivery
|
77
|
+
xml.SaturdayPickup if options.saturday_pickup
|
78
|
+
end
|
79
|
+
|
80
|
+
if options.negotiated_rates
|
81
|
+
xml.RateInformation do
|
82
|
+
xml.NegotiatedRatesIndicator if options.negotiated_rates
|
83
|
+
end
|
78
84
|
end
|
79
85
|
end
|
80
86
|
end
|
@@ -11,6 +11,7 @@ require 'friendly_shipping/services/usps/parse_city_state_lookup_response'
|
|
11
11
|
require 'friendly_shipping/services/usps/parse_rate_response'
|
12
12
|
require 'friendly_shipping/services/usps/parse_time_in_transit_response'
|
13
13
|
require 'friendly_shipping/services/usps/timing_options'
|
14
|
+
require 'friendly_shipping/services/usps/rate_estimate_options'
|
14
15
|
|
15
16
|
module FriendlyShipping
|
16
17
|
module Services
|
@@ -48,25 +49,19 @@ module FriendlyShipping
|
|
48
49
|
|
49
50
|
# Get rate estimates from USPS
|
50
51
|
#
|
51
|
-
# @param [Physical::Shipment] shipment
|
52
|
-
#
|
53
|
-
#
|
54
|
-
# @property [Symbol] box_name The type of box we want to get rates for. Has to be one of the keys
|
55
|
-
# of FriendlyShipping::Services::Usps::CONTAINERS.
|
56
|
-
# @property [Boolean] commercial_pricing Whether we prefer commercial pricing results or retail results
|
57
|
-
# @property [Boolean] hold_for_pickup Whether we want a rate with Hold For Pickup Service
|
58
|
-
# @param [Physical::ShippingMethod] shipping_method The shipping method ("service" in USPS parlance) we want
|
59
|
-
# to get rates for.
|
52
|
+
# @param [Physical::Shipment] shipment
|
53
|
+
# @param [FriendlyShipping::Services::Usps::RateEstimateOptions] options What options
|
54
|
+
# to use for this rate estimate call
|
60
55
|
#
|
61
56
|
# @return [Result<Array<FriendlyShipping::Rate>>] When successfully parsing, an array of rates in a Success Monad.
|
62
57
|
# When the parsing is not successful or USPS can't give us rates, a Failure monad containing something that
|
63
58
|
# can be serialized into an error message using `to_s`.
|
64
|
-
def rate_estimates(shipment,
|
65
|
-
rate_request_xml = SerializeRateRequest.call(shipment: shipment, login: login,
|
59
|
+
def rate_estimates(shipment, options: RateEstimateOptions.new, debug: false)
|
60
|
+
rate_request_xml = SerializeRateRequest.call(shipment: shipment, login: login, options: options)
|
66
61
|
request = build_request(api: :rates, xml: rate_request_xml, debug: debug)
|
67
62
|
|
68
63
|
client.post(request).bind do |response|
|
69
|
-
ParseRateResponse.call(response: response, request: request, shipment: shipment)
|
64
|
+
ParseRateResponse.call(response: response, request: request, shipment: shipment, options: options)
|
70
65
|
end
|
71
66
|
end
|
72
67
|
|
@@ -16,19 +16,18 @@ module FriendlyShipping
|
|
16
16
|
# @param [Array<FriendlyShipping::Rate>] The rates we select from
|
17
17
|
#
|
18
18
|
# @return [FriendlyShipping::Rate] The rate that most closely matches our package
|
19
|
-
def self.call(shipping_method,
|
19
|
+
def self.call(shipping_method, rates, package_options)
|
20
20
|
# Keep all rates with the requested shipping method
|
21
21
|
rates_with_this_shipping_method = rates.select { |r| r.shipping_method == shipping_method }
|
22
22
|
|
23
23
|
# Keep only rates with the package type of this package
|
24
24
|
rates_with_this_package_type = rates_with_this_shipping_method.select do |r|
|
25
|
-
r.data[:box_name] ==
|
26
|
-
r.data[:box_name] == :flat_rate_boxes && package.properties[:box_name]&.match?(FLAT_RATE_BOX)
|
25
|
+
r.data[:box_name] == package_options.box_name
|
27
26
|
end
|
28
27
|
|
29
28
|
# Filter by our package's `hold_for_pickup` option
|
30
29
|
rates_with_this_hold_for_pickup_option = rates_with_this_package_type.select do |r|
|
31
|
-
r.data[:hold_for_pickup] ==
|
30
|
+
r.data[:hold_for_pickup] == package_options.hold_for_pickup
|
32
31
|
end
|
33
32
|
|
34
33
|
# At this point, we have one or two rates left, and they're similar enough.
|
@@ -57,7 +57,7 @@ module FriendlyShipping
|
|
57
57
|
CURRENCY = Money::Currency.new('USD').freeze
|
58
58
|
|
59
59
|
class << self
|
60
|
-
def call(rate_node, package)
|
60
|
+
def call(rate_node, package, package_options)
|
61
61
|
# "A mail class identifier for the postage returned. Not necessarily unique within a <Package/>."
|
62
62
|
# (from the USPS docs). We save this on the data Hash, but do not use it for identifying shipping methods.
|
63
63
|
service_code = rate_node.attributes[SERVICE_CODE_TAG].value
|
@@ -84,7 +84,7 @@ module FriendlyShipping
|
|
84
84
|
# In these cases, return the commercial rate instead of the normal rate.
|
85
85
|
# Some rates are available in both commercial and retail pricing - if we want the commercial pricing here,
|
86
86
|
# we need to specify the commercial_pricing property on the `Physical::Package`.
|
87
|
-
rate_value = if (
|
87
|
+
rate_value = if (package_options.commercial_pricing || rate_node.at(RATE_TAG).text.to_d.zero?) && rate_node.at(COMMERCIAL_RATE_TAG)
|
88
88
|
rate_node.at(COMMERCIAL_RATE_TAG).text.to_d
|
89
89
|
else
|
90
90
|
rate_node.at(RATE_TAG).text.to_d
|
@@ -107,9 +107,7 @@ module FriendlyShipping
|
|
107
107
|
# We find out the box name using a bit of Regex magic using named captures. See the `BOX_REGEX`
|
108
108
|
# constant above.
|
109
109
|
box_name_match = service_name.match(/#{BOX_REGEX}/)
|
110
|
-
box_name =
|
111
|
-
box_name_match.named_captures.compact.keys.last.to_sym
|
112
|
-
end
|
110
|
+
box_name = box_name_match ? box_name_match.named_captures.compact.keys.last.to_sym : :variable
|
113
111
|
|
114
112
|
# Combine all the gathered information in a FriendlyShipping::Rate object.
|
115
113
|
# Careful: This rate is only for one package within the shipment, and we get multiple
|
@@ -16,20 +16,24 @@ module FriendlyShipping
|
|
16
16
|
# @param [FriendlyShipping::Request] request The request that was used to obtain this Response
|
17
17
|
# @param [FriendlyShipping::Response] response The response that USPS returned
|
18
18
|
# @param [Physical::Shipment] shipment The shipment object we're trying to get results for
|
19
|
+
# @param [FriendlyShipping::Services::Usps::RateEstimateOptions] options The options we sent with this request
|
19
20
|
# @return [Result<ApiResult<Array<FriendlyShipping::Rate>>>] When successfully parsing, an array of rates in a Success Monad.
|
20
|
-
def call(request:, response:, shipment:)
|
21
|
+
def call(request:, response:, shipment:, options:)
|
21
22
|
# Filter out error responses and directly return a failure
|
22
23
|
parsing_result = ParseXMLResponse.call(response.body, 'RateV4Response')
|
23
24
|
rates = []
|
24
25
|
parsing_result.fmap do |xml|
|
25
26
|
# Get all the possible rates for each package
|
26
|
-
rates_by_package = rates_from_response_node(xml, shipment)
|
27
|
+
rates_by_package = rates_from_response_node(xml, shipment, options)
|
27
28
|
|
28
29
|
rates = SHIPPING_METHODS.map do |shipping_method|
|
29
30
|
# For every package ...
|
30
31
|
matching_rates = rates_by_package.map do |package, package_rates|
|
31
32
|
# ... choose the rate that fits this package best.
|
32
|
-
|
33
|
+
|
34
|
+
package_options = options.options_for_package(package)
|
35
|
+
|
36
|
+
ChoosePackageRate.call(shipping_method, package_rates, package_options)
|
33
37
|
end.compact # Some shipping rates are not available for every shipping method.
|
34
38
|
|
35
39
|
# in this case, go to the next shipping method.
|
@@ -62,17 +66,17 @@ module FriendlyShipping
|
|
62
66
|
# @param [Physical::Shipment] shipment The shipment we're trying to get rates for
|
63
67
|
#
|
64
68
|
# @return [Hash<Physical::Package => Array<FriendlyShipping::Rate>>]
|
65
|
-
def rates_from_response_node(xml, shipment)
|
69
|
+
def rates_from_response_node(xml, shipment, options)
|
66
70
|
xml.xpath(PACKAGE_NODE_XPATH).each_with_object({}) do |package_node, result|
|
67
71
|
package_id = package_node['ID']
|
68
72
|
corresponding_package = shipment.packages[package_id.to_i]
|
69
|
-
|
73
|
+
package_options = options.options_for_package(corresponding_package)
|
70
74
|
# There should always be a package in the original shipment that corresponds to the package ID
|
71
75
|
# in the USPS response.
|
72
76
|
raise BoxNotFoundError if corresponding_package.nil?
|
73
77
|
|
74
78
|
result[corresponding_package] = package_node.xpath(SERVICE_NODE_NAME).map do |service_node|
|
75
|
-
ParsePackageRate.call(service_node, corresponding_package)
|
79
|
+
ParsePackageRate.call(service_node, corresponding_package, package_options)
|
76
80
|
end
|
77
81
|
end
|
78
82
|
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'friendly_shipping/services/usps/rate_estimate_package_options'
|
4
|
+
|
5
|
+
module FriendlyShipping
|
6
|
+
module Services
|
7
|
+
# Option container for rating a shipment via USPS
|
8
|
+
#
|
9
|
+
# Context: The shipment object we're trying to get results for
|
10
|
+
# USPS returns rates on a package-by-package basis, so the options for obtaining rates are
|
11
|
+
# set on the [FriendlyShipping/RateEstimateobect] hash. The possible options are:
|
12
|
+
|
13
|
+
# @param [Physical::ShippingMethod] shipping_method The shipping method ("service" in USPS parlance) we want
|
14
|
+
# to get rates for.
|
15
|
+
# @param [Boolean] commercial_pricing Whether we prefer commercial pricing results or retail results
|
16
|
+
# @param [Boolean] hold_for_pickup Whether we want a rate with Hold For Pickup Service
|
17
|
+
class Usps
|
18
|
+
class RateEstimateOptions < FriendlyShipping::ShipmentOptions
|
19
|
+
def initialize(
|
20
|
+
package_options_class: FriendlyShipping::Services::Usps::RateEstimatePackageOptions,
|
21
|
+
**kwargs
|
22
|
+
)
|
23
|
+
super kwargs.merge(package_options_class: package_options_class)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'friendly_shipping/services/ups/rate_estimate_package_options'
|
4
|
+
|
5
|
+
module FriendlyShipping
|
6
|
+
module Services
|
7
|
+
# Options for one package when rating
|
8
|
+
#
|
9
|
+
# @param [Symbol] box_name The type of box we want to get rates for. Has to be one of the keys
|
10
|
+
# of FriendlyShipping::Services::Usps::CONTAINERS.
|
11
|
+
class Usps
|
12
|
+
class RateEstimatePackageOptions < FriendlyShipping::PackageOptions
|
13
|
+
attr_reader :box_name,
|
14
|
+
:commercial_pricing,
|
15
|
+
:first_class_mail_type,
|
16
|
+
:hold_for_pickup,
|
17
|
+
:shipping_method,
|
18
|
+
:transmit_dimensions
|
19
|
+
|
20
|
+
def initialize(
|
21
|
+
box_name: :variable,
|
22
|
+
commercial_pricing: false,
|
23
|
+
first_class_mail_type: nil,
|
24
|
+
hold_for_pickup: false,
|
25
|
+
shipping_method: nil,
|
26
|
+
transmit_dimensions: true,
|
27
|
+
**kwargs
|
28
|
+
)
|
29
|
+
@box_name = CONTAINERS.key?(box_name) ? box_name : :variable
|
30
|
+
@commercial_pricing = commercial_pricing
|
31
|
+
@first_class_mail_type = first_class_mail_type
|
32
|
+
@hold_for_pickup = hold_for_pickup
|
33
|
+
@shipping_method = shipping_method
|
34
|
+
@transmit_dimensions = transmit_dimensions
|
35
|
+
super kwargs
|
36
|
+
end
|
37
|
+
|
38
|
+
def container_code
|
39
|
+
CONTAINERS.fetch(box_name)
|
40
|
+
end
|
41
|
+
|
42
|
+
def first_class_mail_type_code
|
43
|
+
FIRST_CLASS_MAIL_TYPES[first_class_mail_type]
|
44
|
+
end
|
45
|
+
|
46
|
+
def service_code
|
47
|
+
return 'ALL' unless shipping_method
|
48
|
+
|
49
|
+
if commercial_pricing
|
50
|
+
"#{shipping_method.service_code} COMMERCIAL"
|
51
|
+
else
|
52
|
+
shipping_method.service_code
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -16,24 +16,24 @@ module FriendlyShipping
|
|
16
16
|
# @param [FriendlyShipping::ShippingMethod] shipping_method The shipping method we want to get rates
|
17
17
|
# for. If empty, we get all of them
|
18
18
|
# @return Array<[FriendlyShipping::Rate]> A set of Rates that this package may be sent with
|
19
|
-
def call(shipment:, login:,
|
19
|
+
def call(shipment:, login:, options:)
|
20
20
|
xml_builder = Nokogiri::XML::Builder.new do |xml|
|
21
21
|
xml.RateV4Request('USERID' => login) do
|
22
22
|
shipment.packages.each_with_index do |package, index|
|
23
|
+
package_options = options.options_for_package(package)
|
23
24
|
xml.Package('ID' => index) do
|
24
|
-
xml.Service(
|
25
|
-
if
|
26
|
-
xml.FirstClassMailType(
|
25
|
+
xml.Service(package_options.service_code)
|
26
|
+
if package_options.first_class_mail_type
|
27
|
+
xml.FirstClassMailType(package_options.first_class_mail_type_code)
|
27
28
|
end
|
28
29
|
xml.ZipOrigination(shipment.origin.zip)
|
29
30
|
xml.ZipDestination(shipment.destination.zip)
|
30
31
|
xml.Pounds(0)
|
31
32
|
xml.Ounces(ounces_for(package))
|
32
33
|
size_code = size_code_for(package)
|
33
|
-
|
34
|
-
xml.Container(container)
|
34
|
+
xml.Container(package_options.container_code)
|
35
35
|
xml.Size(size_code)
|
36
|
-
if
|
36
|
+
if package_options.transmit_dimensions && package_options.container_code == 'VARIABLE'
|
37
37
|
xml.Width("%<width>0.2f" % { width: package.width.convert_to(:inches).value.to_f })
|
38
38
|
xml.Length("%<length>0.2f" % { length: package.length.convert_to(:inches).value.to_f })
|
39
39
|
xml.Height("%<height>0.2f" % { height: package.height.convert_to(:inches).value.to_f })
|
@@ -61,16 +61,6 @@ module FriendlyShipping
|
|
61
61
|
end
|
62
62
|
end
|
63
63
|
|
64
|
-
def service_code_by(shipping_method, package)
|
65
|
-
return 'ALL' unless shipping_method
|
66
|
-
|
67
|
-
if package.properties[:commercial_pricing]
|
68
|
-
"#{shipping_method.service_code} COMMERCIAL"
|
69
|
-
else
|
70
|
-
shipping_method.service_code
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
74
64
|
def ounces_for(package)
|
75
65
|
ounces = package.weight.convert_to(:ounces).value.to_f.round(2).ceil
|
76
66
|
ounces == 16 ? 15.999 : [ounces, 1].max
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: friendly_shipping
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: '0.5'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Martin Meyerhoff
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-01-
|
11
|
+
date: 2020-01-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: data_uri
|
@@ -309,6 +309,8 @@ files:
|
|
309
309
|
- lib/friendly_shipping/services/ups/parse_time_in_transit_response.rb
|
310
310
|
- lib/friendly_shipping/services/ups/parse_void_shipment_response.rb
|
311
311
|
- lib/friendly_shipping/services/ups/parse_xml_response.rb
|
312
|
+
- lib/friendly_shipping/services/ups/rate_estimate_options.rb
|
313
|
+
- lib/friendly_shipping/services/ups/rate_estimate_package_options.rb
|
312
314
|
- lib/friendly_shipping/services/ups/serialize_access_request.rb
|
313
315
|
- lib/friendly_shipping/services/ups/serialize_address_snippet.rb
|
314
316
|
- lib/friendly_shipping/services/ups/serialize_address_validation_request.rb
|
@@ -342,6 +344,8 @@ files:
|
|
342
344
|
- lib/friendly_shipping/services/usps/parse_rate_response.rb
|
343
345
|
- lib/friendly_shipping/services/usps/parse_time_in_transit_response.rb
|
344
346
|
- lib/friendly_shipping/services/usps/parse_xml_response.rb
|
347
|
+
- lib/friendly_shipping/services/usps/rate_estimate_options.rb
|
348
|
+
- lib/friendly_shipping/services/usps/rate_estimate_package_options.rb
|
345
349
|
- lib/friendly_shipping/services/usps/serialize_address_validation_request.rb
|
346
350
|
- lib/friendly_shipping/services/usps/serialize_city_state_lookup_request.rb
|
347
351
|
- lib/friendly_shipping/services/usps/serialize_rate_request.rb
|
@@ -365,7 +369,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
365
369
|
requirements:
|
366
370
|
- - ">="
|
367
371
|
- !ruby/object:Gem::Version
|
368
|
-
version: '2.
|
372
|
+
version: '2.5'
|
369
373
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
370
374
|
requirements:
|
371
375
|
- - ">="
|