friendly_shipping 0.8.0 → 0.8.1
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/.env.template +4 -0
- data/.env.test +4 -0
- data/CHANGELOG.md +11 -0
- data/lib/friendly_shipping/services/ship_engine_ltl/bad_request.rb +9 -0
- data/lib/friendly_shipping/services/ship_engine_ltl/bad_request_handler.rb +33 -0
- data/lib/friendly_shipping/services/ship_engine_ltl/item_options.rb +31 -0
- data/lib/friendly_shipping/services/ship_engine_ltl/package_options.rb +15 -0
- data/lib/friendly_shipping/services/ship_engine_ltl/parse_carrier_response.rb +49 -0
- data/lib/friendly_shipping/services/ship_engine_ltl/parse_quote_response.rb +72 -0
- data/lib/friendly_shipping/services/ship_engine_ltl/quote_options.rb +34 -0
- data/lib/friendly_shipping/services/ship_engine_ltl/serialize_packages.rb +41 -0
- data/lib/friendly_shipping/services/ship_engine_ltl/serialize_quote_request.rb +109 -0
- data/lib/friendly_shipping/services/ship_engine_ltl.rb +133 -0
- data/lib/friendly_shipping/services/ups/label_package_options.rb +7 -3
- data/lib/friendly_shipping/services/ups/serialize_package_node.rb +11 -1
- data/lib/friendly_shipping/services/ups/serialize_shipment_confirm_request.rb +6 -3
- data/lib/friendly_shipping/services/ups_freight/generate_location_hash.rb +10 -6
- data/lib/friendly_shipping/services/usps/parse_package_rate.rb +2 -1
- data/lib/friendly_shipping/services/usps/parse_time_in_transit_response.rb +6 -2
- data/lib/friendly_shipping/services/usps/shipping_methods.rb +4 -2
- data/lib/friendly_shipping/services/usps_international/parse_package_rate.rb +2 -1
- data/lib/friendly_shipping/services/usps_international/serialize_rate_request.rb +2 -2
- data/lib/friendly_shipping/version.rb +1 -1
- data/lib/friendly_shipping.rb +1 -0
- metadata +14 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7bc054d44e12f0d44f1240a8c831918fa7eec158f8cc66ffb104586a2a2f4180
|
4
|
+
data.tar.gz: 900e8bb08f94858c0b147d886f9eaeae72c3db74efe04b7cbe8a3d9e7184f4fe
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: db80927fd83629f29a503512672dd39566e758b20e16d29d92332427e9eabd22aef9f9f03d286468fd36c78ea74dd06906a63b9e631bd7304f3f4cfa372e8718
|
7
|
+
data.tar.gz: 99930e58a990bc2e4a50027cbb5a4a379062f86ca21a897edb3761d594843444f54de3b1ac09b65b4b454c46f52d75941cee688f09f86e8230cb97ae73d0d76a
|
data/.env.template
CHANGED
@@ -5,8 +5,12 @@
|
|
5
5
|
SHIPENGINE_API_KEY=ShipEngine API key
|
6
6
|
SHIPENGINE_CARRIER_ID=Carrier ID from your ShipEngine account to run test labels with
|
7
7
|
|
8
|
+
SHIPENGINE_LTL_CARRIER_ID=LTL carrier ID from your ShipEngine account to run tests with
|
9
|
+
SHIPENGINE_LTL_CARRIER_SCAC=Standard Carrier Alpha Code from your ShipEngine account to run tests with
|
10
|
+
|
8
11
|
UPS_KEY=UPS API access key
|
9
12
|
UPS_LOGIN=UPS login name
|
10
13
|
UPS_PASSWORD=UPS login password
|
14
|
+
UPS_SHIPPER_NUMBER=UPS shipper number
|
11
15
|
|
12
16
|
USPS_LOGIN=USPS login name
|
data/.env.test
ADDED
data/CHANGELOG.md
CHANGED
@@ -4,6 +4,17 @@ All notable changes to this project will be documented in this file.
|
|
4
4
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
5
5
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
6
6
|
|
7
|
+
## [0.8.1] - 2023-08-03
|
8
|
+
- USPS Service: Fix international ounces remainder (#166)
|
9
|
+
- UPS Service: Fix bug causing inflated international product costs (#167)
|
10
|
+
- UPS Service: Add declared value to UPS package serializer (#168)
|
11
|
+
- UPS Service: Add declared value to UPS label package options (#169)
|
12
|
+
- TForce Service: Truncate long values in UPS Freight label request (#170)
|
13
|
+
- USPS Service: Add new USPS Ground Advantage shipping method (#171)
|
14
|
+
- ShipEngine Service: Basic ShipEngine LTL service class (#172)
|
15
|
+
- UPS Service: Add new billing options for Non-Resident Importer (#174)
|
16
|
+
- ShipEngine Service: Request quotes from ShipEngine LTL API (#175)
|
17
|
+
|
7
18
|
## [0.8.0] - 2023-04-18
|
8
19
|
- Rails 7 support: Fix deprecation warning about ActiveSupport#sum (#164)
|
9
20
|
- UPS Service: Truncate product descriptions (#163)
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'friendly_shipping/services/ship_engine_ltl/bad_request'
|
4
|
+
|
5
|
+
module FriendlyShipping
|
6
|
+
module Services
|
7
|
+
class ShipEngineLTL
|
8
|
+
class BadRequestHandler
|
9
|
+
extend Dry::Monads::Result::Mixin
|
10
|
+
|
11
|
+
def self.call(error, original_request: nil, original_response: nil)
|
12
|
+
if error.http_code == 400
|
13
|
+
Failure(
|
14
|
+
ApiFailure.new(
|
15
|
+
BadRequest.new(error),
|
16
|
+
original_request: original_request,
|
17
|
+
original_response: original_response
|
18
|
+
)
|
19
|
+
)
|
20
|
+
else
|
21
|
+
Failure(
|
22
|
+
ApiFailure.new(
|
23
|
+
error,
|
24
|
+
original_request: original_request,
|
25
|
+
original_response: original_response
|
26
|
+
)
|
27
|
+
)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FriendlyShipping
|
4
|
+
module Services
|
5
|
+
class ShipEngineLTL
|
6
|
+
class ItemOptions < FriendlyShipping::ItemOptions
|
7
|
+
attr_reader :packaging_code,
|
8
|
+
:freight_class,
|
9
|
+
:nmfc_code,
|
10
|
+
:stackable,
|
11
|
+
:hazardous_materials
|
12
|
+
|
13
|
+
def initialize(
|
14
|
+
packaging_code: nil,
|
15
|
+
freight_class: nil,
|
16
|
+
nmfc_code: nil,
|
17
|
+
stackable: true,
|
18
|
+
hazardous_materials: false,
|
19
|
+
**kwargs
|
20
|
+
)
|
21
|
+
@packaging_code = packaging_code
|
22
|
+
@freight_class = freight_class
|
23
|
+
@nmfc_code = nmfc_code
|
24
|
+
@stackable = stackable
|
25
|
+
@hazardous_materials = hazardous_materials
|
26
|
+
super(**kwargs)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'friendly_shipping/services/ups/label_item_options'
|
4
|
+
|
5
|
+
module FriendlyShipping
|
6
|
+
module Services
|
7
|
+
class ShipEngineLTL
|
8
|
+
class PackageOptions < FriendlyShipping::PackageOptions
|
9
|
+
def initialize(**kwargs)
|
10
|
+
super(**kwargs.merge(item_options_class: ItemOptions))
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module FriendlyShipping
|
6
|
+
module Services
|
7
|
+
class ShipEngineLTL
|
8
|
+
class ParseCarrierResponse
|
9
|
+
extend Dry::Monads::Result::Mixin
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def call(request:, response:)
|
13
|
+
parsed_json = JSON.parse(response.body)
|
14
|
+
carriers = parsed_json.fetch('carriers', []).map do |carrier_data|
|
15
|
+
FriendlyShipping::Carrier.new(
|
16
|
+
id: carrier_data['carrier_id'],
|
17
|
+
name: carrier_data['name'],
|
18
|
+
data: {
|
19
|
+
countries: carrier_data['countries'],
|
20
|
+
features: carrier_data['features'],
|
21
|
+
scac: carrier_data['scac']
|
22
|
+
}
|
23
|
+
)
|
24
|
+
end
|
25
|
+
|
26
|
+
if carriers.any?
|
27
|
+
Success(
|
28
|
+
ApiResult.new(
|
29
|
+
carriers,
|
30
|
+
original_request: request,
|
31
|
+
original_response: response
|
32
|
+
)
|
33
|
+
)
|
34
|
+
else
|
35
|
+
errors = parsed_json.fetch('errors', [{ 'message' => 'Unknown error' }])
|
36
|
+
Failure(
|
37
|
+
ApiResult.new(
|
38
|
+
errors.map { |e| e['message'] },
|
39
|
+
original_request: request,
|
40
|
+
original_response: response
|
41
|
+
)
|
42
|
+
)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module FriendlyShipping
|
6
|
+
module Services
|
7
|
+
class ShipEngineLTL
|
8
|
+
class ParseQuoteResponse
|
9
|
+
extend Dry::Monads::Result::Mixin
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def call(request:, response:)
|
13
|
+
parsed_json = JSON.parse(response.body)
|
14
|
+
rates = build_rates(parsed_json)
|
15
|
+
if rates.any?
|
16
|
+
Success(
|
17
|
+
ApiResult.new(
|
18
|
+
rates,
|
19
|
+
original_request: request,
|
20
|
+
original_response: response
|
21
|
+
)
|
22
|
+
)
|
23
|
+
else
|
24
|
+
errors = parsed_json.fetch('errors', [{ 'message' => 'Unknown error' }])
|
25
|
+
Failure(
|
26
|
+
ApiResult.new(
|
27
|
+
errors.map { |e| e['message'] },
|
28
|
+
original_request: request,
|
29
|
+
original_response: response
|
30
|
+
)
|
31
|
+
)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def build_rates(parsed_json)
|
38
|
+
total = build_total(parsed_json)
|
39
|
+
return [] unless total.positive?
|
40
|
+
|
41
|
+
[
|
42
|
+
FriendlyShipping::Rate.new(
|
43
|
+
shipping_method: build_shipping_method(parsed_json),
|
44
|
+
amounts: { total: total }
|
45
|
+
)
|
46
|
+
]
|
47
|
+
end
|
48
|
+
|
49
|
+
def build_shipping_method(parsed_json)
|
50
|
+
description = parsed_json.dig("service", "carrier_description")
|
51
|
+
code = parsed_json.dig("service", "code")
|
52
|
+
|
53
|
+
FriendlyShipping::ShippingMethod.new(
|
54
|
+
name: description,
|
55
|
+
service_code: code,
|
56
|
+
multi_package: true
|
57
|
+
)
|
58
|
+
end
|
59
|
+
|
60
|
+
def build_total(parsed_json)
|
61
|
+
total_charges = parsed_json.fetch("charges", []).detect { |e| e['type'] == "total" }
|
62
|
+
return 0 unless total_charges
|
63
|
+
|
64
|
+
currency = Money::Currency.new(total_charges.dig("amount", "currency"))
|
65
|
+
value = total_charges.dig("amount", "value")
|
66
|
+
Money.new(value * currency.subunit_to_unit, currency)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'friendly_shipping/shipment_options'
|
4
|
+
|
5
|
+
module FriendlyShipping
|
6
|
+
module Services
|
7
|
+
class ShipEngineLTL
|
8
|
+
class QuoteOptions < ShipmentOptions
|
9
|
+
attr_reader :service_code,
|
10
|
+
:pickup_date,
|
11
|
+
:accessorial_service_codes,
|
12
|
+
:packages_serializer_class
|
13
|
+
|
14
|
+
# @param [String] service_code
|
15
|
+
# @param [Time] pickup_date
|
16
|
+
# @param [Array<String>] accessorial_service_codes
|
17
|
+
# @param [Class] packages_serializer_class
|
18
|
+
def initialize(
|
19
|
+
service_code: nil,
|
20
|
+
pickup_date: nil,
|
21
|
+
accessorial_service_codes: [],
|
22
|
+
packages_serializer_class: SerializePackages,
|
23
|
+
**kwargs
|
24
|
+
)
|
25
|
+
@service_code = service_code
|
26
|
+
@pickup_date = pickup_date
|
27
|
+
@accessorial_service_codes = accessorial_service_codes
|
28
|
+
@packages_serializer_class = packages_serializer_class
|
29
|
+
super(**kwargs.merge(package_options_class: PackageOptions))
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FriendlyShipping
|
4
|
+
module Services
|
5
|
+
class ShipEngineLTL
|
6
|
+
class SerializePackages
|
7
|
+
class << self
|
8
|
+
# @param [Array<Physical::Package>] packages
|
9
|
+
# @param [FriendlyShipping::Services::ShipEngineLTL::QuoteOptions] options
|
10
|
+
def call(packages:, options:)
|
11
|
+
packages.flat_map do |package|
|
12
|
+
package_options = options.options_for_package(package)
|
13
|
+
package.items.map do |item|
|
14
|
+
item_options = package_options.options_for_item(item)
|
15
|
+
{
|
16
|
+
code: item_options.packaging_code,
|
17
|
+
freight_class: item_options.freight_class,
|
18
|
+
nmfc_code: item_options.nmfc_code,
|
19
|
+
description: item.description || "Commodities",
|
20
|
+
dimensions: {
|
21
|
+
width: item.width.convert_to(:inches).value.ceil,
|
22
|
+
height: item.height.convert_to(:inches).value.ceil,
|
23
|
+
length: item.length.convert_to(:inches).value.ceil,
|
24
|
+
unit: "inches"
|
25
|
+
},
|
26
|
+
weight: {
|
27
|
+
value: item.weight.convert_to(:pounds).value.ceil,
|
28
|
+
unit: "pounds"
|
29
|
+
},
|
30
|
+
quantity: 1, # we don't support this yet
|
31
|
+
stackable: item_options.stackable,
|
32
|
+
hazardous_materials: item_options.hazardous_materials
|
33
|
+
}
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FriendlyShipping
|
4
|
+
module Services
|
5
|
+
class ShipEngineLTL
|
6
|
+
class SerializeQuoteRequest
|
7
|
+
class << self
|
8
|
+
# @param [Physical::Shipment] shipment
|
9
|
+
# @param [FriendlyShipping::Services::ShipEngineLTL::QuoteOptions] options
|
10
|
+
def call(shipment:, options:)
|
11
|
+
{
|
12
|
+
shipment: {
|
13
|
+
service_code: options.service_code,
|
14
|
+
pickup_date: options.pickup_date.strftime('%Y-%m-%d'),
|
15
|
+
packages: options.packages_serializer_class.call(packages: shipment.packages, options: options),
|
16
|
+
options: serialize_options(options),
|
17
|
+
ship_from: serialize_ship_address(shipment.origin),
|
18
|
+
ship_to: serialize_ship_address(shipment.destination),
|
19
|
+
bill_to: serialize_bill_address(shipment.origin),
|
20
|
+
requested_by: serialize_requested_by(shipment.origin),
|
21
|
+
}.compact,
|
22
|
+
shipment_measurements: serialize_shipment_measurements(shipment.packages)
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
# @param [FriendlyShipping::Services::ShipEngineLTL::QuoteOptions] options
|
29
|
+
def serialize_options(options)
|
30
|
+
options.accessorial_service_codes.map do |code|
|
31
|
+
{ code: code }
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# @param [Physical::Location] location
|
36
|
+
def serialize_ship_address(location)
|
37
|
+
{
|
38
|
+
account: location.properties.with_indifferent_access['account_number'],
|
39
|
+
address: serialize_address(location),
|
40
|
+
contact: serialize_contact(location)
|
41
|
+
}.compact
|
42
|
+
end
|
43
|
+
|
44
|
+
# @param [Physical::Location] location
|
45
|
+
def serialize_bill_address(location)
|
46
|
+
{
|
47
|
+
type: "shipper",
|
48
|
+
payment_terms: "prepaid",
|
49
|
+
account: location.properties.with_indifferent_access['account_number'],
|
50
|
+
address: serialize_address(location),
|
51
|
+
contact: serialize_contact(location)
|
52
|
+
}.compact
|
53
|
+
end
|
54
|
+
|
55
|
+
# @param [Physical::Location] location
|
56
|
+
def serialize_address(location)
|
57
|
+
{
|
58
|
+
company_name: location.company_name,
|
59
|
+
address_line1: location.address1,
|
60
|
+
city_locality: location.city,
|
61
|
+
state_province: location.region.code,
|
62
|
+
postal_code: location.zip,
|
63
|
+
country_code: location.country.code
|
64
|
+
}.compact
|
65
|
+
end
|
66
|
+
|
67
|
+
# @param [Physical::Location] location
|
68
|
+
def serialize_contact(location)
|
69
|
+
{
|
70
|
+
name: location.name,
|
71
|
+
phone_number: location.phone,
|
72
|
+
email: location.email
|
73
|
+
}.compact
|
74
|
+
end
|
75
|
+
|
76
|
+
# @param [Physical::Location] location
|
77
|
+
def serialize_requested_by(location)
|
78
|
+
{
|
79
|
+
company_name: location.company_name,
|
80
|
+
contact: serialize_contact(location)
|
81
|
+
}.compact
|
82
|
+
end
|
83
|
+
|
84
|
+
# @param [Array<Physical::Package>] packages
|
85
|
+
def serialize_shipment_measurements(packages)
|
86
|
+
{
|
87
|
+
total_linear_length: {
|
88
|
+
value: packages.sum(&:length).convert_to(:inches).value.ceil,
|
89
|
+
unit: "inches"
|
90
|
+
},
|
91
|
+
total_width: {
|
92
|
+
value: packages.map(&:width).max.convert_to(:inches).value.ceil,
|
93
|
+
unit: "inches"
|
94
|
+
},
|
95
|
+
total_height: {
|
96
|
+
value: packages.map(&:height).max.convert_to(:inches).value.ceil,
|
97
|
+
unit: "inches"
|
98
|
+
},
|
99
|
+
total_weight: {
|
100
|
+
value: +packages.sum(&:weight).convert_to(:pounds).value.ceil,
|
101
|
+
unit: "pounds"
|
102
|
+
}
|
103
|
+
}
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/monads'
|
4
|
+
require 'friendly_shipping/http_client'
|
5
|
+
require 'friendly_shipping/services/ship_engine_ltl/bad_request_handler'
|
6
|
+
require 'friendly_shipping/services/ship_engine_ltl/parse_carrier_response'
|
7
|
+
require 'friendly_shipping/services/ship_engine_ltl/parse_quote_response'
|
8
|
+
require 'friendly_shipping/services/ship_engine_ltl/serialize_packages'
|
9
|
+
require 'friendly_shipping/services/ship_engine_ltl/serialize_quote_request'
|
10
|
+
require 'friendly_shipping/services/ship_engine_ltl/quote_options'
|
11
|
+
require 'friendly_shipping/services/ship_engine_ltl/package_options'
|
12
|
+
require 'friendly_shipping/services/ship_engine_ltl/item_options'
|
13
|
+
|
14
|
+
module FriendlyShipping
|
15
|
+
module Services
|
16
|
+
class ShipEngineLTL
|
17
|
+
include Dry::Monads::Result::Mixin
|
18
|
+
|
19
|
+
API_BASE = "https://api.shipengine.com/v-beta/ltl/"
|
20
|
+
API_PATHS = {
|
21
|
+
connections: "connections",
|
22
|
+
carriers: "carriers",
|
23
|
+
quotes: "quotes"
|
24
|
+
}.freeze
|
25
|
+
|
26
|
+
def initialize(
|
27
|
+
token:,
|
28
|
+
test: true,
|
29
|
+
client: FriendlyShipping::HttpClient.new(
|
30
|
+
error_handler: FriendlyShipping::Services::ShipEngineLTL::BadRequestHandler
|
31
|
+
)
|
32
|
+
)
|
33
|
+
@token = token
|
34
|
+
@test = test
|
35
|
+
@client = client
|
36
|
+
end
|
37
|
+
|
38
|
+
# Get configured LTL carriers from ShipEngine
|
39
|
+
#
|
40
|
+
# @return [Result<ApiResult<Array<Carrier>>>] LTL carriers configured in your account
|
41
|
+
def carriers(debug: false)
|
42
|
+
request = FriendlyShipping::Request.new(
|
43
|
+
url: API_BASE + API_PATHS[:carriers],
|
44
|
+
headers: request_headers,
|
45
|
+
debug: debug
|
46
|
+
)
|
47
|
+
client.get(request).bind do |response|
|
48
|
+
ParseCarrierResponse.call(request: request, response: response)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Connect an LTL carrier to ShipEngine
|
53
|
+
#
|
54
|
+
# @param [Hash] credentials The carrier's connection information
|
55
|
+
# @param [String] scac Standard Carrier Alpha Code
|
56
|
+
#
|
57
|
+
# @return [Result<ApiResult<Hash>>] The unique carrier ID assigned by ShipEngine
|
58
|
+
def connect_carrier(credentials, scac, debug: false)
|
59
|
+
request = FriendlyShipping::Request.new(
|
60
|
+
url: API_BASE + API_PATHS[:connections] + "/#{scac}",
|
61
|
+
body: { credentials: credentials }.to_json,
|
62
|
+
headers: request_headers,
|
63
|
+
debug: debug
|
64
|
+
)
|
65
|
+
client.post(request).bind do |response|
|
66
|
+
Success(
|
67
|
+
ApiResult.new(
|
68
|
+
JSON.parse(response.body),
|
69
|
+
original_request: request,
|
70
|
+
original_response: response
|
71
|
+
)
|
72
|
+
)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Update an existing LTL carrier in ShipEngine
|
77
|
+
#
|
78
|
+
# @param [Hash] credentials The carrier's connection information
|
79
|
+
# @param [String] scac Standard Carrier Alpha Code
|
80
|
+
# @param [String] carrier_id The carrier ID from ShipEngine that you want to update
|
81
|
+
#
|
82
|
+
# @return [Result<ApiResult<Hash>>] The unique carrier ID assigned by ShipEngine
|
83
|
+
def update_carrier(credentials, scac, carrier_id, debug: false)
|
84
|
+
request = FriendlyShipping::Request.new(
|
85
|
+
url: API_BASE + API_PATHS[:connections] + "/#{scac}/#{carrier_id}",
|
86
|
+
body: { credentials: credentials }.to_json,
|
87
|
+
headers: request_headers,
|
88
|
+
debug: debug
|
89
|
+
)
|
90
|
+
client.put(request).bind do |response|
|
91
|
+
Success(
|
92
|
+
ApiResult.new(
|
93
|
+
JSON.parse(response.body),
|
94
|
+
original_request: request,
|
95
|
+
original_response: response
|
96
|
+
)
|
97
|
+
)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Request an LTL price quote from ShipEngine
|
102
|
+
#
|
103
|
+
# @param [String] carrier_id The carrier ID from ShipEngine that you want to quote against
|
104
|
+
# @param [Physical::Shipment] shipment The shipment to quote
|
105
|
+
# @param [FriendlyShipping::Services::ShipEngineLTL::QuoteOptions] options The options for the quote
|
106
|
+
#
|
107
|
+
# @return [Result<ApiResult<Hash>>] The price quote from ShipEngine
|
108
|
+
def request_quote(carrier_id, shipment, options, debug: false)
|
109
|
+
request = FriendlyShipping::Request.new(
|
110
|
+
url: API_BASE + API_PATHS[:quotes] + "/#{carrier_id}",
|
111
|
+
http_method: "POST",
|
112
|
+
body: SerializeQuoteRequest.call(shipment: shipment, options: options).to_json,
|
113
|
+
headers: request_headers,
|
114
|
+
debug: debug
|
115
|
+
)
|
116
|
+
client.post(request).bind do |response|
|
117
|
+
ParseQuoteResponse.call(request: request, response: response)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
attr_reader :token, :test, :client
|
124
|
+
|
125
|
+
def request_headers
|
126
|
+
{
|
127
|
+
content_type: :json,
|
128
|
+
"api-key": token
|
129
|
+
}
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -11,8 +11,10 @@ module FriendlyShipping
|
|
11
11
|
# values are _reference number values_. Example: `{ reference_numbers: { xn: 'my_reference_1 }`
|
12
12
|
# @option delivery_confirmation [Symbol] Can be set to any key from PACKAGE_DELIVERY_CONFIRMATION_CODES.
|
13
13
|
# Only possible for domestic shipments or shipments between the US and Puerto Rico.
|
14
|
-
# @option shipper_release [Boolean] Indicates that the package may be released by driver without a signature from
|
15
|
-
# consignee. Default: false
|
14
|
+
# @option shipper_release [Boolean] Indicates that the package may be released by driver without a signature from
|
15
|
+
# the consignee. Default: false
|
16
|
+
# @option declared_value [Boolean] When true, declared value (calculated as the sum of all items in the shipment)
|
17
|
+
# will be included in the request. Default: false
|
16
18
|
class LabelPackageOptions < FriendlyShipping::PackageOptions
|
17
19
|
PACKAGE_DELIVERY_CONFIRMATION_CODES = {
|
18
20
|
delivery_confirmation: 1,
|
@@ -20,17 +22,19 @@ module FriendlyShipping
|
|
20
22
|
delivery_confirmation_adult_signature_required: 3
|
21
23
|
}.freeze
|
22
24
|
|
23
|
-
attr_reader :reference_numbers, :shipper_release
|
25
|
+
attr_reader :reference_numbers, :shipper_release, :declared_value
|
24
26
|
|
25
27
|
def initialize(
|
26
28
|
reference_numbers: {},
|
27
29
|
delivery_confirmation: nil,
|
28
30
|
shipper_release: false,
|
31
|
+
declared_value: false,
|
29
32
|
**kwargs
|
30
33
|
)
|
31
34
|
@reference_numbers = reference_numbers
|
32
35
|
@delivery_confirmation = delivery_confirmation
|
33
36
|
@shipper_release = shipper_release
|
37
|
+
@declared_value = declared_value
|
34
38
|
super(**kwargs.merge(item_options_class: LabelItemOptions))
|
35
39
|
end
|
36
40
|
|
@@ -10,7 +10,8 @@ module FriendlyShipping
|
|
10
10
|
reference_numbers: {},
|
11
11
|
delivery_confirmation_code: nil,
|
12
12
|
shipper_release: false,
|
13
|
-
transmit_dimensions: true
|
13
|
+
transmit_dimensions: true,
|
14
|
+
declared_value: false
|
14
15
|
)
|
15
16
|
xml.Package do
|
16
17
|
xml.PackagingType do
|
@@ -49,6 +50,15 @@ module FriendlyShipping
|
|
49
50
|
xml.DCISType(delivery_confirmation_code)
|
50
51
|
end
|
51
52
|
end
|
53
|
+
if declared_value
|
54
|
+
xml.DeclaredValue do
|
55
|
+
xml.CurrencyCode('USD')
|
56
|
+
monetary_value = package.items.inject(Money.new(0, 'USD')) do |package_sum, item|
|
57
|
+
package_sum + (item.cost || Money.new(0, 'USD'))
|
58
|
+
end
|
59
|
+
xml.MonetaryValue(monetary_value)
|
60
|
+
end
|
61
|
+
end
|
52
62
|
end
|
53
63
|
|
54
64
|
reference_numbers.each do |reference_code, reference_number|
|
@@ -106,6 +106,8 @@ module FriendlyShipping
|
|
106
106
|
xml.SoldTo do
|
107
107
|
sold_to_location = options.sold_to || shipment.destination
|
108
108
|
SerializeShipmentAddressSnippet.call(xml: xml, location: sold_to_location)
|
109
|
+
xml.AccountNumber(options.sold_to.account_number) if options.sold_to.try(:account_number).present?
|
110
|
+
xml.TaxIdentificationNumber(options.sold_to.tax_id_number) if options.sold_to.try(:tax_id_number).present?
|
109
111
|
end
|
110
112
|
end
|
111
113
|
|
@@ -124,7 +126,7 @@ module FriendlyShipping
|
|
124
126
|
|
125
127
|
contents_description = shipment.packages.flat_map do |package|
|
126
128
|
package.items.map(&:description)
|
127
|
-
end.compact.join(', ').slice(0, 50)
|
129
|
+
end.compact.uniq.join(', ').slice(0, 50)
|
128
130
|
|
129
131
|
unless contents_description.empty?
|
130
132
|
xml.Description(contents_description)
|
@@ -160,7 +162,8 @@ module FriendlyShipping
|
|
160
162
|
package: package,
|
161
163
|
reference_numbers: reference_numbers,
|
162
164
|
delivery_confirmation_code: delivery_confirmation_code,
|
163
|
-
shipper_release: package_options.shipper_release
|
165
|
+
shipper_release: package_options.shipper_release,
|
166
|
+
declared_value: package_options.declared_value
|
164
167
|
)
|
165
168
|
end
|
166
169
|
end
|
@@ -263,7 +266,7 @@ module FriendlyShipping
|
|
263
266
|
xml.CommodityCode(item_options.commodity_code)
|
264
267
|
xml.OriginCountryCode(item_options.country_of_origin || shipment.origin.country.code)
|
265
268
|
xml.Unit do
|
266
|
-
xml.Value(cost
|
269
|
+
xml.Value(cost)
|
267
270
|
xml.Number(items.length)
|
268
271
|
xml.UnitOfMeasurement do
|
269
272
|
xml.Code(item_options.product_unit_of_measure_code)
|
@@ -7,17 +7,17 @@ module FriendlyShipping
|
|
7
7
|
class << self
|
8
8
|
def call(location:)
|
9
9
|
{
|
10
|
-
Name: location.company_name.presence || location.name,
|
10
|
+
Name: truncate(location.company_name.presence || location.name),
|
11
11
|
Address: {
|
12
12
|
AddressLine: address_line(location),
|
13
|
-
City: location.city,
|
13
|
+
City: truncate(location.city, length: 29),
|
14
14
|
StateProvinceCode: location.region&.code,
|
15
15
|
PostalCode: location.zip,
|
16
16
|
CountryCode: location.country&.code
|
17
17
|
},
|
18
|
-
AttentionName: location.name,
|
18
|
+
AttentionName: truncate(location.name),
|
19
19
|
Phone: {
|
20
|
-
Number: location.phone
|
20
|
+
Number: truncate(location.phone, length: 14)
|
21
21
|
}.compact.presence
|
22
22
|
}.compact
|
23
23
|
end
|
@@ -29,8 +29,12 @@ module FriendlyShipping
|
|
29
29
|
location.address1,
|
30
30
|
location.address2,
|
31
31
|
location.address3
|
32
|
-
].compact.reject(&:empty?)
|
33
|
-
address_lines.size > 1 ? address_lines : address_lines.first
|
32
|
+
].compact.reject(&:empty?).map { |e| truncate(e) }
|
33
|
+
address_lines.size > 1 ? address_lines : truncate(address_lines.first)
|
34
|
+
end
|
35
|
+
|
36
|
+
def truncate(value, length: 35)
|
37
|
+
value && value[0..(length - 1)]
|
34
38
|
end
|
35
39
|
end
|
36
40
|
end
|
@@ -94,7 +94,8 @@ module FriendlyShipping
|
|
94
94
|
|
95
95
|
rate_value =
|
96
96
|
if commercial_rate_requested_or_rate_is_zero && commercial_rate_available
|
97
|
-
rate_node.at(COMMERCIAL_RATE_TAG)&.text
|
97
|
+
commercial_rate = rate_node.at(COMMERCIAL_RATE_TAG)&.text.to_d
|
98
|
+
commercial_rate.zero? ? rate_node.at(COMMERCIAL_PLUS_RATE_TAG).text.to_d : commercial_rate
|
98
99
|
else
|
99
100
|
rate_node.at(RATE_TAG).text.to_d
|
100
101
|
end
|
@@ -106,10 +106,12 @@ module FriendlyShipping
|
|
106
106
|
# “0” = All Mail Classes
|
107
107
|
# “1” = Priority Mail Express
|
108
108
|
# “2” = Priority Mail
|
109
|
-
# “3” = First
|
109
|
+
# “3” = First-Class - replaced by Ground Advantage (up to 15.999 oz)
|
110
110
|
# “4” = Marketing Mail
|
111
111
|
# “5” = Periodicals
|
112
112
|
# “6” = Package Services
|
113
|
+
# “7” = Parcel Select Ground - replaced by Ground Advantage (1-70 lbs)
|
114
|
+
# “9” = Ground Advantage (1-70 lbs)
|
113
115
|
#
|
114
116
|
# However, no shipping methods really map to "Marketing Mail" or "Periodicals".
|
115
117
|
# This will likely be somewhat more work in the future.
|
@@ -117,7 +119,9 @@ module FriendlyShipping
|
|
117
119
|
'1' => 'Priority Mail Express',
|
118
120
|
'2' => 'Priority Mail',
|
119
121
|
'3' => 'First-Class',
|
120
|
-
'6' => 'Package Services'
|
122
|
+
'6' => 'Package Services',
|
123
|
+
'7' => 'Parcel Select Ground',
|
124
|
+
'9' => 'Ground Advantage'
|
121
125
|
}.freeze
|
122
126
|
|
123
127
|
# This code carries a few details about the shipment:
|
@@ -35,11 +35,13 @@ module FriendlyShipping
|
|
35
35
|
hold_for_pickup: '2',
|
36
36
|
sunday_holiday_delivery: '23'
|
37
37
|
},
|
38
|
-
priority_mail_cubic: '999'
|
38
|
+
priority_mail_cubic: '999',
|
39
|
+
ground_advantage: '1058'
|
39
40
|
}.freeze
|
40
41
|
|
41
42
|
SHIPPING_METHODS = [
|
42
43
|
['FIRST CLASS', 'First-Class'],
|
44
|
+
['GROUND ADVANTAGE', 'Ground Advantage', CLASS_IDS[:ground_advantage]],
|
43
45
|
['PACKAGE SERVICES', 'Package Services'],
|
44
46
|
['PRIORITY', 'Priority Mail'],
|
45
47
|
['PRIORITY MAIL EXPRESS', 'Priority Mail Express', CLASS_IDS[:priority_mail_express].values],
|
@@ -55,7 +57,7 @@ module FriendlyShipping
|
|
55
57
|
service_code: code,
|
56
58
|
domestic: true,
|
57
59
|
international: false,
|
58
|
-
data: { class_ids: class_ids }
|
60
|
+
data: { class_ids: Array(class_ids) }
|
59
61
|
)
|
60
62
|
end.freeze
|
61
63
|
end
|
@@ -47,7 +47,8 @@ module FriendlyShipping
|
|
47
47
|
|
48
48
|
rate_value =
|
49
49
|
if commercial_rate_requested_or_rate_is_zero && commercial_rate_available
|
50
|
-
rate_node.at(COMMERCIAL_RATE_TAG)&.text
|
50
|
+
commercial_rate = rate_node.at(COMMERCIAL_RATE_TAG)&.text.to_d
|
51
|
+
commercial_rate.zero? ? rate_node.at(COMMERCIAL_PLUS_RATE_TAG).text.to_d : commercial_rate
|
51
52
|
else
|
52
53
|
rate_node.at(RATE_TAG).text.to_d
|
53
54
|
end
|
@@ -56,12 +56,12 @@ module FriendlyShipping
|
|
56
56
|
end
|
57
57
|
|
58
58
|
def ounces_for(package)
|
59
|
-
ounces = package.weight.convert_to(:ounces).value.to_f.round(2).ceil
|
59
|
+
ounces = (package.weight.convert_to(:ounces).value.to_f % 16).round(2).ceil
|
60
60
|
ounces == 16 ? 15.999 : [ounces, 1].max
|
61
61
|
end
|
62
62
|
|
63
63
|
def pounds_for(package)
|
64
|
-
package.weight.convert_to(:pounds).value.to_f.
|
64
|
+
package.weight.convert_to(:pounds).value.to_f.floor
|
65
65
|
end
|
66
66
|
|
67
67
|
def girth(package)
|
data/lib/friendly_shipping.rb
CHANGED
@@ -17,6 +17,7 @@ require "friendly_shipping/api_result"
|
|
17
17
|
require "friendly_shipping/api_failure"
|
18
18
|
|
19
19
|
require "friendly_shipping/services/ship_engine"
|
20
|
+
require 'friendly_shipping/services/ship_engine_ltl'
|
20
21
|
require "friendly_shipping/services/ups"
|
21
22
|
require "friendly_shipping/services/ups_freight"
|
22
23
|
require "friendly_shipping/services/usps"
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: friendly_shipping
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.8.
|
4
|
+
version: 0.8.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Martin Meyerhoff
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: exe
|
11
11
|
cert_chain: []
|
12
|
-
date: 2023-
|
12
|
+
date: 2023-08-03 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: dry-monads
|
@@ -256,6 +256,7 @@ extra_rdoc_files: []
|
|
256
256
|
files:
|
257
257
|
- ".circleci/config.yml"
|
258
258
|
- ".env.template"
|
259
|
+
- ".env.test"
|
259
260
|
- ".github/dependabot.yml"
|
260
261
|
- ".gitignore"
|
261
262
|
- ".rspec"
|
@@ -298,6 +299,16 @@ files:
|
|
298
299
|
- lib/friendly_shipping/services/ship_engine/rate_estimates_options.rb
|
299
300
|
- lib/friendly_shipping/services/ship_engine/serialize_label_shipment.rb
|
300
301
|
- lib/friendly_shipping/services/ship_engine/serialize_rate_estimate_request.rb
|
302
|
+
- lib/friendly_shipping/services/ship_engine_ltl.rb
|
303
|
+
- lib/friendly_shipping/services/ship_engine_ltl/bad_request.rb
|
304
|
+
- lib/friendly_shipping/services/ship_engine_ltl/bad_request_handler.rb
|
305
|
+
- lib/friendly_shipping/services/ship_engine_ltl/item_options.rb
|
306
|
+
- lib/friendly_shipping/services/ship_engine_ltl/package_options.rb
|
307
|
+
- lib/friendly_shipping/services/ship_engine_ltl/parse_carrier_response.rb
|
308
|
+
- lib/friendly_shipping/services/ship_engine_ltl/parse_quote_response.rb
|
309
|
+
- lib/friendly_shipping/services/ship_engine_ltl/quote_options.rb
|
310
|
+
- lib/friendly_shipping/services/ship_engine_ltl/serialize_packages.rb
|
311
|
+
- lib/friendly_shipping/services/ship_engine_ltl/serialize_quote_request.rb
|
301
312
|
- lib/friendly_shipping/services/ups.rb
|
302
313
|
- lib/friendly_shipping/services/ups/label.rb
|
303
314
|
- lib/friendly_shipping/services/ups/label_billing_options.rb
|
@@ -406,7 +417,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
406
417
|
- !ruby/object:Gem::Version
|
407
418
|
version: '0'
|
408
419
|
requirements: []
|
409
|
-
rubygems_version: 3.
|
420
|
+
rubygems_version: 3.4.10
|
410
421
|
signing_key:
|
411
422
|
specification_version: 4
|
412
423
|
summary: An integration layer for shipping services
|