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.
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