friendly_shipping 0.2.6 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|