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.
Files changed (26) hide show
  1. checksums.yaml +4 -4
  2. data/.env.template +4 -0
  3. data/.env.test +4 -0
  4. data/CHANGELOG.md +11 -0
  5. data/lib/friendly_shipping/services/ship_engine_ltl/bad_request.rb +9 -0
  6. data/lib/friendly_shipping/services/ship_engine_ltl/bad_request_handler.rb +33 -0
  7. data/lib/friendly_shipping/services/ship_engine_ltl/item_options.rb +31 -0
  8. data/lib/friendly_shipping/services/ship_engine_ltl/package_options.rb +15 -0
  9. data/lib/friendly_shipping/services/ship_engine_ltl/parse_carrier_response.rb +49 -0
  10. data/lib/friendly_shipping/services/ship_engine_ltl/parse_quote_response.rb +72 -0
  11. data/lib/friendly_shipping/services/ship_engine_ltl/quote_options.rb +34 -0
  12. data/lib/friendly_shipping/services/ship_engine_ltl/serialize_packages.rb +41 -0
  13. data/lib/friendly_shipping/services/ship_engine_ltl/serialize_quote_request.rb +109 -0
  14. data/lib/friendly_shipping/services/ship_engine_ltl.rb +133 -0
  15. data/lib/friendly_shipping/services/ups/label_package_options.rb +7 -3
  16. data/lib/friendly_shipping/services/ups/serialize_package_node.rb +11 -1
  17. data/lib/friendly_shipping/services/ups/serialize_shipment_confirm_request.rb +6 -3
  18. data/lib/friendly_shipping/services/ups_freight/generate_location_hash.rb +10 -6
  19. data/lib/friendly_shipping/services/usps/parse_package_rate.rb +2 -1
  20. data/lib/friendly_shipping/services/usps/parse_time_in_transit_response.rb +6 -2
  21. data/lib/friendly_shipping/services/usps/shipping_methods.rb +4 -2
  22. data/lib/friendly_shipping/services/usps_international/parse_package_rate.rb +2 -1
  23. data/lib/friendly_shipping/services/usps_international/serialize_rate_request.rb +2 -2
  24. data/lib/friendly_shipping/version.rb +1 -1
  25. data/lib/friendly_shipping.rb +1 -0
  26. metadata +14 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 269d5f03467b9fc61a71f0585c7ff434156465e008d01e26e319666a15b13fb1
4
- data.tar.gz: 006fe64631e6b7eef89c796d5474dafb71896e7eff8d081572c6a85a641fb537
3
+ metadata.gz: 7bc054d44e12f0d44f1240a8c831918fa7eec158f8cc66ffb104586a2a2f4180
4
+ data.tar.gz: 900e8bb08f94858c0b147d886f9eaeae72c3db74efe04b7cbe8a3d9e7184f4fe
5
5
  SHA512:
6
- metadata.gz: b0ca75ff4a78b69b1da507dbc5fc9872629217f5a28fd37ea9645078bbda1b592beb74eb71b44b69c8e4649b878e1e715060ad545f8b8b8bdd19a3b256996112
7
- data.tar.gz: 1945415f57c1ab8d22bf80e582b22938235965c1db3981c78ef3c6c76087507ae4c5a9e3db7a2a1e27899a9e90ef2a3d97b6cee24673b51698df0c07747aae49
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,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,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
@@ -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 the
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 * items.length)
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&.to_d || rate_node.at(COMMERCIAL_PLUS_RATE_TAG).text.to_d
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 Class Mail
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&.to_d || rate_node.at(COMMERCIAL_PLUS_RATE_TAG).text.to_d
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.round(2).ceil
64
+ package.weight.convert_to(:pounds).value.to_f.floor
65
65
  end
66
66
 
67
67
  def girth(package)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FriendlyShipping
4
- VERSION = "0.8.0"
4
+ VERSION = "0.8.1"
5
5
  end
@@ -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.0
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-04-18 00:00:00.000000000 Z
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.3.26
420
+ rubygems_version: 3.4.10
410
421
  signing_key:
411
422
  specification_version: 4
412
423
  summary: An integration layer for shipping services