friendly_shipping 0.8.0 → 0.8.1

Sign up to get free protection for your applications and to get access to all the features.
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