friendly_shipping 0.7.3 → 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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.env.template +4 -0
  3. data/.env.test +4 -0
  4. data/CHANGELOG.md +23 -0
  5. data/friendly_shipping.gemspec +1 -1
  6. data/lib/friendly_shipping/package_options.rb +4 -0
  7. data/lib/friendly_shipping/rate.rb +1 -1
  8. data/lib/friendly_shipping/services/ship_engine/label_customs_options.rb +25 -0
  9. data/lib/friendly_shipping/services/ship_engine/label_item_options.rb +27 -0
  10. data/lib/friendly_shipping/services/ship_engine/label_options.rb +5 -1
  11. data/lib/friendly_shipping/services/ship_engine/label_package_options.rb +2 -1
  12. data/lib/friendly_shipping/services/ship_engine/serialize_label_shipment.rb +37 -0
  13. data/lib/friendly_shipping/services/ship_engine_ltl/bad_request.rb +9 -0
  14. data/lib/friendly_shipping/services/ship_engine_ltl/bad_request_handler.rb +33 -0
  15. data/lib/friendly_shipping/services/ship_engine_ltl/item_options.rb +31 -0
  16. data/lib/friendly_shipping/services/ship_engine_ltl/package_options.rb +15 -0
  17. data/lib/friendly_shipping/services/ship_engine_ltl/parse_carrier_response.rb +49 -0
  18. data/lib/friendly_shipping/services/ship_engine_ltl/parse_quote_response.rb +72 -0
  19. data/lib/friendly_shipping/services/ship_engine_ltl/quote_options.rb +34 -0
  20. data/lib/friendly_shipping/services/ship_engine_ltl/serialize_packages.rb +41 -0
  21. data/lib/friendly_shipping/services/ship_engine_ltl/serialize_quote_request.rb +109 -0
  22. data/lib/friendly_shipping/services/ship_engine_ltl.rb +133 -0
  23. data/lib/friendly_shipping/services/ups/label_item_options.rb +4 -1
  24. data/lib/friendly_shipping/services/ups/label_package_options.rb +7 -3
  25. data/lib/friendly_shipping/services/ups/parse_rate_response.rb +3 -3
  26. data/lib/friendly_shipping/services/ups/parse_shipment_accept_response.rb +1 -1
  27. data/lib/friendly_shipping/services/ups/serialize_address_snippet.rb +5 -2
  28. data/lib/friendly_shipping/services/ups/serialize_package_node.rb +11 -1
  29. data/lib/friendly_shipping/services/ups/serialize_shipment_confirm_request.rb +11 -7
  30. data/lib/friendly_shipping/services/ups_freight/api_error.rb +2 -0
  31. data/lib/friendly_shipping/services/ups_freight/generate_location_hash.rb +10 -6
  32. data/lib/friendly_shipping/services/usps/parse_package_rate.rb +2 -1
  33. data/lib/friendly_shipping/services/usps/parse_time_in_transit_response.rb +6 -2
  34. data/lib/friendly_shipping/services/usps/rate_estimate_package_options.rb +4 -8
  35. data/lib/friendly_shipping/services/usps/shipping_methods.rb +4 -2
  36. data/lib/friendly_shipping/services/usps_international/parse_package_rate.rb +81 -0
  37. data/lib/friendly_shipping/services/usps_international/parse_rate_response.rb +86 -0
  38. data/lib/friendly_shipping/services/usps_international/rate_estimate_options.rb +28 -0
  39. data/lib/friendly_shipping/services/usps_international/rate_estimate_package_options.rb +45 -0
  40. data/lib/friendly_shipping/services/usps_international/serialize_rate_request.rb +75 -0
  41. data/lib/friendly_shipping/services/usps_international/shipping_methods.rb +38 -0
  42. data/lib/friendly_shipping/services/usps_international.rb +80 -0
  43. data/lib/friendly_shipping/version.rb +1 -1
  44. data/lib/friendly_shipping.rb +2 -0
  45. metadata +24 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4c32d3c28aea8a8cc0367288ea92fd4c1f29c5e18eb680a82f273e3f4319d14e
4
- data.tar.gz: 6618acc12ea1a92156dbfd2864a812d4c44cebd2b4f4c05b13d3d19d20d6fc62
3
+ metadata.gz: 7bc054d44e12f0d44f1240a8c831918fa7eec158f8cc66ffb104586a2a2f4180
4
+ data.tar.gz: 900e8bb08f94858c0b147d886f9eaeae72c3db74efe04b7cbe8a3d9e7184f4fe
5
5
  SHA512:
6
- metadata.gz: ef98ea7a648d25aba9d2d2df3d4d624a5c6413f1008d6bd9e2aff3520779aee02c24163242152698cdacf91a9c9b75a1686f950a53b1eb1aa406f6882c2864fe
7
- data.tar.gz: 6e6d8669b5fa64c18cbf89a215e642042f1bcc31f35900a9836782bd80ca307e6bdeafdf36b8a946e0d3da3068338b1db69133827af0ec59cd2f538cb1c74492
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
@@ -0,0 +1,4 @@
1
+ # .env.test
2
+
3
+ SHIPENGINE_LTL_CARRIER_ID=abc-123-def
4
+ SHIPENGINE_LTL_CARRIER_SCAC=UPGF
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
 
@@ -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"
@@ -24,5 +24,9 @@ module FriendlyShipping
24
24
 
25
25
  attr_reader :item_options,
26
26
  :item_options_class
27
+
28
+ def value_or_default(key, default, kwargs)
29
+ kwargs.key?(key) ? kwargs.delete(key) : default
30
+ end
27
31
  end
28
32
  end
@@ -39,7 +39,7 @@ module FriendlyShipping
39
39
  def total_amount
40
40
  raise NoAmountsGiven if amounts.empty?
41
41
 
42
- amounts.map { |_name, amount| amount }.sum
42
+ amounts.map { |_name, amount| amount }.sum(Money.new(0, 'USD'))
43
43
  end
44
44
  end
45
45
  end
@@ -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,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FriendlyShipping
4
+ module Services
5
+ class ShipEngineLTL
6
+ class BadRequest < ShipEngine::BadRequest; end
7
+ end
8
+ end
9
+ 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