friendly_shipping 0.2.6 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +72 -7
- data/lib/friendly_shipping.rb +2 -0
- data/lib/friendly_shipping/api_failure.rb +21 -0
- data/lib/friendly_shipping/api_result.rb +17 -0
- data/lib/friendly_shipping/request.rb +3 -2
- data/lib/friendly_shipping/services/ship_engine.rb +28 -6
- data/lib/friendly_shipping/services/ship_engine/parse_carrier_response.rb +32 -28
- data/lib/friendly_shipping/services/ship_engine/parse_label_response.rb +15 -15
- data/lib/friendly_shipping/services/ship_engine/parse_rate_estimate_response.rb +11 -1
- data/lib/friendly_shipping/services/ship_engine/parse_void_response.rb +11 -9
- data/lib/friendly_shipping/services/ups.rb +47 -1
- data/lib/friendly_shipping/services/ups/parse_address_validation_response.rb +50 -0
- data/lib/friendly_shipping/services/ups/parse_city_state_lookup_response.rb +30 -0
- data/lib/friendly_shipping/services/ups/parse_rate_response.rb +31 -25
- data/lib/friendly_shipping/services/ups/serialize_address_validation_request.rb +39 -0
- data/lib/friendly_shipping/services/ups/serialize_city_state_lookup_request.rb +26 -0
- data/lib/friendly_shipping/services/usps.rb +43 -3
- data/lib/friendly_shipping/services/usps/parse_address_validation_response.rb +39 -0
- data/lib/friendly_shipping/services/usps/parse_city_state_lookup_response.rb +37 -0
- data/lib/friendly_shipping/services/usps/parse_rate_response.rb +12 -7
- data/lib/friendly_shipping/services/usps/parse_xml_response.rb +6 -6
- data/lib/friendly_shipping/services/usps/serialize_address_validation_request.rb +25 -0
- data/lib/friendly_shipping/services/usps/serialize_city_state_lookup_request.rb +20 -0
- data/lib/friendly_shipping/services/usps/serialize_rate_request.rb +4 -4
- data/lib/friendly_shipping/version.rb +1 -1
- metadata +12 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 821bec044ee86ff1e468dd392ae55c475510af6c
|
4
|
+
data.tar.gz: 153b7226c2e01afe5b6d7fdc260e39b712986ae5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a1ad8abee5b1a56f33cc8c5b8a4d55d4bce1f056e74a0719ea237ab577587921b43ada0ca50f35ab52bba261b2fc18598e612a82185a9d172c3d45918cf07f8e
|
7
|
+
data.tar.gz: 29a07b0165a570d6cbf5c008e2ff1a67aa6ce2e245be10357cbaaeb13b88c888e74419dec65ca9df7ce147e38524eb1b42a55393dba6f17c8cf2d2e4cef61338
|
data/README.md
CHANGED
@@ -1,8 +1,6 @@
|
|
1
|
-
# FriendlyShipping
|
1
|
+
# FriendlyShipping - the friendly shipping provider API wrapper
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
TODO: Delete this and the text above, and describe your gem
|
3
|
+
This gem provides wrappers for popular shipping provider APIs. Currently, there are implementations for rate quoting and address validation, as well as shipping label generation via the `ShipStation` API.
|
6
4
|
|
7
5
|
## Installation
|
8
6
|
|
@@ -22,7 +20,74 @@ Or install it yourself as:
|
|
22
20
|
|
23
21
|
## Usage
|
24
22
|
|
25
|
-
|
23
|
+
The entry point for using FriendlyShipping are the `service` objects that each represent an API provider. Currently, there are three services:
|
24
|
+
|
25
|
+
1. `FriendlyShipping::Services::Ups`
|
26
|
+
2. `FriendlyShipping::Services::Usps`
|
27
|
+
3. `FriendlyShipping::Services::ShipEngine`
|
28
|
+
|
29
|
+
The services are instantiated with the credentials they need as well as a `test` flag to indicate whether to use their respective `sandbox` environments.
|
30
|
+
|
31
|
+
The individual API endpoints can be reached through the public methods of the respective service objects, such as `#rate_estimates`, `#labels`, or `#address_validation`. This gem makes heavy use of classes from the [`physical` gem](https://github.com/friendlycart/physical) to represent the physical world (shipments, packages, addresses, and items).
|
32
|
+
|
33
|
+
### Return values
|
34
|
+
|
35
|
+
All calls to `FriendlyShipping` will return a `Dry::Monads::Result` instance, so either a `Success` or `Failure`. Wrapped inside this monad is a `FriendlyShipping::ApiResult` (in the success case) or a `FriendlyShipping::ApiFailure` object that contains the desired data under `#data` `ApiResult` and `ApiFailure` also contain the original request generated by this gem and the response returned by the respective service.
|
36
|
+
|
37
|
+
### Supported Services
|
38
|
+
|
39
|
+
#### ShipEngine
|
40
|
+
|
41
|
+
The service class for ShipEngine is `FriendlyShipping::Services::ShipEngine`. Initialize like so:
|
42
|
+
|
43
|
+
```rb
|
44
|
+
service = FriendlyShipping::Services::ShipEngine.new(token: ENV['SHIPENGINE_TOKEN'], test: true)
|
45
|
+
```
|
46
|
+
|
47
|
+
The following methods are supported:
|
48
|
+
|
49
|
+
- `#carriers` - List all configured carriers
|
50
|
+
- `#rate_estimates(physical_shipment, carriers: [friendly_shipping_carrier])` - Get rate estimates for a shipment
|
51
|
+
- `#labels(physical_shipment)` - Get labels for a shipments. Currently only supports USPS labels, other services are untested. The API of this method is still subject to change.
|
52
|
+
- `#void(physical_label)` - Void a label and get the cost refunded
|
53
|
+
|
54
|
+
#### UPS (United Parcel Service)
|
55
|
+
|
56
|
+
The service class for UPS is `FriendlyShipping::Services::Ups`. Initialize like so:
|
57
|
+
|
58
|
+
```rb
|
59
|
+
service = FriendlyShipping::Services::Ups.new(
|
60
|
+
key: ENV['UPS_API_KEY'],
|
61
|
+
login: ENV['UPS_API_LOGIN'],
|
62
|
+
password: ENV['UPS_API_PASSWORD'],
|
63
|
+
test: true
|
64
|
+
)
|
65
|
+
```
|
66
|
+
|
67
|
+
The following methods are supported:
|
68
|
+
|
69
|
+
- `#carriers` - List all configured carriers (always returns UPS)
|
70
|
+
- `#rate_estimates(physical_shipment)` - Get rate estimates for a shipment
|
71
|
+
- `#address_validation(physical_location)` - Perform a detailed address validation and determine whether an address is commercial or residential.
|
72
|
+
- `#city_state_lookup(physical_location)` - Lookup City and State for a given ZIP code.
|
73
|
+
|
74
|
+
#### USPS (United States Postal Service)
|
75
|
+
|
76
|
+
The service class for USPS is `FriendlyShipping::Services::Usps`. Initialize like so:
|
77
|
+
|
78
|
+
```rb
|
79
|
+
service = FriendlyShipping::Services::Usps.new(
|
80
|
+
login: ENV['USPS_API_LOGIN'],
|
81
|
+
test: true
|
82
|
+
)
|
83
|
+
```
|
84
|
+
|
85
|
+
The following methods are supported:
|
86
|
+
|
87
|
+
- `#carriers` - List all configured carriers (always returns USPS)
|
88
|
+
- `#rate_estimates(physical_shipment)` - Get rate estimates for a shipment
|
89
|
+
- `#address_validation(physical_location)` - Perform a detailed address validation and determine whether an address is commercial or residential.
|
90
|
+
- `#city_state_lookup(physical_location)` - Lookup City and State for a given ZIP code.
|
26
91
|
|
27
92
|
## Development
|
28
93
|
|
@@ -32,7 +97,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
32
97
|
|
33
98
|
## Contributing
|
34
99
|
|
35
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
100
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/friendlycart/friendly_shipping. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
36
101
|
|
37
102
|
## License
|
38
103
|
|
@@ -40,4 +105,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
40
105
|
|
41
106
|
## Code of Conduct
|
42
107
|
|
43
|
-
Everyone interacting in the FriendlyShipping project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/
|
108
|
+
Everyone interacting in the FriendlyShipping project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/friendlycart/friendly_shipping/blob/master/CODE_OF_CONDUCT.md).
|
data/lib/friendly_shipping.rb
CHANGED
@@ -9,6 +9,8 @@ require "friendly_shipping/carrier"
|
|
9
9
|
require "friendly_shipping/shipping_method"
|
10
10
|
require "friendly_shipping/label"
|
11
11
|
require "friendly_shipping/rate"
|
12
|
+
require "friendly_shipping/api_result"
|
13
|
+
require "friendly_shipping/api_failure"
|
12
14
|
|
13
15
|
require "friendly_shipping/services/ship_engine"
|
14
16
|
require "friendly_shipping/services/ups"
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FriendlyShipping
|
4
|
+
class ApiFailure
|
5
|
+
attr_reader :failure, :original_request, :original_response
|
6
|
+
|
7
|
+
def initialize(failure, original_request:, original_response:)
|
8
|
+
@failure = failure
|
9
|
+
|
10
|
+
# We do not want to attach debugging information in every single response to save memory in production
|
11
|
+
return unless original_request&.debug
|
12
|
+
|
13
|
+
@original_request = original_request
|
14
|
+
@original_response = original_response
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_s
|
18
|
+
failure.to_s
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FriendlyShipping
|
4
|
+
class ApiResult
|
5
|
+
attr_reader :data, :original_request, :original_response
|
6
|
+
|
7
|
+
def initialize(data, original_request: nil, original_response: nil)
|
8
|
+
@data = data
|
9
|
+
|
10
|
+
# We do not want to attach debugging information in every single response to save memory in production
|
11
|
+
return unless original_request&.debug
|
12
|
+
|
13
|
+
@original_request = original_request
|
14
|
+
@original_response = original_response
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -2,12 +2,13 @@
|
|
2
2
|
|
3
3
|
module FriendlyShipping
|
4
4
|
class Request
|
5
|
-
attr_reader :url, :body, :headers
|
5
|
+
attr_reader :url, :body, :headers, :debug
|
6
6
|
|
7
|
-
def initialize(url:, body: nil, headers: {})
|
7
|
+
def initialize(url:, body: nil, headers: {}, debug: false)
|
8
8
|
@url = url
|
9
9
|
@body = body
|
10
10
|
@headers = headers
|
11
|
+
@debug = debug
|
11
12
|
end
|
12
13
|
end
|
13
14
|
end
|
@@ -24,27 +24,49 @@ module FriendlyShipping
|
|
24
24
|
@client = client
|
25
25
|
end
|
26
26
|
|
27
|
+
# Get configured carriers from USPS
|
28
|
+
#
|
29
|
+
# @return [Result<ApiResult<Array<Carrier>>>] Carriers configured in your shipstation account
|
27
30
|
def carriers
|
28
31
|
request = FriendlyShipping::Request.new(
|
29
32
|
url: API_BASE + API_PATHS[:carriers],
|
30
33
|
headers: request_headers
|
31
34
|
)
|
32
35
|
client.get(request).fmap do |response|
|
33
|
-
ParseCarrierResponse.
|
36
|
+
ParseCarrierResponse.call(request: request, response: response)
|
34
37
|
end
|
35
38
|
end
|
36
39
|
|
37
|
-
|
40
|
+
# Get rate estimates from ShipEngine
|
41
|
+
#
|
42
|
+
# @param [Physical::Shipment] shipment The shipment object we're trying to get results for
|
43
|
+
#
|
44
|
+
# @options[:carriers] [Physical::Carrier] The carriers we want to get rates from. What counts
|
45
|
+
# here is the carrier code, so by specifying them upfront you can save a request.
|
46
|
+
#
|
47
|
+
# @return [Result<ApiResult<Array<FriendlyShipping::Rate>>>] When successfully parsing, an array of rates in a Success Monad.
|
48
|
+
# When the parsing is not successful or ShipEngine can't give us rates, a Failure monad containing something that
|
49
|
+
# can be serialized into an error message using `to_s`.
|
50
|
+
def rate_estimates(shipment, options = {})
|
51
|
+
selected_carriers = options[:carriers] || carriers.value!.data
|
38
52
|
request = FriendlyShipping::Request.new(
|
39
53
|
url: API_BASE + 'rates/estimate',
|
40
|
-
body: SerializeRateEstimateRequest.call(shipment: shipment, carriers:
|
54
|
+
body: SerializeRateEstimateRequest.call(shipment: shipment, carriers: selected_carriers).to_json,
|
41
55
|
headers: request_headers
|
42
56
|
)
|
43
|
-
client.post(request).
|
44
|
-
ParseRateEstimateResponse.call(response: response, request: request, carriers:
|
57
|
+
client.post(request).bind do |response|
|
58
|
+
ParseRateEstimateResponse.call(response: response, request: request, carriers: selected_carriers)
|
45
59
|
end
|
46
60
|
end
|
47
61
|
|
62
|
+
# Get label(s) from ShipEngine
|
63
|
+
#
|
64
|
+
# @param [Physical::Shipment] shipment The shipment object we're trying to get labels for
|
65
|
+
# Note: Some ShipEngine carriers, notably USPS, only support one package per shipment, and that's
|
66
|
+
# all that the integration supports at this point.
|
67
|
+
#
|
68
|
+
# @return [Result<ApiResult<Array<FriendlyShipping::Label>>>] The label returned.
|
69
|
+
#
|
48
70
|
def labels(shipment)
|
49
71
|
request = FriendlyShipping::Request.new(
|
50
72
|
url: API_BASE + API_PATHS[:labels],
|
@@ -63,7 +85,7 @@ module FriendlyShipping
|
|
63
85
|
headers: request_headers
|
64
86
|
)
|
65
87
|
client.put(request).bind do |response|
|
66
|
-
ParseVoidResponse.
|
88
|
+
ParseVoidResponse.call(request: request, response: response)
|
67
89
|
end
|
68
90
|
end
|
69
91
|
|
@@ -6,41 +6,45 @@ module FriendlyShipping
|
|
6
6
|
module Services
|
7
7
|
class ShipEngine
|
8
8
|
class ParseCarrierResponse
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
class << self
|
10
|
+
def call(request:, response:)
|
11
|
+
parsed_json = JSON.parse(response.body)
|
12
|
+
carriers = parsed_json['carriers'].map do |carrier_data|
|
13
|
+
carrier = FriendlyShipping::Carrier.new(
|
14
|
+
id: carrier_data['carrier_id'],
|
15
|
+
name: carrier_data['friendly_name'],
|
16
|
+
code: carrier_data['carrier_code'],
|
17
|
+
balance: carrier_data['balance'],
|
18
|
+
data: carrier_data
|
19
|
+
)
|
12
20
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
id: carrier_data['carrier_id'],
|
18
|
-
name: carrier_data['friendly_name'],
|
19
|
-
code: carrier_data['carrier_code'],
|
20
|
-
balance: carrier_data['balance'],
|
21
|
-
data: carrier_data
|
22
|
-
)
|
21
|
+
carrier_data['services'].each do |method_hash|
|
22
|
+
shipping_method = parse_shipping_method(carrier, method_hash)
|
23
|
+
carrier.shipping_methods << shipping_method
|
24
|
+
end
|
23
25
|
|
24
|
-
|
25
|
-
shipping_method = parse_shipping_method(carrier, method_hash)
|
26
|
-
carrier.shipping_methods << shipping_method
|
26
|
+
carrier
|
27
27
|
end
|
28
28
|
|
29
|
-
|
29
|
+
ApiResult.new(
|
30
|
+
carriers,
|
31
|
+
original_request: request,
|
32
|
+
original_response: response
|
33
|
+
)
|
30
34
|
end
|
31
|
-
end
|
32
35
|
|
33
|
-
|
36
|
+
private
|
34
37
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
38
|
+
def parse_shipping_method(carrier, shipping_method_data)
|
39
|
+
FriendlyShipping::ShippingMethod.new(
|
40
|
+
carrier: carrier,
|
41
|
+
name: shipping_method_data["name"],
|
42
|
+
service_code: shipping_method_data["service_code"],
|
43
|
+
domestic: shipping_method_data["domestic"],
|
44
|
+
international: shipping_method_data["international"],
|
45
|
+
multi_package: shipping_method_data["is_multi_package_supported"]
|
46
|
+
)
|
47
|
+
end
|
44
48
|
end
|
45
49
|
end
|
46
50
|
end
|
@@ -9,6 +9,7 @@ module FriendlyShipping
|
|
9
9
|
class ParseLabelResponse
|
10
10
|
def self.call(request:, response:)
|
11
11
|
parsed_json = JSON.parse(response.body)
|
12
|
+
|
12
13
|
label_uri_string = parsed_json['label_download']['href']
|
13
14
|
label_data = nil
|
14
15
|
label_url = nil
|
@@ -17,21 +18,20 @@ module FriendlyShipping
|
|
17
18
|
else
|
18
19
|
label_url = label_uri_string
|
19
20
|
end
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
]
|
21
|
+
|
22
|
+
label = FriendlyShipping::Label.new(
|
23
|
+
id: parsed_json['label_id'],
|
24
|
+
shipment_id: parsed_json['shipment_id'],
|
25
|
+
tracking_number: parsed_json['tracking_number'],
|
26
|
+
service_code: parsed_json['service_code'],
|
27
|
+
label_href: label_url,
|
28
|
+
label_data: label_data,
|
29
|
+
label_format: parsed_json['label_format'].to_sym,
|
30
|
+
shipment_cost: parsed_json['shipment_cost']['amount'],
|
31
|
+
data: parsed_json
|
32
|
+
)
|
33
|
+
|
34
|
+
ApiResult.new([label], original_request: request, original_response: response)
|
35
35
|
end
|
36
36
|
end
|
37
37
|
end
|
@@ -7,10 +7,12 @@ module FriendlyShipping
|
|
7
7
|
module Services
|
8
8
|
class ShipEngine
|
9
9
|
class ParseRateEstimateResponse
|
10
|
+
extend Dry::Monads::Result::Mixin
|
11
|
+
|
10
12
|
class << self
|
11
13
|
def call(response:, carriers:, request:)
|
12
14
|
parsed_json = JSON.parse(response.body)
|
13
|
-
parsed_json.map do |rate|
|
15
|
+
rates = parsed_json.map do |rate|
|
14
16
|
carrier = carriers.detect { |c| c.id == rate['carrier_id'] }
|
15
17
|
next unless carrier
|
16
18
|
|
@@ -29,6 +31,14 @@ module FriendlyShipping
|
|
29
31
|
original_response: response
|
30
32
|
)
|
31
33
|
end.compact
|
34
|
+
|
35
|
+
Success(
|
36
|
+
ApiResult.new(
|
37
|
+
rates,
|
38
|
+
original_request: request,
|
39
|
+
original_response: response
|
40
|
+
)
|
41
|
+
)
|
32
42
|
end
|
33
43
|
|
34
44
|
private
|
@@ -6,18 +6,20 @@ module FriendlyShipping
|
|
6
6
|
module Services
|
7
7
|
class ShipEngine
|
8
8
|
class ParseVoidResponse
|
9
|
-
|
9
|
+
extend Dry::Monads::Result::Mixin
|
10
10
|
|
11
|
-
|
12
|
-
|
13
|
-
def initialize(response:)
|
14
|
-
@response = response
|
15
|
-
end
|
16
|
-
|
17
|
-
def call
|
11
|
+
def self.call(request:, response:)
|
18
12
|
parsed_json = JSON.parse(response.body)
|
19
13
|
approved, message = parsed_json["approved"], parsed_json["message"]
|
20
|
-
approved
|
14
|
+
if approved
|
15
|
+
Success(
|
16
|
+
ApiResult.new(message, original_request: request, original_response: response)
|
17
|
+
)
|
18
|
+
else
|
19
|
+
Failure(
|
20
|
+
ApiFailure.new(message, original_request: request, original_response: response)
|
21
|
+
)
|
22
|
+
end
|
21
23
|
end
|
22
24
|
end
|
23
25
|
end
|
@@ -3,7 +3,11 @@
|
|
3
3
|
require 'dry/monads/result'
|
4
4
|
require 'friendly_shipping/services/ups/client'
|
5
5
|
require 'friendly_shipping/services/ups/serialize_access_request'
|
6
|
+
require 'friendly_shipping/services/ups/serialize_city_state_lookup_request'
|
7
|
+
require 'friendly_shipping/services/ups/serialize_address_validation_request'
|
6
8
|
require 'friendly_shipping/services/ups/serialize_rating_service_selection_request'
|
9
|
+
require 'friendly_shipping/services/ups/parse_address_validation_response'
|
10
|
+
require 'friendly_shipping/services/ups/parse_city_state_lookup_response'
|
7
11
|
require 'friendly_shipping/services/ups/parse_rate_response'
|
8
12
|
require 'friendly_shipping/services/ups/shipping_methods'
|
9
13
|
|
@@ -25,6 +29,8 @@ module FriendlyShipping
|
|
25
29
|
LIVE_URL = 'https://onlinetools.ups.com'
|
26
30
|
|
27
31
|
RESOURCES = {
|
32
|
+
address_validation: '/ups.app/xml/XAV',
|
33
|
+
city_state_lookup: '/ups.app/xml/AV',
|
28
34
|
rates: '/ups.app/xml/Rate'
|
29
35
|
}.freeze
|
30
36
|
|
@@ -40,7 +46,11 @@ module FriendlyShipping
|
|
40
46
|
Success([CARRIER])
|
41
47
|
end
|
42
48
|
|
43
|
-
|
49
|
+
# Get rates for a shipment
|
50
|
+
# @param [Physical::Shipment] location The shipment we want to get rates for
|
51
|
+
# @return [Result<ApiResult<Array<Rate>>>] The rates returned from UPS encoded in a
|
52
|
+
# `FriendlyShipping::ApiResult` object.
|
53
|
+
def rate_estimates(shipment, _options = {})
|
44
54
|
rate_request_xml = SerializeRatingServiceSelectionRequest.call(shipment: shipment)
|
45
55
|
url = base_url + RESOURCES[:rates]
|
46
56
|
request = FriendlyShipping::Request.new(
|
@@ -53,6 +63,42 @@ module FriendlyShipping
|
|
53
63
|
end
|
54
64
|
end
|
55
65
|
|
66
|
+
# Validate an address.
|
67
|
+
# @param [Physical::Location] location The address we want to verify
|
68
|
+
# @return [Result<ApiResult<Array<Physical::Location>>>] The response data from UPS encoded in a
|
69
|
+
# `Physical::Location` object. Name and Company name are always nil, the
|
70
|
+
# address lines will be made conformant to what UPS considers right. The returned location will
|
71
|
+
# have the address_type set if possible.
|
72
|
+
def address_validation(location)
|
73
|
+
address_validation_request_xml = SerializeAddressValidationRequest.call(location: location)
|
74
|
+
url = base_url + RESOURCES[:address_validation]
|
75
|
+
request = FriendlyShipping::Request.new(
|
76
|
+
url: url,
|
77
|
+
body: access_request_xml + address_validation_request_xml
|
78
|
+
)
|
79
|
+
|
80
|
+
client.post(request).bind do |response|
|
81
|
+
ParseAddressValidationResponse.call(response: response, request: request)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Find city and state for a given ZIP code
|
86
|
+
# @param [Physical::Location] location A location object with country and ZIP code set
|
87
|
+
# @return [Result<ApiResult<Array<Physical::Location>>>] The response data from UPS encoded in a
|
88
|
+
# `Physical::Location` object. Country, City and ZIP code will be set, everything else nil.
|
89
|
+
def city_state_lookup(location)
|
90
|
+
city_state_lookup_request_xml = SerializeCityStateLookupRequest.call(location: location)
|
91
|
+
url = base_url + RESOURCES[:city_state_lookup]
|
92
|
+
request = FriendlyShipping::Request.new(
|
93
|
+
url: url,
|
94
|
+
body: access_request_xml + city_state_lookup_request_xml
|
95
|
+
)
|
96
|
+
|
97
|
+
client.post(request).bind do |response|
|
98
|
+
ParseCityStateLookupResponse.call(response: response, request: request, location: location)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
56
102
|
private
|
57
103
|
|
58
104
|
def access_request_xml
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FriendlyShipping
|
4
|
+
module Services
|
5
|
+
class Ups
|
6
|
+
class ParseAddressValidationResponse
|
7
|
+
extend Dry::Monads::Result::Mixin
|
8
|
+
|
9
|
+
def self.call(request:, response:)
|
10
|
+
parsing_result = ParseXMLResponse.call(response.body, 'AddressValidationResponse')
|
11
|
+
|
12
|
+
parsing_result.bind do |xml|
|
13
|
+
if xml.at('NoCandidatesIndicator')
|
14
|
+
Failure(
|
15
|
+
FriendlyShipping::ApiFailure.new(
|
16
|
+
'Address is probably invalid. No similar valid addresses found.',
|
17
|
+
original_request: request,
|
18
|
+
original_response: response
|
19
|
+
)
|
20
|
+
)
|
21
|
+
else
|
22
|
+
Success(
|
23
|
+
FriendlyShipping::ApiResult.new(
|
24
|
+
build_suggestions(xml),
|
25
|
+
original_request: request,
|
26
|
+
original_response: response
|
27
|
+
)
|
28
|
+
)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.build_suggestions(xml)
|
34
|
+
xml.xpath('//AddressKeyFormat').map do |address_fragment|
|
35
|
+
Physical::Location.new(
|
36
|
+
address1: address_fragment.xpath('AddressLine[1]')[0]&.text,
|
37
|
+
address2: address_fragment.xpath('AddressLine[2]')[0]&.text,
|
38
|
+
company_name: address_fragment.at('ConsigneeName')&.text,
|
39
|
+
city: address_fragment.at('PoliticalDivision2')&.text,
|
40
|
+
region: address_fragment.at('PoliticalDivision1')&.text,
|
41
|
+
country: address_fragment.at('CountryCode')&.text,
|
42
|
+
zip: "#{address_fragment.at('PostcodePrimaryLow')&.text}-#{address_fragment.at('PostcodeExtendedLow')&.text}",
|
43
|
+
address_type: address_fragment.at('AddressClassification/Description')&.text&.downcase
|
44
|
+
)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FriendlyShipping
|
4
|
+
module Services
|
5
|
+
class Ups
|
6
|
+
class ParseCityStateLookupResponse
|
7
|
+
extend Dry::Monads::Result::Mixin
|
8
|
+
|
9
|
+
def self.call(request:, response:, location:)
|
10
|
+
parsing_result = ParseXMLResponse.call(response.body, 'AddressValidationResponse')
|
11
|
+
|
12
|
+
parsing_result.fmap do |xml|
|
13
|
+
FriendlyShipping::ApiResult.new(
|
14
|
+
[
|
15
|
+
Physical::Location.new(
|
16
|
+
city: xml.at('AddressValidationResult/Address/City')&.text,
|
17
|
+
region: xml.at('AddressValidationResult/Address/StateProvinceCode')&.text,
|
18
|
+
country: location.country,
|
19
|
+
zip: xml.at('AddressValidationResult/Address/PostcodePrimaryLow')&.text,
|
20
|
+
)
|
21
|
+
],
|
22
|
+
original_request: request,
|
23
|
+
original_response: response
|
24
|
+
)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -9,33 +9,39 @@ module FriendlyShipping
|
|
9
9
|
def self.call(request:, response:, shipment:)
|
10
10
|
parsing_result = ParseXMLResponse.call(response.body, 'RatingServiceSelectionResponse')
|
11
11
|
parsing_result.fmap do |xml|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
total_cents = rated_shipment.at('TotalCharges/MonetaryValue').text.to_d * currency.subunit_to_unit
|
20
|
-
insurance_price = rated_shipment.at('ServiceOptionsCharges/MonetaryValue').text.to_f
|
21
|
-
negotiated_rate = rated_shipment.at(
|
22
|
-
'NegotiatedRates/NetSummaryCharges/GrandTotal/MonetaryValue'
|
23
|
-
)&.text.to_f
|
12
|
+
FriendlyShipping::ApiResult.new(
|
13
|
+
build_rates(xml, shipment),
|
14
|
+
original_request: request,
|
15
|
+
original_response: response
|
16
|
+
)
|
17
|
+
end
|
18
|
+
end
|
24
19
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
data: {
|
31
|
-
insurance_price: insurance_price,
|
32
|
-
negotiated_rate: negotiated_rate,
|
33
|
-
days_to_delivery: days_to_delivery
|
34
|
-
},
|
35
|
-
original_request: request,
|
36
|
-
original_response: response
|
37
|
-
)
|
20
|
+
def self.build_rates(xml, shipment)
|
21
|
+
xml.root.css('> RatedShipment').map do |rated_shipment|
|
22
|
+
service_code = rated_shipment.at('Service/Code').text
|
23
|
+
shipping_method = CARRIER.shipping_methods.detect do |sm|
|
24
|
+
sm.service_code == service_code && shipment.origin.country.in?(sm.origin_countries)
|
38
25
|
end
|
26
|
+
days_to_delivery = rated_shipment.at('GuaranteedDaysToDelivery').text.to_i
|
27
|
+
currency = Money::Currency.new(rated_shipment.at('TotalCharges/CurrencyCode').text)
|
28
|
+
total_cents = rated_shipment.at('TotalCharges/MonetaryValue').text.to_d * currency.subunit_to_unit
|
29
|
+
insurance_price = rated_shipment.at('ServiceOptionsCharges/MonetaryValue').text.to_f
|
30
|
+
negotiated_rate = rated_shipment.at(
|
31
|
+
'NegotiatedRates/NetSummaryCharges/GrandTotal/MonetaryValue'
|
32
|
+
)&.text.to_f
|
33
|
+
|
34
|
+
FriendlyShipping::Rate.new(
|
35
|
+
shipping_method: shipping_method,
|
36
|
+
amounts: { total: Money.new(total_cents, currency) },
|
37
|
+
warnings: [rated_shipment.at("RatedShipmentWarning")&.text].compact,
|
38
|
+
errors: [],
|
39
|
+
data: {
|
40
|
+
insurance_price: insurance_price,
|
41
|
+
negotiated_rate: negotiated_rate,
|
42
|
+
days_to_delivery: days_to_delivery
|
43
|
+
}
|
44
|
+
)
|
39
45
|
end
|
40
46
|
end
|
41
47
|
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FriendlyShipping
|
4
|
+
module Services
|
5
|
+
class Ups
|
6
|
+
class SerializeAddressValidationRequest
|
7
|
+
attr_reader :location
|
8
|
+
|
9
|
+
REQUEST_ACTION = 'XAV'
|
10
|
+
REQUEST_OPTIONS = {
|
11
|
+
validation: 1,
|
12
|
+
classification: 2,
|
13
|
+
both: 3
|
14
|
+
}.freeze
|
15
|
+
|
16
|
+
def self.call(location:)
|
17
|
+
xml_builder = Nokogiri::XML::Builder.new do |xml|
|
18
|
+
xml.AddressValidationRequest do
|
19
|
+
xml.Request do
|
20
|
+
xml.RequestAction REQUEST_ACTION
|
21
|
+
xml.RequestOption REQUEST_OPTIONS[:both]
|
22
|
+
end
|
23
|
+
|
24
|
+
xml.AddressKeyFormat do
|
25
|
+
xml.AddressLine location.address1
|
26
|
+
xml.AddressLine location.address2
|
27
|
+
xml.PoliticalDivision2 location.city
|
28
|
+
xml.PoliticalDivision1 location.region.code
|
29
|
+
xml.PostcodePrimaryLow location.zip
|
30
|
+
xml.CountryCode location.country.code
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
xml_builder.to_xml
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FriendlyShipping
|
4
|
+
module Services
|
5
|
+
class Ups
|
6
|
+
class SerializeCityStateLookupRequest
|
7
|
+
REQUEST_ACTION = 'AV'
|
8
|
+
|
9
|
+
def self.call(location:)
|
10
|
+
xml_builder = Nokogiri::XML::Builder.new do |xml|
|
11
|
+
xml.AddressValidationRequest do
|
12
|
+
xml.Request do
|
13
|
+
xml.RequestAction REQUEST_ACTION
|
14
|
+
end
|
15
|
+
xml.Address do
|
16
|
+
xml.PostalCode location.zip
|
17
|
+
xml.CountryCode location.country.code
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
xml_builder.to_xml
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -2,7 +2,11 @@
|
|
2
2
|
|
3
3
|
require 'friendly_shipping/services/usps/client'
|
4
4
|
require 'friendly_shipping/services/usps/shipping_methods'
|
5
|
+
require 'friendly_shipping/services/usps/serialize_address_validation_request'
|
6
|
+
require 'friendly_shipping/services/usps/serialize_city_state_lookup_request'
|
5
7
|
require 'friendly_shipping/services/usps/serialize_rate_request'
|
8
|
+
require 'friendly_shipping/services/usps/parse_address_validation_response'
|
9
|
+
require 'friendly_shipping/services/usps/parse_city_state_lookup_response'
|
6
10
|
require 'friendly_shipping/services/usps/parse_rate_response'
|
7
11
|
|
8
12
|
module FriendlyShipping
|
@@ -23,7 +27,9 @@ module FriendlyShipping
|
|
23
27
|
LIVE_URL = 'https://secure.shippingapis.com/ShippingAPI.dll'
|
24
28
|
|
25
29
|
RESOURCES = {
|
26
|
-
|
30
|
+
address_validation: 'Verify',
|
31
|
+
city_state_lookup: 'CityStateLookup',
|
32
|
+
rates: 'RateV4'
|
27
33
|
}.freeze
|
28
34
|
|
29
35
|
def initialize(login:, test: true, client: Client)
|
@@ -51,8 +57,8 @@ module FriendlyShipping
|
|
51
57
|
# @return [Result<Array<FriendlyShipping::Rate>>] When successfully parsing, an array of rates in a Success Monad.
|
52
58
|
# When the parsing is not successful or USPS can't give us rates, a Failure monad containing something that
|
53
59
|
# can be serialized into an error message using `to_s`.
|
54
|
-
def rate_estimates(shipment,
|
55
|
-
rate_request_xml = SerializeRateRequest.call(shipment: shipment, login: login, shipping_method: shipping_method)
|
60
|
+
def rate_estimates(shipment, options = {})
|
61
|
+
rate_request_xml = SerializeRateRequest.call(shipment: shipment, login: login, shipping_method: options[:shipping_method])
|
56
62
|
request = FriendlyShipping::Request.new(url: base_url, body: "API=#{RESOURCES[:rates]}&XML=#{CGI.escape rate_request_xml}")
|
57
63
|
|
58
64
|
client.post(request).bind do |response|
|
@@ -60,6 +66,40 @@ module FriendlyShipping
|
|
60
66
|
end
|
61
67
|
end
|
62
68
|
|
69
|
+
# Validate an address.
|
70
|
+
# @param [Physical::Location] location The address we want to verify
|
71
|
+
# @return [Result<ApiResult<Array<Physical::Location>>>] The response data from UPS encoded in a
|
72
|
+
# `Physical::Location` object. Name and Company name are always nil, the
|
73
|
+
# address lines will be made conformant to what USPS considers right. The returned location will
|
74
|
+
# have the address_type set if possible.
|
75
|
+
def address_validation(location)
|
76
|
+
address_validation_request_xml = SerializeAddressValidationRequest.call(location: location, login: login)
|
77
|
+
request = FriendlyShipping::Request.new(
|
78
|
+
url: base_url,
|
79
|
+
body: "API=#{RESOURCES[:address_validation]}&XML=#{CGI.escape address_validation_request_xml}"
|
80
|
+
)
|
81
|
+
|
82
|
+
client.post(request).bind do |response|
|
83
|
+
ParseAddressValidationResponse.call(response: response, request: request)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Find city and state for a given ZIP code
|
88
|
+
# @param [Physical::Location] location A location object with country and ZIP code set
|
89
|
+
# @return [Result<ApiResult<Array<Physical::Location>>>] The response data from USPS encoded in a
|
90
|
+
# `Physical::Location` object. Country, City and ZIP code will be set, everything else nil.
|
91
|
+
def city_state_lookup(location)
|
92
|
+
city_state_lookup_request_xml = SerializeCityStateLookupRequest.call(location: location, login: login)
|
93
|
+
request = FriendlyShipping::Request.new(
|
94
|
+
url: base_url,
|
95
|
+
body: "API=#{RESOURCES[:city_state_lookup]}&XML=#{CGI.escape city_state_lookup_request_xml}"
|
96
|
+
)
|
97
|
+
|
98
|
+
client.post(request).bind do |response|
|
99
|
+
ParseCityStateLookupResponse.call(response: response, request: request)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
63
103
|
private
|
64
104
|
|
65
105
|
def base_url
|
@@ -0,0 +1,39 @@
|
|
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(response.body, 'AddressValidateResponse')
|
16
|
+
parsing_result.fmap do |xml|
|
17
|
+
address = xml.root.at('Address')
|
18
|
+
suggestions = [
|
19
|
+
Physical::Location.new(
|
20
|
+
address1: address&.at('Address2')&.text, # USPS swaps Address1 and Address2 in the response
|
21
|
+
address2: address&.at('Address1')&.text,
|
22
|
+
city: address&.at('City')&.text,
|
23
|
+
region: address&.at('State')&.text,
|
24
|
+
zip: address&.at('Zip5')&.text,
|
25
|
+
country: 'US'
|
26
|
+
)
|
27
|
+
]
|
28
|
+
FriendlyShipping::ApiResult.new(
|
29
|
+
suggestions,
|
30
|
+
original_request: request,
|
31
|
+
original_response: response
|
32
|
+
)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,37 @@
|
|
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(response.body, 'CityStateLookupResponse')
|
16
|
+
parsing_result.fmap do |xml|
|
17
|
+
address = xml.root.at('ZipCode')
|
18
|
+
suggestions = [
|
19
|
+
Physical::Location.new(
|
20
|
+
city: address&.at('City')&.text,
|
21
|
+
region: address&.at('State')&.text,
|
22
|
+
zip: address&.at('Zip5')&.text,
|
23
|
+
country: 'US'
|
24
|
+
)
|
25
|
+
]
|
26
|
+
FriendlyShipping::ApiResult.new(
|
27
|
+
suggestions,
|
28
|
+
original_request: request,
|
29
|
+
original_response: response,
|
30
|
+
)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -16,19 +16,20 @@ 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
|
-
# @return [Result<Array<FriendlyShipping::Rate
|
19
|
+
# @return [Result<ApiResult<Array<FriendlyShipping::Rate>>>] When successfully parsing, an array of rates in a Success Monad.
|
20
20
|
def call(request:, response:, shipment:)
|
21
21
|
# Filter out error responses and directly return a failure
|
22
22
|
parsing_result = ParseXMLResponse.call(response.body, 'RateV4Response')
|
23
|
+
rates = []
|
23
24
|
parsing_result.fmap do |xml|
|
24
25
|
# Get all the possible rates for each package
|
25
26
|
rates_by_package = rates_from_response_node(xml, shipment)
|
26
27
|
|
27
|
-
SHIPPING_METHODS.map do |shipping_method|
|
28
|
+
rates = SHIPPING_METHODS.map do |shipping_method|
|
28
29
|
# For every package ...
|
29
|
-
matching_rates = rates_by_package.map do |package,
|
30
|
+
matching_rates = rates_by_package.map do |package, package_rates|
|
30
31
|
# ... choose the rate that fits this package best.
|
31
|
-
ChoosePackageRate.call(shipping_method, package,
|
32
|
+
ChoosePackageRate.call(shipping_method, package, package_rates)
|
32
33
|
end.compact # Some shipping rates are not available for every shipping method.
|
33
34
|
|
34
35
|
# in this case, go to the next shipping method.
|
@@ -38,11 +39,15 @@ module FriendlyShipping
|
|
38
39
|
FriendlyShipping::Rate.new(
|
39
40
|
amounts: matching_rates.map(&:amounts).reduce({}, :merge),
|
40
41
|
shipping_method: shipping_method,
|
41
|
-
data: matching_rates.first.data
|
42
|
-
original_request: request,
|
43
|
-
original_response: response
|
42
|
+
data: matching_rates.first.data
|
44
43
|
)
|
45
44
|
end.compact
|
45
|
+
|
46
|
+
ApiResult.new(
|
47
|
+
rates,
|
48
|
+
original_request: request,
|
49
|
+
original_response: response
|
50
|
+
)
|
46
51
|
end
|
47
52
|
end
|
48
53
|
|
@@ -5,13 +5,13 @@ module FriendlyShipping
|
|
5
5
|
class Usps
|
6
6
|
class ParseXMLResponse
|
7
7
|
extend Dry::Monads::Result::Mixin
|
8
|
-
|
8
|
+
ERROR_TAG = 'Error'
|
9
9
|
|
10
10
|
class << self
|
11
11
|
def call(response_body, expected_root_tag)
|
12
12
|
xml = Nokogiri.XML(response_body)
|
13
13
|
|
14
|
-
if xml.root.nil? || ![expected_root_tag,
|
14
|
+
if xml.root.nil? || ![expected_root_tag, 'Error'].include?(xml.root.name)
|
15
15
|
Failure('Invalid document')
|
16
16
|
elsif request_successful?(xml)
|
17
17
|
Success(xml)
|
@@ -25,13 +25,13 @@ module FriendlyShipping
|
|
25
25
|
private
|
26
26
|
|
27
27
|
def request_successful?(xml)
|
28
|
-
xml.xpath('Error/Number')&.text.blank?
|
28
|
+
xml.xpath('//Error/Number')&.text.blank?
|
29
29
|
end
|
30
30
|
|
31
31
|
def error_message(xml)
|
32
|
-
number = xml.xpath('Error/Number')&.text
|
33
|
-
desc = xml.xpath('Error/Description')&.text
|
34
|
-
[number, desc].select(&:present?).join(': ').presence || 'USPS could not process the request.'
|
32
|
+
number = xml.xpath('//Error/Number')&.text
|
33
|
+
desc = xml.xpath('//Error/Description')&.text
|
34
|
+
[number, desc].select(&:present?).join(': ').presence&.strip || 'USPS could not process the request.'
|
35
35
|
end
|
36
36
|
end
|
37
37
|
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FriendlyShipping
|
4
|
+
module Services
|
5
|
+
class Usps
|
6
|
+
class SerializeAddressValidationRequest
|
7
|
+
def self.call(location:, login:)
|
8
|
+
xml_builder = Nokogiri::XML::Builder.new do |xml|
|
9
|
+
xml.AddressValidateRequest USERID: login do
|
10
|
+
xml.Address do
|
11
|
+
xml.Address1 location.address2 # USPS swaps Address1 and Address2 in the request
|
12
|
+
xml.Address2 location.address1
|
13
|
+
xml.City location.city
|
14
|
+
xml.State location.region.code
|
15
|
+
xml.Zip5 location.zip
|
16
|
+
xml.Zip4
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
xml_builder.to_xml
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FriendlyShipping
|
4
|
+
module Services
|
5
|
+
class Usps
|
6
|
+
class SerializeCityStateLookupRequest
|
7
|
+
def self.call(location:, login:)
|
8
|
+
xml_builder = Nokogiri::XML::Builder.new do |xml|
|
9
|
+
xml.CityStateLookupRequest(USERID: login) do
|
10
|
+
xml.ZipCode do
|
11
|
+
xml.Zip5 location.zip
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
xml_builder.to_xml
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -33,10 +33,10 @@ module FriendlyShipping
|
|
33
33
|
container = CONTAINERS[package.properties[:box_name] || :rectangular]
|
34
34
|
xml.Container(container)
|
35
35
|
xml.Size(size_code)
|
36
|
-
xml.Width("
|
37
|
-
xml.Length("
|
38
|
-
xml.Height("
|
39
|
-
xml.Girth("
|
36
|
+
xml.Width("%<width>0.2f" % { width: package.width.convert_to(:inches).value.to_f })
|
37
|
+
xml.Length("%<length>0.2f" % { length: package.length.convert_to(:inches).value.to_f })
|
38
|
+
xml.Height("%<height>0.2f" % { height: package.height.convert_to(:inches).value.to_f })
|
39
|
+
xml.Girth("%<girth>0.2f" % { girth: girth(package) })
|
40
40
|
xml.Machinable(machinable(package))
|
41
41
|
end
|
42
42
|
end
|
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.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Martin Meyerhoff
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-10-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: data_uri
|
@@ -158,6 +158,8 @@ files:
|
|
158
158
|
- bin/setup
|
159
159
|
- friendly_shipping.gemspec
|
160
160
|
- lib/friendly_shipping.rb
|
161
|
+
- lib/friendly_shipping/api_failure.rb
|
162
|
+
- lib/friendly_shipping/api_result.rb
|
161
163
|
- lib/friendly_shipping/bad_request.rb
|
162
164
|
- lib/friendly_shipping/carrier.rb
|
163
165
|
- lib/friendly_shipping/label.rb
|
@@ -174,10 +176,14 @@ files:
|
|
174
176
|
- lib/friendly_shipping/services/ship_engine/serialize_rate_estimate_request.rb
|
175
177
|
- lib/friendly_shipping/services/ups.rb
|
176
178
|
- lib/friendly_shipping/services/ups/client.rb
|
179
|
+
- lib/friendly_shipping/services/ups/parse_address_validation_response.rb
|
180
|
+
- lib/friendly_shipping/services/ups/parse_city_state_lookup_response.rb
|
177
181
|
- lib/friendly_shipping/services/ups/parse_rate_response.rb
|
178
182
|
- lib/friendly_shipping/services/ups/parse_xml_response.rb
|
179
183
|
- lib/friendly_shipping/services/ups/serialize_access_request.rb
|
180
184
|
- lib/friendly_shipping/services/ups/serialize_address_snippet.rb
|
185
|
+
- lib/friendly_shipping/services/ups/serialize_address_validation_request.rb
|
186
|
+
- lib/friendly_shipping/services/ups/serialize_city_state_lookup_request.rb
|
181
187
|
- lib/friendly_shipping/services/ups/serialize_package_node.rb
|
182
188
|
- lib/friendly_shipping/services/ups/serialize_rating_service_selection_request.rb
|
183
189
|
- lib/friendly_shipping/services/ups/shipping_methods.rb
|
@@ -185,9 +191,13 @@ files:
|
|
185
191
|
- lib/friendly_shipping/services/usps/choose_package_rate.rb
|
186
192
|
- lib/friendly_shipping/services/usps/client.rb
|
187
193
|
- lib/friendly_shipping/services/usps/machinable_package.rb
|
194
|
+
- lib/friendly_shipping/services/usps/parse_address_validation_response.rb
|
195
|
+
- lib/friendly_shipping/services/usps/parse_city_state_lookup_response.rb
|
188
196
|
- lib/friendly_shipping/services/usps/parse_package_rate.rb
|
189
197
|
- lib/friendly_shipping/services/usps/parse_rate_response.rb
|
190
198
|
- lib/friendly_shipping/services/usps/parse_xml_response.rb
|
199
|
+
- lib/friendly_shipping/services/usps/serialize_address_validation_request.rb
|
200
|
+
- lib/friendly_shipping/services/usps/serialize_city_state_lookup_request.rb
|
191
201
|
- lib/friendly_shipping/services/usps/serialize_rate_request.rb
|
192
202
|
- lib/friendly_shipping/services/usps/shipping_methods.rb
|
193
203
|
- lib/friendly_shipping/shipping_method.rb
|