friendly_shipping 0.7.3 → 0.8.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.env.template +4 -0
- data/.env.test +4 -0
- data/CHANGELOG.md +23 -0
- data/friendly_shipping.gemspec +1 -1
- data/lib/friendly_shipping/package_options.rb +4 -0
- data/lib/friendly_shipping/rate.rb +1 -1
- data/lib/friendly_shipping/services/ship_engine/label_customs_options.rb +25 -0
- data/lib/friendly_shipping/services/ship_engine/label_item_options.rb +27 -0
- data/lib/friendly_shipping/services/ship_engine/label_options.rb +5 -1
- data/lib/friendly_shipping/services/ship_engine/label_package_options.rb +2 -1
- data/lib/friendly_shipping/services/ship_engine/serialize_label_shipment.rb +37 -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_item_options.rb +4 -1
- data/lib/friendly_shipping/services/ups/label_package_options.rb +7 -3
- data/lib/friendly_shipping/services/ups/parse_rate_response.rb +3 -3
- data/lib/friendly_shipping/services/ups/parse_shipment_accept_response.rb +1 -1
- data/lib/friendly_shipping/services/ups/serialize_address_snippet.rb +5 -2
- data/lib/friendly_shipping/services/ups/serialize_package_node.rb +11 -1
- data/lib/friendly_shipping/services/ups/serialize_shipment_confirm_request.rb +11 -7
- data/lib/friendly_shipping/services/ups_freight/api_error.rb +2 -0
- 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/rate_estimate_package_options.rb +4 -8
- data/lib/friendly_shipping/services/usps/shipping_methods.rb +4 -2
- data/lib/friendly_shipping/services/usps_international/parse_package_rate.rb +81 -0
- data/lib/friendly_shipping/services/usps_international/parse_rate_response.rb +86 -0
- data/lib/friendly_shipping/services/usps_international/rate_estimate_options.rb +28 -0
- data/lib/friendly_shipping/services/usps_international/rate_estimate_package_options.rb +45 -0
- data/lib/friendly_shipping/services/usps_international/serialize_rate_request.rb +75 -0
- data/lib/friendly_shipping/services/usps_international/shipping_methods.rb +38 -0
- data/lib/friendly_shipping/services/usps_international.rb +80 -0
- data/lib/friendly_shipping/version.rb +1 -1
- data/lib/friendly_shipping.rb +2 -0
- metadata +24 -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,29 @@ 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
|
+
|
18
|
+
## [0.8.0] - 2023-04-18
|
19
|
+
- Rails 7 support: Fix deprecation warning about ActiveSupport#sum (#164)
|
20
|
+
- UPS Service: Truncate product descriptions (#163)
|
21
|
+
- TForce Service: Handle Timeouts gracefully (#162)
|
22
|
+
- UPS Service: Support per-item origin countries for paperless invoices (#161)
|
23
|
+
- USPS Service: Fix for currency formatting when shipping internationally (#160)
|
24
|
+
- ShipEngine Service: Add support for customs information (#159)
|
25
|
+
- UPS Service: Require both name and attention name for international shipping (#158)
|
26
|
+
- UPS Service: Allow third-party billing for taxes and fees (#156)
|
27
|
+
- USPS: New service for international shipping (#155)
|
28
|
+
- UPS Service: Parse missing package charges (#154)
|
29
|
+
|
7
30
|
## [0.7.3] - 2023-01-24
|
8
31
|
- UPS Service: Record USPS tracking code (#153)
|
9
32
|
|
data/friendly_shipping.gemspec
CHANGED
@@ -7,7 +7,7 @@ require "friendly_shipping/version"
|
|
7
7
|
Gem::Specification.new do |spec|
|
8
8
|
spec.name = "friendly_shipping"
|
9
9
|
spec.version = FriendlyShipping::VERSION
|
10
|
-
spec.authors = ["Martin Meyerhoff"]
|
10
|
+
spec.authors = ["Martin Meyerhoff", "Matthew Bass"]
|
11
11
|
spec.email = ["mamhoff@gmail.com"]
|
12
12
|
|
13
13
|
spec.summary = "An integration layer for shipping services"
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FriendlyShipping
|
4
|
+
module Services
|
5
|
+
class ShipEngine
|
6
|
+
# Represents customs options for obtaining international shipment labels.
|
7
|
+
# @option contents [String] The contents of the shipment.
|
8
|
+
# Valid values are: gift, merchandise, returned_goods, documents, sample
|
9
|
+
# @option non_delivery [String] Indicates what should be done if the shipment cannot be delivered.
|
10
|
+
# Valid values are: treat_as_abandoned, return_to_sender
|
11
|
+
class LabelCustomsOptions
|
12
|
+
attr_reader :contents,
|
13
|
+
:non_delivery
|
14
|
+
|
15
|
+
def initialize(
|
16
|
+
contents: "merchandise",
|
17
|
+
non_delivery: "return_to_sender"
|
18
|
+
)
|
19
|
+
@contents = contents
|
20
|
+
@non_delivery = non_delivery
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'friendly_shipping/item_options'
|
4
|
+
|
5
|
+
module FriendlyShipping
|
6
|
+
module Services
|
7
|
+
class ShipEngine
|
8
|
+
# Represents item options for obtaining shipment labels.
|
9
|
+
# @option commodity_code [String] This item's HS or NMFC code for international shipments.
|
10
|
+
# @option country_of_origin [String] This item's country of origin for international shipments.
|
11
|
+
class LabelItemOptions < FriendlyShipping::ItemOptions
|
12
|
+
attr_reader :commodity_code,
|
13
|
+
:country_of_origin
|
14
|
+
|
15
|
+
def initialize(
|
16
|
+
commodity_code: nil,
|
17
|
+
country_of_origin: nil,
|
18
|
+
**kwargs
|
19
|
+
)
|
20
|
+
@commodity_code = commodity_code
|
21
|
+
@country_of_origin = country_of_origin
|
22
|
+
super(**kwargs)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'friendly_shipping/shipment_options'
|
4
|
+
require 'friendly_shipping/services/ship_engine/label_customs_options'
|
4
5
|
|
5
6
|
module FriendlyShipping
|
6
7
|
module Services
|
@@ -17,19 +18,22 @@ module FriendlyShipping
|
|
17
18
|
attr_reader :shipping_method,
|
18
19
|
:label_download_type,
|
19
20
|
:label_format,
|
20
|
-
:label_image_id
|
21
|
+
:label_image_id,
|
22
|
+
:customs_options
|
21
23
|
|
22
24
|
def initialize(
|
23
25
|
shipping_method:,
|
24
26
|
label_download_type: :url,
|
25
27
|
label_format: :pdf,
|
26
28
|
label_image_id: nil,
|
29
|
+
customs_options: LabelCustomsOptions.new,
|
27
30
|
**kwargs
|
28
31
|
)
|
29
32
|
@shipping_method = shipping_method
|
30
33
|
@label_download_type = label_download_type
|
31
34
|
@label_format = label_format
|
32
35
|
@label_image_id = label_image_id
|
36
|
+
@customs_options = customs_options
|
33
37
|
super(**kwargs.merge(package_options_class: LabelPackageOptions))
|
34
38
|
end
|
35
39
|
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'friendly_shipping/package_options'
|
4
|
+
require 'friendly_shipping/services/ship_engine/label_item_options'
|
4
5
|
|
5
6
|
module FriendlyShipping
|
6
7
|
module Services
|
@@ -20,7 +21,7 @@ module FriendlyShipping
|
|
20
21
|
def initialize(package_code: nil, messages: [], **kwargs)
|
21
22
|
@package_code = package_code
|
22
23
|
@messages = messages
|
23
|
-
super(**kwargs)
|
24
|
+
super(**kwargs.merge(item_options_class: LabelItemOptions))
|
24
25
|
end
|
25
26
|
end
|
26
27
|
end
|
@@ -21,6 +21,10 @@ module FriendlyShipping
|
|
21
21
|
shipment_hash[:shipment][:carrier_id] = options.shipping_method.carrier.id
|
22
22
|
end
|
23
23
|
|
24
|
+
if international?(shipment)
|
25
|
+
shipment_hash[:shipment][:customs] = serialize_customs(shipment.packages, options)
|
26
|
+
end
|
27
|
+
|
24
28
|
if test
|
25
29
|
shipment_hash[:test_label] = true
|
26
30
|
end
|
@@ -68,6 +72,35 @@ module FriendlyShipping
|
|
68
72
|
end
|
69
73
|
end
|
70
74
|
|
75
|
+
def serialize_customs(packages, options)
|
76
|
+
{
|
77
|
+
contents: options.customs_options.contents,
|
78
|
+
non_delivery: options.customs_options.non_delivery,
|
79
|
+
customs_items: serialize_customs_items(packages, options)
|
80
|
+
}
|
81
|
+
end
|
82
|
+
|
83
|
+
def serialize_customs_items(packages, options)
|
84
|
+
packages.map do |package|
|
85
|
+
package.items.group_by(&:sku).map do |sku, items|
|
86
|
+
reference_item = items.first
|
87
|
+
package_options = options.options_for_package(package)
|
88
|
+
item_options = package_options.options_for_item(reference_item)
|
89
|
+
{
|
90
|
+
sku: sku,
|
91
|
+
description: reference_item.description,
|
92
|
+
quantity: items.count,
|
93
|
+
value: {
|
94
|
+
amount: reference_item.cost.to_d,
|
95
|
+
currency: reference_item.cost.currency.to_s
|
96
|
+
},
|
97
|
+
harmonized_tariff_code: item_options.commodity_code,
|
98
|
+
country_of_origin: item_options.country_of_origin
|
99
|
+
}
|
100
|
+
end
|
101
|
+
end.flatten
|
102
|
+
end
|
103
|
+
|
71
104
|
def serialize_weight(weight)
|
72
105
|
ounces = weight.convert_to(:ounce).value.to_f
|
73
106
|
{
|
@@ -78,6 +111,10 @@ module FriendlyShipping
|
|
78
111
|
}
|
79
112
|
}
|
80
113
|
end
|
114
|
+
|
115
|
+
def international?(shipment)
|
116
|
+
shipment.origin.country != shipment.destination.country
|
117
|
+
end
|
81
118
|
end
|
82
119
|
end
|
83
120
|
end
|
@@ -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
|