friendly_shipping 0.3.0 → 0.3.3
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/CHANGELOG.md +24 -0
- data/README.md +2 -1
- data/lib/friendly_shipping/http_client.rb +64 -0
- data/lib/friendly_shipping/services/ship_engine.rb +18 -11
- data/lib/friendly_shipping/services/ship_engine/bad_request.rb +29 -0
- data/lib/friendly_shipping/services/ship_engine/bad_request_handler.rb +21 -0
- data/lib/friendly_shipping/services/ship_engine/parse_void_response.rb +0 -2
- data/lib/friendly_shipping/services/ship_engine/serialize_label_shipment.rb +62 -60
- data/lib/friendly_shipping/services/ups.rb +29 -8
- data/lib/friendly_shipping/services/ups/parse_address_classification_response.rb +26 -0
- data/lib/friendly_shipping/services/ups/serialize_address_snippet.rb +2 -2
- data/lib/friendly_shipping/services/usps.rb +17 -15
- data/lib/friendly_shipping/version.rb +1 -1
- metadata +7 -6
- data/lib/friendly_shipping/bad_request.rb +0 -25
- data/lib/friendly_shipping/services/ship_engine/client.rb +0 -68
- data/lib/friendly_shipping/services/ups/client.rb +0 -38
- data/lib/friendly_shipping/services/usps/client.rb +0 -36
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2eff4626b4d3f7d779b8837725bd6c585fc3bd47
|
4
|
+
data.tar.gz: 3818a4de3a68cb8f9a7cf5d62650e7da66604250
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d8a23d14f73876ca9f47b6e5f6272df7d958c0c9f7a754438c9208d8c654e7ba5ca79ee01bdc9d06ed105045335f6a75016007dbdf9166a27fd55b6096c63d8f
|
7
|
+
data.tar.gz: 54458eb117ed1d541af4208979e73f2d1f5ce7e7b8afcbaea702e024165da7c6320e67ffc4d8f82913a8e1e6efb08d29ef1ca44114bef3b09c3f2d1d7d734893
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# Changelog
|
2
|
+
All notable changes to this project will be documented in this file.
|
3
|
+
|
4
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
5
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
6
|
+
|
7
|
+
## [Unreleased]
|
8
|
+
|
9
|
+
## [0.3.3] - 2017-10-25
|
10
|
+
|
11
|
+
### Changed
|
12
|
+
- Fix: ShipEngine#labels test mode works again.
|
13
|
+
|
14
|
+
## [0.3.2] - 2017-10-25
|
15
|
+
|
16
|
+
### Changed
|
17
|
+
- Fix: ShipEngine#labels now works as expected.
|
18
|
+
|
19
|
+
## [0.3.1] - 2017-06-20
|
20
|
+
### Added
|
21
|
+
- Endpoint for UPS address classification
|
22
|
+
|
23
|
+
### Changed
|
24
|
+
- `ShipEngine#labels` now needs a second argument, the shipping method.
|
data/README.md
CHANGED
@@ -48,7 +48,7 @@ The following methods are supported:
|
|
48
48
|
|
49
49
|
- `#carriers` - List all configured carriers
|
50
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.
|
51
|
+
- `#labels(physical_shipment, shipping_method:)` - Get labels for a shipments. Currently only supports USPS labels, other services are untested.
|
52
52
|
- `#void(physical_label)` - Void a label and get the cost refunded
|
53
53
|
|
54
54
|
#### UPS (United Parcel Service)
|
@@ -68,6 +68,7 @@ The following methods are supported:
|
|
68
68
|
|
69
69
|
- `#carriers` - List all configured carriers (always returns UPS)
|
70
70
|
- `#rate_estimates(physical_shipment)` - Get rate estimates for a shipment
|
71
|
+
- `#address_classification(physical_location)` - Determine whether an address is commercial or residential.
|
71
72
|
- `#address_validation(physical_location)` - Perform a detailed address validation and determine whether an address is commercial or residential.
|
72
73
|
- `#city_state_lookup(physical_location)` - Lookup City and State for a given ZIP code.
|
73
74
|
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/monads/result'
|
4
|
+
require 'rest-client'
|
5
|
+
|
6
|
+
module FriendlyShipping
|
7
|
+
class HttpClient
|
8
|
+
include Dry::Monads::Result::Mixin
|
9
|
+
|
10
|
+
attr_reader :error_handler
|
11
|
+
|
12
|
+
def initialize(error_handler: method(:wrap_in_failure))
|
13
|
+
@error_handler = error_handler
|
14
|
+
end
|
15
|
+
|
16
|
+
def get(request)
|
17
|
+
http_response = ::RestClient.get(
|
18
|
+
request.url, request.headers
|
19
|
+
)
|
20
|
+
|
21
|
+
Success(convert_to_friendly_response(http_response))
|
22
|
+
rescue ::RestClient::Exception => e
|
23
|
+
error_handler.call(e)
|
24
|
+
end
|
25
|
+
|
26
|
+
def post(friendly_shipping_request)
|
27
|
+
http_response = ::RestClient.post(
|
28
|
+
friendly_shipping_request.url,
|
29
|
+
friendly_shipping_request.body,
|
30
|
+
friendly_shipping_request.headers
|
31
|
+
)
|
32
|
+
|
33
|
+
Success(convert_to_friendly_response(http_response))
|
34
|
+
rescue ::RestClient::Exception => e
|
35
|
+
error_handler.call(e)
|
36
|
+
end
|
37
|
+
|
38
|
+
def put(request)
|
39
|
+
http_response = ::RestClient.put(
|
40
|
+
request.url,
|
41
|
+
request.body,
|
42
|
+
request.headers
|
43
|
+
)
|
44
|
+
|
45
|
+
Success(convert_to_friendly_response(http_response))
|
46
|
+
rescue ::RestClient::Exception => e
|
47
|
+
error_handler.call(e)
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def wrap_in_failure(error)
|
53
|
+
Failure(error)
|
54
|
+
end
|
55
|
+
|
56
|
+
def convert_to_friendly_response(http_response)
|
57
|
+
FriendlyShipping::Response.new(
|
58
|
+
status: http_response.code,
|
59
|
+
body: http_response.body,
|
60
|
+
headers: http_response.headers
|
61
|
+
)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -1,7 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'dry/monads/result'
|
4
|
-
require 'friendly_shipping/
|
4
|
+
require 'friendly_shipping/http_client'
|
5
|
+
require 'friendly_shipping/services/ship_engine/bad_request_handler'
|
5
6
|
require 'friendly_shipping/services/ship_engine/parse_carrier_response'
|
6
7
|
require 'friendly_shipping/services/ship_engine/serialize_label_shipment'
|
7
8
|
require 'friendly_shipping/services/ship_engine/serialize_rate_estimate_request'
|
@@ -18,7 +19,7 @@ module FriendlyShipping
|
|
18
19
|
labels: "labels"
|
19
20
|
}.freeze
|
20
21
|
|
21
|
-
def initialize(token:, test: true, client:
|
22
|
+
def initialize(token:, test: true, client: FriendlyShipping::HttpClient.new(error_handler: BadRequestHandler))
|
22
23
|
@token = token
|
23
24
|
@test = test
|
24
25
|
@client = client
|
@@ -27,10 +28,11 @@ module FriendlyShipping
|
|
27
28
|
# Get configured carriers from USPS
|
28
29
|
#
|
29
30
|
# @return [Result<ApiResult<Array<Carrier>>>] Carriers configured in your shipstation account
|
30
|
-
def carriers
|
31
|
+
def carriers(debug: false)
|
31
32
|
request = FriendlyShipping::Request.new(
|
32
33
|
url: API_BASE + API_PATHS[:carriers],
|
33
|
-
headers: request_headers
|
34
|
+
headers: request_headers,
|
35
|
+
debug: debug
|
34
36
|
)
|
35
37
|
client.get(request).fmap do |response|
|
36
38
|
ParseCarrierResponse.call(request: request, response: response)
|
@@ -47,12 +49,13 @@ module FriendlyShipping
|
|
47
49
|
# @return [Result<ApiResult<Array<FriendlyShipping::Rate>>>] When successfully parsing, an array of rates in a Success Monad.
|
48
50
|
# When the parsing is not successful or ShipEngine can't give us rates, a Failure monad containing something that
|
49
51
|
# can be serialized into an error message using `to_s`.
|
50
|
-
def rate_estimates(shipment,
|
51
|
-
selected_carriers
|
52
|
+
def rate_estimates(shipment, selected_carriers: nil, debug: false)
|
53
|
+
selected_carriers ||= carriers.value!.data
|
52
54
|
request = FriendlyShipping::Request.new(
|
53
55
|
url: API_BASE + 'rates/estimate',
|
54
56
|
body: SerializeRateEstimateRequest.call(shipment: shipment, carriers: selected_carriers).to_json,
|
55
|
-
headers: request_headers
|
57
|
+
headers: request_headers,
|
58
|
+
debug: debug
|
56
59
|
)
|
57
60
|
client.post(request).bind do |response|
|
58
61
|
ParseRateEstimateResponse.call(response: response, request: request, carriers: selected_carriers)
|
@@ -64,13 +67,16 @@ module FriendlyShipping
|
|
64
67
|
# @param [Physical::Shipment] shipment The shipment object we're trying to get labels for
|
65
68
|
# Note: Some ShipEngine carriers, notably USPS, only support one package per shipment, and that's
|
66
69
|
# all that the integration supports at this point.
|
70
|
+
# @param [FriendlyShipping::ShippingMethod] shipping_method The shipping method we want to use.
|
71
|
+
# Specifically, the "#service_code" will be serialized. If a carrier is set, it's `#id` will
|
72
|
+
# also be sent to ShipEngine.
|
67
73
|
#
|
68
74
|
# @return [Result<ApiResult<Array<FriendlyShipping::Label>>>] The label returned.
|
69
75
|
#
|
70
|
-
def labels(shipment)
|
76
|
+
def labels(shipment, shipping_method:)
|
71
77
|
request = FriendlyShipping::Request.new(
|
72
78
|
url: API_BASE + API_PATHS[:labels],
|
73
|
-
body: SerializeLabelShipment.
|
79
|
+
body: SerializeLabelShipment.call(shipment: shipment, shipping_method: shipping_method, test: test).to_json,
|
74
80
|
headers: request_headers
|
75
81
|
)
|
76
82
|
client.post(request).fmap do |response|
|
@@ -78,11 +84,12 @@ module FriendlyShipping
|
|
78
84
|
end
|
79
85
|
end
|
80
86
|
|
81
|
-
def void(label)
|
87
|
+
def void(label, debug: false)
|
82
88
|
request = FriendlyShipping::Request.new(
|
83
89
|
url: "#{API_BASE}labels/#{label.id}/void",
|
84
90
|
body: '',
|
85
|
-
headers: request_headers
|
91
|
+
headers: request_headers,
|
92
|
+
debug: debug
|
86
93
|
)
|
87
94
|
client.put(request).bind do |response|
|
88
95
|
ParseVoidResponse.call(request: request, response: response)
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module FriendlyShipping
|
6
|
+
module Services
|
7
|
+
class ShipEngine
|
8
|
+
class BadRequest < StandardError
|
9
|
+
attr_reader :rest_error, :response
|
10
|
+
|
11
|
+
def initialize(rest_error)
|
12
|
+
@rest_error = rest_error
|
13
|
+
@response = rest_error.response
|
14
|
+
super parse_json_errors || rest_error
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def parse_json_errors
|
20
|
+
parsed_body = JSON.parse(response.body)
|
21
|
+
messages = parsed_body.fetch('errors')&.map { |e| e.fetch('message') }
|
22
|
+
messages&.join(', ')
|
23
|
+
rescue JSON::ParserError, KeyError => _e
|
24
|
+
nil
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'friendly_shipping/services/ship_engine/bad_request'
|
4
|
+
|
5
|
+
module FriendlyShipping
|
6
|
+
module Services
|
7
|
+
class ShipEngine
|
8
|
+
class BadRequestHandler
|
9
|
+
extend Dry::Monads::Result::Mixin
|
10
|
+
|
11
|
+
def self.call(error)
|
12
|
+
if error.http_code == 400
|
13
|
+
Failure(BadRequest.new(error))
|
14
|
+
else
|
15
|
+
Failure(error)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -4,76 +4,78 @@ module FriendlyShipping
|
|
4
4
|
module Services
|
5
5
|
class ShipEngine
|
6
6
|
class SerializeLabelShipment
|
7
|
-
|
7
|
+
class << self
|
8
|
+
def call(shipment:, shipping_method:, test:)
|
9
|
+
shipment_hash = {
|
10
|
+
label_format: shipment.options[:label_format].presence || "pdf",
|
11
|
+
label_download_type: shipment.options[:label_download_type].presence || "url",
|
12
|
+
shipment: {
|
13
|
+
service_code: shipping_method.service_code,
|
14
|
+
ship_to: serialize_address(shipment.destination),
|
15
|
+
ship_from: serialize_address(shipment.origin),
|
16
|
+
packages: serialize_packages(shipment.packages)
|
17
|
+
}
|
18
|
+
}
|
19
|
+
# A carrier might not be necessary if the service code is unique within ShipEngine.
|
20
|
+
if shipping_method.carrier
|
21
|
+
shipment_hash[:shipment][:carrier_id] = shipping_method.carrier.id
|
22
|
+
end
|
8
23
|
|
9
|
-
|
10
|
-
|
11
|
-
|
24
|
+
if test
|
25
|
+
shipment_hash[:test_label] = true
|
26
|
+
end
|
12
27
|
|
13
|
-
|
14
|
-
shipment_hash = {
|
15
|
-
label_format: shipment.options[:label_format].presence || "pdf",
|
16
|
-
label_download_type: shipment.options[:label_download_type].presence || "url",
|
17
|
-
shipment: {
|
18
|
-
service_code: shipment.service_code,
|
19
|
-
ship_to: serialize_address(shipment.destination),
|
20
|
-
ship_from: serialize_address(shipment.origin),
|
21
|
-
packages: serialize_packages(shipment.packages)
|
22
|
-
}
|
23
|
-
}
|
24
|
-
if shipment.options[:carrier_id]
|
25
|
-
shipment_hash[:shipment][:carrier_id] = shipment.options[:carrier_id]
|
28
|
+
shipment_hash
|
26
29
|
end
|
27
|
-
shipment_hash
|
28
|
-
end
|
29
30
|
|
30
|
-
|
31
|
+
private
|
31
32
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
33
|
+
def serialize_address(address)
|
34
|
+
{
|
35
|
+
name: address.name,
|
36
|
+
phone: address.phone,
|
37
|
+
company_name: address.company_name,
|
38
|
+
address_line1: address.address1,
|
39
|
+
address_line2: address.address2,
|
40
|
+
city_locality: address.city,
|
41
|
+
state_province: address.region.code,
|
42
|
+
postal_code: address.zip,
|
43
|
+
country_code: address.country.code,
|
44
|
+
address_residential_indicator: "No"
|
45
|
+
}
|
46
|
+
end
|
46
47
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
48
|
+
def serialize_packages(packages)
|
49
|
+
packages.map do |package|
|
50
|
+
package_hash = serialize_weight(package.weight)
|
51
|
+
if package.container.properties[:usps_label_messages]
|
52
|
+
package_hash[:label_messages] = package.container.properties[:usps_label_messages]
|
53
|
+
end
|
54
|
+
package_code = package.container.properties[:usps_package_code]
|
55
|
+
if package_code
|
56
|
+
package_hash[:package_code] = package_code
|
57
|
+
else
|
58
|
+
package_hash[:dimensions] = {
|
59
|
+
unit: 'inch',
|
60
|
+
width: package.container.width.convert_to(:inches).value.to_f.round(2),
|
61
|
+
length: package.container.length.convert_to(:inches).value.to_f.round(2),
|
62
|
+
height: package.container.height.convert_to(:inches).value.to_f.round(2)
|
63
|
+
}
|
64
|
+
end
|
65
|
+
package_hash
|
63
66
|
end
|
64
|
-
package_hash
|
65
67
|
end
|
66
|
-
end
|
67
68
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
69
|
+
def serialize_weight(weight)
|
70
|
+
ounces = weight.convert_to(:ounce).value.to_f
|
71
|
+
{
|
72
|
+
weight: {
|
73
|
+
# Max weight for USPS First Class is 15.9 oz, not 16 oz
|
74
|
+
value: ounces.between?(15.9, 16) ? 15.9 : ounces,
|
75
|
+
unit: "ounce"
|
76
|
+
}
|
75
77
|
}
|
76
|
-
|
78
|
+
end
|
77
79
|
end
|
78
80
|
end
|
79
81
|
end
|
@@ -1,11 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'dry/monads/result'
|
4
|
-
require 'friendly_shipping/
|
4
|
+
require 'friendly_shipping/http_client'
|
5
5
|
require 'friendly_shipping/services/ups/serialize_access_request'
|
6
6
|
require 'friendly_shipping/services/ups/serialize_city_state_lookup_request'
|
7
7
|
require 'friendly_shipping/services/ups/serialize_address_validation_request'
|
8
8
|
require 'friendly_shipping/services/ups/serialize_rating_service_selection_request'
|
9
|
+
require 'friendly_shipping/services/ups/parse_address_classification_response'
|
9
10
|
require 'friendly_shipping/services/ups/parse_address_validation_response'
|
10
11
|
require 'friendly_shipping/services/ups/parse_city_state_lookup_response'
|
11
12
|
require 'friendly_shipping/services/ups/parse_rate_response'
|
@@ -34,7 +35,7 @@ module FriendlyShipping
|
|
34
35
|
rates: '/ups.app/xml/Rate'
|
35
36
|
}.freeze
|
36
37
|
|
37
|
-
def initialize(key:, login:, password:, test: true, client:
|
38
|
+
def initialize(key:, login:, password:, test: true, client: HttpClient.new)
|
38
39
|
@key = key
|
39
40
|
@login = login
|
40
41
|
@password = password
|
@@ -50,12 +51,13 @@ module FriendlyShipping
|
|
50
51
|
# @param [Physical::Shipment] location The shipment we want to get rates for
|
51
52
|
# @return [Result<ApiResult<Array<Rate>>>] The rates returned from UPS encoded in a
|
52
53
|
# `FriendlyShipping::ApiResult` object.
|
53
|
-
def rate_estimates(shipment,
|
54
|
+
def rate_estimates(shipment, debug: false)
|
54
55
|
rate_request_xml = SerializeRatingServiceSelectionRequest.call(shipment: shipment)
|
55
56
|
url = base_url + RESOURCES[:rates]
|
56
57
|
request = FriendlyShipping::Request.new(
|
57
58
|
url: url,
|
58
|
-
body: access_request_xml + rate_request_xml
|
59
|
+
body: access_request_xml + rate_request_xml,
|
60
|
+
debug: debug
|
59
61
|
)
|
60
62
|
|
61
63
|
client.post(request).bind do |response|
|
@@ -69,12 +71,13 @@ module FriendlyShipping
|
|
69
71
|
# `Physical::Location` object. Name and Company name are always nil, the
|
70
72
|
# address lines will be made conformant to what UPS considers right. The returned location will
|
71
73
|
# have the address_type set if possible.
|
72
|
-
def address_validation(location)
|
74
|
+
def address_validation(location, debug: false)
|
73
75
|
address_validation_request_xml = SerializeAddressValidationRequest.call(location: location)
|
74
76
|
url = base_url + RESOURCES[:address_validation]
|
75
77
|
request = FriendlyShipping::Request.new(
|
76
78
|
url: url,
|
77
|
-
body: access_request_xml + address_validation_request_xml
|
79
|
+
body: access_request_xml + address_validation_request_xml,
|
80
|
+
debug: debug
|
78
81
|
)
|
79
82
|
|
80
83
|
client.post(request).bind do |response|
|
@@ -82,16 +85,34 @@ module FriendlyShipping
|
|
82
85
|
end
|
83
86
|
end
|
84
87
|
|
88
|
+
# Classify an address.
|
89
|
+
# @param [Physical::Location] location The address we want to classify
|
90
|
+
# @return [Result<ApiResult<String>>] Either `"commercial"`, `"residential"`, or `"unknown"`
|
91
|
+
def address_classification(location, debug: false)
|
92
|
+
address_validation_request_xml = SerializeAddressValidationRequest.call(location: location)
|
93
|
+
url = base_url + RESOURCES[:address_validation]
|
94
|
+
request = FriendlyShipping::Request.new(
|
95
|
+
url: url,
|
96
|
+
body: access_request_xml + address_validation_request_xml,
|
97
|
+
debug: debug
|
98
|
+
)
|
99
|
+
|
100
|
+
client.post(request).bind do |response|
|
101
|
+
ParseAddressClassificationResponse.call(response: response, request: request)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
85
105
|
# Find city and state for a given ZIP code
|
86
106
|
# @param [Physical::Location] location A location object with country and ZIP code set
|
87
107
|
# @return [Result<ApiResult<Array<Physical::Location>>>] The response data from UPS encoded in a
|
88
108
|
# `Physical::Location` object. Country, City and ZIP code will be set, everything else nil.
|
89
|
-
def city_state_lookup(location)
|
109
|
+
def city_state_lookup(location, debug: false)
|
90
110
|
city_state_lookup_request_xml = SerializeCityStateLookupRequest.call(location: location)
|
91
111
|
url = base_url + RESOURCES[:city_state_lookup]
|
92
112
|
request = FriendlyShipping::Request.new(
|
93
113
|
url: url,
|
94
|
-
body: access_request_xml + city_state_lookup_request_xml
|
114
|
+
body: access_request_xml + city_state_lookup_request_xml,
|
115
|
+
debug: debug
|
95
116
|
)
|
96
117
|
|
97
118
|
client.post(request).bind do |response|
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FriendlyShipping
|
4
|
+
module Services
|
5
|
+
class Ups
|
6
|
+
class ParseAddressClassificationResponse
|
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
|
+
address_type = xml.at('AddressClassification/Description')&.text&.downcase
|
14
|
+
Success(
|
15
|
+
FriendlyShipping::ApiResult.new(
|
16
|
+
address_type,
|
17
|
+
original_request: request,
|
18
|
+
original_response: response
|
19
|
+
)
|
20
|
+
)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -40,9 +40,9 @@ module FriendlyShipping
|
|
40
40
|
|
41
41
|
# Quote residential rates by default. If UPS doesn't know if the address is residential or
|
42
42
|
# commercial, it will quote a residential rate by default. Even with this flag being set,
|
43
|
-
# if UPS knows the address is commercial it will quote a commercial rate.
|
43
|
+
# if UPS knows the address is commercial it will often quote a commercial rate.
|
44
44
|
#
|
45
|
-
xml.ResidentialAddressIndicator
|
45
|
+
xml.ResidentialAddressIndicator unless location.commercial?
|
46
46
|
end
|
47
47
|
end
|
48
48
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'friendly_shipping/
|
3
|
+
require 'friendly_shipping/http_client'
|
4
4
|
require 'friendly_shipping/services/usps/shipping_methods'
|
5
5
|
require 'friendly_shipping/services/usps/serialize_address_validation_request'
|
6
6
|
require 'friendly_shipping/services/usps/serialize_city_state_lookup_request'
|
@@ -32,7 +32,7 @@ module FriendlyShipping
|
|
32
32
|
rates: 'RateV4'
|
33
33
|
}.freeze
|
34
34
|
|
35
|
-
def initialize(login:, test: true, client:
|
35
|
+
def initialize(login:, test: true, client: HttpClient.new)
|
36
36
|
@login = login
|
37
37
|
@test = test
|
38
38
|
@client = client
|
@@ -57,9 +57,9 @@ module FriendlyShipping
|
|
57
57
|
# @return [Result<Array<FriendlyShipping::Rate>>] When successfully parsing, an array of rates in a Success Monad.
|
58
58
|
# When the parsing is not successful or USPS can't give us rates, a Failure monad containing something that
|
59
59
|
# can be serialized into an error message using `to_s`.
|
60
|
-
def rate_estimates(shipment,
|
61
|
-
rate_request_xml = SerializeRateRequest.call(shipment: shipment, login: login, shipping_method:
|
62
|
-
request =
|
60
|
+
def rate_estimates(shipment, shipping_method: nil, debug: false)
|
61
|
+
rate_request_xml = SerializeRateRequest.call(shipment: shipment, login: login, shipping_method: shipping_method)
|
62
|
+
request = build_request(api: :rates_request, xml: rate_request_xml, debug: debug)
|
63
63
|
|
64
64
|
client.post(request).bind do |response|
|
65
65
|
ParseRateResponse.call(response: response, request: request, shipment: shipment)
|
@@ -72,12 +72,9 @@ module FriendlyShipping
|
|
72
72
|
# `Physical::Location` object. Name and Company name are always nil, the
|
73
73
|
# address lines will be made conformant to what USPS considers right. The returned location will
|
74
74
|
# have the address_type set if possible.
|
75
|
-
def address_validation(location)
|
75
|
+
def address_validation(location, debug: false)
|
76
76
|
address_validation_request_xml = SerializeAddressValidationRequest.call(location: location, login: login)
|
77
|
-
request =
|
78
|
-
url: base_url,
|
79
|
-
body: "API=#{RESOURCES[:address_validation]}&XML=#{CGI.escape address_validation_request_xml}"
|
80
|
-
)
|
77
|
+
request = build_request(api: :address_validation, xml: address_validation_request_xml, debug: debug)
|
81
78
|
|
82
79
|
client.post(request).bind do |response|
|
83
80
|
ParseAddressValidationResponse.call(response: response, request: request)
|
@@ -88,12 +85,9 @@ module FriendlyShipping
|
|
88
85
|
# @param [Physical::Location] location A location object with country and ZIP code set
|
89
86
|
# @return [Result<ApiResult<Array<Physical::Location>>>] The response data from USPS encoded in a
|
90
87
|
# `Physical::Location` object. Country, City and ZIP code will be set, everything else nil.
|
91
|
-
def city_state_lookup(location)
|
88
|
+
def city_state_lookup(location, debug: false)
|
92
89
|
city_state_lookup_request_xml = SerializeCityStateLookupRequest.call(location: location, login: login)
|
93
|
-
request =
|
94
|
-
url: base_url,
|
95
|
-
body: "API=#{RESOURCES[:city_state_lookup]}&XML=#{CGI.escape city_state_lookup_request_xml}"
|
96
|
-
)
|
90
|
+
request = build_request(api: :city_state_lookup, xml: city_state_lookup_request_xml, debug: debug)
|
97
91
|
|
98
92
|
client.post(request).bind do |response|
|
99
93
|
ParseCityStateLookupResponse.call(response: response, request: request)
|
@@ -102,6 +96,14 @@ module FriendlyShipping
|
|
102
96
|
|
103
97
|
private
|
104
98
|
|
99
|
+
def build_request(api:, xml:, debug:)
|
100
|
+
FriendlyShipping::Request.new(
|
101
|
+
url: base_url,
|
102
|
+
body: "API=#{RESOURCES[api]}&XML=#{CGI.escape xml}",
|
103
|
+
debug: debug
|
104
|
+
)
|
105
|
+
end
|
106
|
+
|
105
107
|
def base_url
|
106
108
|
test ? TEST_URL : LIVE_URL
|
107
109
|
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.3.
|
4
|
+
version: 0.3.3
|
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-10-
|
11
|
+
date: 2019-10-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: data_uri
|
@@ -149,6 +149,7 @@ files:
|
|
149
149
|
- ".rubocop-relaxed.yml"
|
150
150
|
- ".rubocop.yml"
|
151
151
|
- ".travis.yml"
|
152
|
+
- CHANGELOG.md
|
152
153
|
- CODE_OF_CONDUCT.md
|
153
154
|
- Gemfile
|
154
155
|
- LICENSE.txt
|
@@ -160,14 +161,15 @@ files:
|
|
160
161
|
- lib/friendly_shipping.rb
|
161
162
|
- lib/friendly_shipping/api_failure.rb
|
162
163
|
- lib/friendly_shipping/api_result.rb
|
163
|
-
- lib/friendly_shipping/bad_request.rb
|
164
164
|
- lib/friendly_shipping/carrier.rb
|
165
|
+
- lib/friendly_shipping/http_client.rb
|
165
166
|
- lib/friendly_shipping/label.rb
|
166
167
|
- lib/friendly_shipping/rate.rb
|
167
168
|
- lib/friendly_shipping/request.rb
|
168
169
|
- lib/friendly_shipping/response.rb
|
169
170
|
- lib/friendly_shipping/services/ship_engine.rb
|
170
|
-
- lib/friendly_shipping/services/ship_engine/
|
171
|
+
- lib/friendly_shipping/services/ship_engine/bad_request.rb
|
172
|
+
- lib/friendly_shipping/services/ship_engine/bad_request_handler.rb
|
171
173
|
- lib/friendly_shipping/services/ship_engine/parse_carrier_response.rb
|
172
174
|
- lib/friendly_shipping/services/ship_engine/parse_label_response.rb
|
173
175
|
- lib/friendly_shipping/services/ship_engine/parse_rate_estimate_response.rb
|
@@ -175,7 +177,7 @@ files:
|
|
175
177
|
- lib/friendly_shipping/services/ship_engine/serialize_label_shipment.rb
|
176
178
|
- lib/friendly_shipping/services/ship_engine/serialize_rate_estimate_request.rb
|
177
179
|
- lib/friendly_shipping/services/ups.rb
|
178
|
-
- lib/friendly_shipping/services/ups/
|
180
|
+
- lib/friendly_shipping/services/ups/parse_address_classification_response.rb
|
179
181
|
- lib/friendly_shipping/services/ups/parse_address_validation_response.rb
|
180
182
|
- lib/friendly_shipping/services/ups/parse_city_state_lookup_response.rb
|
181
183
|
- lib/friendly_shipping/services/ups/parse_rate_response.rb
|
@@ -189,7 +191,6 @@ files:
|
|
189
191
|
- lib/friendly_shipping/services/ups/shipping_methods.rb
|
190
192
|
- lib/friendly_shipping/services/usps.rb
|
191
193
|
- lib/friendly_shipping/services/usps/choose_package_rate.rb
|
192
|
-
- lib/friendly_shipping/services/usps/client.rb
|
193
194
|
- lib/friendly_shipping/services/usps/machinable_package.rb
|
194
195
|
- lib/friendly_shipping/services/usps/parse_address_validation_response.rb
|
195
196
|
- lib/friendly_shipping/services/usps/parse_city_state_lookup_response.rb
|
@@ -1,25 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'json'
|
4
|
-
|
5
|
-
module FriendlyShipping
|
6
|
-
class BadRequest < StandardError
|
7
|
-
attr_reader :rest_error, :response
|
8
|
-
|
9
|
-
def initialize(rest_error)
|
10
|
-
@rest_error = rest_error
|
11
|
-
@response = rest_error.response
|
12
|
-
super parse_json_errors || rest_error
|
13
|
-
end
|
14
|
-
|
15
|
-
private
|
16
|
-
|
17
|
-
def parse_json_errors
|
18
|
-
parsed_body = JSON.parse(response.body)
|
19
|
-
messages = parsed_body.fetch('errors')&.map { |e| e.fetch('message') }
|
20
|
-
messages&.join(', ')
|
21
|
-
rescue JSON::ParserError, KeyError => _e
|
22
|
-
nil
|
23
|
-
end
|
24
|
-
end
|
25
|
-
end
|
@@ -1,68 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'dry/monads/result'
|
4
|
-
require 'friendly_shipping/bad_request'
|
5
|
-
require 'rest-client'
|
6
|
-
|
7
|
-
module FriendlyShipping
|
8
|
-
module Services
|
9
|
-
class ShipEngine
|
10
|
-
class Client
|
11
|
-
extend Dry::Monads::Result::Mixin
|
12
|
-
class <<self
|
13
|
-
def get(request)
|
14
|
-
http_response = ::RestClient.get(
|
15
|
-
request.url, request.headers
|
16
|
-
)
|
17
|
-
|
18
|
-
Success(convert_to_friendly_response(http_response))
|
19
|
-
rescue ::RestClient::Exception => e
|
20
|
-
Failure(e)
|
21
|
-
end
|
22
|
-
|
23
|
-
def post(friendly_shipping_request)
|
24
|
-
http_response = ::RestClient.post(
|
25
|
-
friendly_shipping_request.url,
|
26
|
-
friendly_shipping_request.body,
|
27
|
-
friendly_shipping_request.headers
|
28
|
-
)
|
29
|
-
|
30
|
-
Success(convert_to_friendly_response(http_response))
|
31
|
-
rescue ::RestClient::Exception => e
|
32
|
-
if e.http_code == 400
|
33
|
-
Failure(BadRequest.new(e))
|
34
|
-
else
|
35
|
-
Failure(e)
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
def put(request)
|
40
|
-
http_response = ::RestClient.put(
|
41
|
-
request.url,
|
42
|
-
request.body,
|
43
|
-
request.headers
|
44
|
-
)
|
45
|
-
|
46
|
-
Success(convert_to_friendly_response(http_response))
|
47
|
-
rescue ::RestClient::Exception => e
|
48
|
-
if e.http_code == 400
|
49
|
-
Failure(BadRequest.new(e))
|
50
|
-
else
|
51
|
-
Failure(e)
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
|
-
private
|
56
|
-
|
57
|
-
def convert_to_friendly_response(http_response)
|
58
|
-
FriendlyShipping::Response.new(
|
59
|
-
status: http_response.code,
|
60
|
-
body: http_response.body,
|
61
|
-
headers: http_response.headers
|
62
|
-
)
|
63
|
-
end
|
64
|
-
end
|
65
|
-
end
|
66
|
-
end
|
67
|
-
end
|
68
|
-
end
|
@@ -1,38 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'dry/monads/result'
|
4
|
-
require 'friendly_shipping/bad_request'
|
5
|
-
require 'rest-client'
|
6
|
-
|
7
|
-
module FriendlyShipping
|
8
|
-
module Services
|
9
|
-
class Ups
|
10
|
-
class Client
|
11
|
-
extend Dry::Monads::Result::Mixin
|
12
|
-
class << self
|
13
|
-
def post(friendly_shipping_request)
|
14
|
-
http_response = ::RestClient.post(
|
15
|
-
friendly_shipping_request.url,
|
16
|
-
friendly_shipping_request.body,
|
17
|
-
friendly_shipping_request.headers
|
18
|
-
)
|
19
|
-
|
20
|
-
Success(convert_to_friendly_response(http_response))
|
21
|
-
rescue ::RestClient::Exception => e
|
22
|
-
Failure(e)
|
23
|
-
end
|
24
|
-
|
25
|
-
private
|
26
|
-
|
27
|
-
def convert_to_friendly_response(http_response)
|
28
|
-
FriendlyShipping::Response.new(
|
29
|
-
status: http_response.code,
|
30
|
-
body: http_response.body,
|
31
|
-
headers: http_response.headers
|
32
|
-
)
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|
@@ -1,36 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'dry/monads/result'
|
4
|
-
require 'friendly_shipping/bad_request'
|
5
|
-
require 'rest-client'
|
6
|
-
|
7
|
-
module FriendlyShipping
|
8
|
-
module Services
|
9
|
-
class Usps
|
10
|
-
class Client
|
11
|
-
extend Dry::Monads::Result::Mixin
|
12
|
-
class <<self
|
13
|
-
# USPS allows both GET and POST request. We're using POST here as those request
|
14
|
-
# are less limited in size.
|
15
|
-
def post(request)
|
16
|
-
http_response = ::RestClient.post(request.url, request.body)
|
17
|
-
|
18
|
-
Success(convert_to_friendly_response(http_response))
|
19
|
-
rescue ::RestClient::Exception => e
|
20
|
-
Failure(e)
|
21
|
-
end
|
22
|
-
|
23
|
-
private
|
24
|
-
|
25
|
-
def convert_to_friendly_response(http_response)
|
26
|
-
FriendlyShipping::Response.new(
|
27
|
-
status: http_response.code,
|
28
|
-
body: http_response.body,
|
29
|
-
headers: http_response.headers
|
30
|
-
)
|
31
|
-
end
|
32
|
-
end
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
36
|
-
end
|