friendly_shipping 0.5 → 0.6.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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop-relaxed.yml +3 -4
  3. data/.rubocop.yml +9 -0
  4. data/CHANGELOG.md +29 -0
  5. data/friendly_shipping.gemspec +12 -12
  6. data/lib/friendly_shipping/api_failure.rb +2 -15
  7. data/lib/friendly_shipping/http_client.rb +16 -9
  8. data/lib/friendly_shipping/request.rb +7 -1
  9. data/lib/friendly_shipping/services/ship_engine/bad_request_handler.rb +15 -3
  10. data/lib/friendly_shipping/services/ups.rb +9 -1
  11. data/lib/friendly_shipping/services/ups/parse_address_classification_response.rb +5 -2
  12. data/lib/friendly_shipping/services/ups/parse_address_validation_response.rb +5 -2
  13. data/lib/friendly_shipping/services/ups/parse_city_state_lookup_response.rb +5 -2
  14. data/lib/friendly_shipping/services/ups/parse_rate_response.rb +5 -1
  15. data/lib/friendly_shipping/services/ups/parse_shipment_accept_response.rb +5 -1
  16. data/lib/friendly_shipping/services/ups/parse_shipment_confirm_response.rb +5 -1
  17. data/lib/friendly_shipping/services/ups/parse_time_in_transit_response.rb +5 -1
  18. data/lib/friendly_shipping/services/ups/parse_void_shipment_response.rb +5 -1
  19. data/lib/friendly_shipping/services/ups/parse_xml_response.rb +16 -7
  20. data/lib/friendly_shipping/services/ups_freight.rb +44 -13
  21. data/lib/friendly_shipping/services/ups_freight/generate_delivery_options_hash.rb +21 -0
  22. data/lib/friendly_shipping/services/ups_freight/generate_document_options_hash.rb +28 -0
  23. data/lib/friendly_shipping/services/ups_freight/generate_email_options_hash.rb +25 -0
  24. data/lib/friendly_shipping/services/ups_freight/generate_freight_rate_request_hash.rb +2 -10
  25. data/lib/friendly_shipping/services/ups_freight/generate_freight_ship_request_hash.rb +81 -0
  26. data/lib/friendly_shipping/services/ups_freight/generate_location_hash.rb +5 -2
  27. data/lib/friendly_shipping/services/ups_freight/generate_pickup_options_hash.rb +21 -0
  28. data/lib/friendly_shipping/services/ups_freight/generate_pickup_request_hash.rb +31 -0
  29. data/lib/friendly_shipping/services/ups_freight/label_delivery_options.rb +29 -0
  30. data/lib/friendly_shipping/services/ups_freight/label_document_options.rb +56 -0
  31. data/lib/friendly_shipping/services/ups_freight/label_email_options.rb +40 -0
  32. data/lib/friendly_shipping/services/ups_freight/label_item_options.rb +10 -0
  33. data/lib/friendly_shipping/services/ups_freight/label_options.rb +37 -0
  34. data/lib/friendly_shipping/services/ups_freight/label_package_options.rb +10 -0
  35. data/lib/friendly_shipping/services/ups_freight/label_pickup_options.rb +29 -0
  36. data/lib/friendly_shipping/services/ups_freight/parse_freight_label_response.rb +57 -0
  37. data/lib/friendly_shipping/services/ups_freight/parse_freight_rate_response.rb +29 -32
  38. data/lib/friendly_shipping/services/ups_freight/parse_shipment_document.rb +24 -0
  39. data/lib/friendly_shipping/services/ups_freight/pickup_request_options.rb +29 -0
  40. data/lib/friendly_shipping/services/ups_freight/rates_options.rb +3 -6
  41. data/lib/friendly_shipping/services/ups_freight/restful_api_error_handler.rb +30 -0
  42. data/lib/friendly_shipping/services/ups_freight/shipment_document.rb +21 -0
  43. data/lib/friendly_shipping/services/ups_freight/shipment_information.rb +35 -0
  44. data/lib/friendly_shipping/services/usps.rb +1 -0
  45. data/lib/friendly_shipping/services/usps/parse_address_validation_response.rb +5 -1
  46. data/lib/friendly_shipping/services/usps/parse_city_state_lookup_response.rb +5 -1
  47. data/lib/friendly_shipping/services/usps/parse_rate_response.rb +5 -2
  48. data/lib/friendly_shipping/services/usps/parse_time_in_transit_response.rb +6 -2
  49. data/lib/friendly_shipping/services/usps/parse_xml_response.rb +15 -5
  50. data/lib/friendly_shipping/services/usps/rate_estimate_options.rb +1 -1
  51. data/lib/friendly_shipping/services/usps/rate_estimate_package_options.rb +4 -1
  52. data/lib/friendly_shipping/services/usps/serialize_rate_request.rb +11 -5
  53. data/lib/friendly_shipping/services/usps/shipping_methods.rb +4 -4
  54. data/lib/friendly_shipping/version.rb +1 -1
  55. metadata +74 -39
  56. data/lib/friendly_shipping/services/ups_freight/generate_ups_security_hash.rb +0 -23
  57. data/lib/friendly_shipping/services/ups_freight/parse_json_response.rb +0 -38
@@ -4,11 +4,21 @@ require 'dry/monads/result'
4
4
  require 'friendly_shipping/http_client'
5
5
  require 'friendly_shipping/services/ups_freight/shipping_methods'
6
6
  require 'friendly_shipping/services/ups_freight/rates_options'
7
+ require 'friendly_shipping/services/ups_freight/label_options'
7
8
  require 'friendly_shipping/services/ups_freight/rates_package_options'
8
9
  require 'friendly_shipping/services/ups_freight/rates_item_options'
10
+ require 'friendly_shipping/services/ups_freight/label_package_options'
11
+ require 'friendly_shipping/services/ups_freight/label_item_options'
12
+ require 'friendly_shipping/services/ups_freight/label_document_options'
13
+ require 'friendly_shipping/services/ups_freight/label_email_options'
14
+ require 'friendly_shipping/services/ups_freight/label_pickup_options'
15
+ require 'friendly_shipping/services/ups_freight/label_delivery_options'
16
+ require 'friendly_shipping/services/ups_freight/pickup_request_options'
17
+ require 'friendly_shipping/services/ups_freight/parse_freight_label_response'
9
18
  require 'friendly_shipping/services/ups_freight/parse_freight_rate_response'
10
19
  require 'friendly_shipping/services/ups_freight/generate_freight_rate_request_hash'
11
- require 'friendly_shipping/services/ups_freight/generate_ups_security_hash'
20
+ require 'friendly_shipping/services/ups_freight/generate_freight_ship_request_hash'
21
+ require 'friendly_shipping/services/ups_freight/restful_api_error_handler'
12
22
 
13
23
  module FriendlyShipping
14
24
  module Services
@@ -28,10 +38,11 @@ module FriendlyShipping
28
38
  LIVE_URL = 'https://onlinetools.ups.com'
29
39
 
30
40
  RESOURCES = {
31
- rates: '/rest/FreightRate'
41
+ rates: '/ship/v1801/freight/rating/ground',
42
+ labels: '/ship/v1607/freight/shipments/Ground'
32
43
  }.freeze
33
44
 
34
- def initialize(key:, login:, password:, test: true, client: HttpClient.new)
45
+ def initialize(key:, login:, password:, test: true, client: HttpClient.new(error_handler: RestfulApiErrorHandler))
35
46
  @key = key
36
47
  @login = login
37
48
  @password = password
@@ -44,28 +55,48 @@ module FriendlyShipping
44
55
  end
45
56
 
46
57
  # Get rates for a shipment
47
- # @param [Physical::Shipment] location The shipment we want to get rates for
58
+ # @param [Physical::Shipment] shipment The shipment we want to get rates for
48
59
  # @param [FriendlyShipping::Services::UpsFreight::RatesOptions] options Options for obtaining rates for this shipment.
49
60
  # @return [Result<ApiResult<Array<Rate>>>] The rates returned from UPS encoded in a
50
61
  # `FriendlyShipping::ApiResult` object.
51
62
  def rate_estimates(shipment, options:, debug: false)
52
63
  freight_rate_request_hash = GenerateFreightRateRequestHash.call(shipment: shipment, options: options)
53
- url = base_url + RESOURCES[:rates]
54
- request = FriendlyShipping::Request.new(
55
- url: url,
56
- body: authentication_hash.merge(freight_rate_request_hash).to_json,
57
- debug: debug
58
- )
64
+ request = build_request(:rates, freight_rate_request_hash, debug)
59
65
 
60
- client.post(request).bind do |response|
66
+ client.post(request).fmap do |response|
61
67
  ParseFreightRateResponse.call(response: response, request: request)
62
68
  end
63
69
  end
64
70
 
71
+ # Get labels for a shipment
72
+ # @param [Physical::Shipment] shipment The shipment we want to get rates for
73
+ # @param [FriendlyShipping::Services::UpsFreight::LabelOptions] options Options for shipping this shipment.
74
+ # @return [Result<ApiResult<ShipmentInformation>] The information that you need for shipping this shipment.
75
+ def labels(shipment, options:, debug: false)
76
+ freight_ship_request_hash = GenerateFreightShipRequestHash.call(shipment: shipment, options: options)
77
+ request = build_request(:labels, freight_ship_request_hash, debug)
78
+
79
+ client.post(request).fmap do |response|
80
+ ParseFreightLabelResponse.call(response: response, request: request)
81
+ end
82
+ end
83
+
65
84
  private
66
85
 
67
- def authentication_hash
68
- GenerateUpsSecurityHash.call(key: key, login: login, password: password)
86
+ def build_request(action, payload, debug)
87
+ url = base_url + RESOURCES[action]
88
+ FriendlyShipping::Request.new(
89
+ url: url,
90
+ body: payload.to_json,
91
+ headers: {
92
+ Content_Type: 'application/json',
93
+ Accept: 'application/json',
94
+ Username: login,
95
+ Password: password,
96
+ AccessLicenseNumber: key
97
+ },
98
+ debug: debug
99
+ )
69
100
  end
70
101
 
71
102
  def base_url
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FriendlyShipping
4
+ module Services
5
+ class UpsFreight
6
+ class GenerateDeliveryOptionsHash
7
+ def self.call(delivery_options:)
8
+ {
9
+ DeliveryOptions: {
10
+ LiftGateRequiredIndicator: delivery_options.lift_gate_required ? "" : nil,
11
+ WeekendPickupIndicator: delivery_options.weekend_delivery ? "" : nil,
12
+ InsidePickupIndicator: delivery_options.inside_delivery ? "" : nil,
13
+ HolidayPickupIndicator: delivery_options.holiday_delivery ? "" : nil,
14
+ LimitedAccessPickupIndicator: delivery_options.limited_access_delivery ? "" : nil
15
+ }.compact.presence
16
+ }.compact.presence
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FriendlyShipping
4
+ module Services
5
+ class UpsFreight
6
+ class GenerateDocumentOptionsHash
7
+ def self.call(document_options:)
8
+ {
9
+ Type: {
10
+ Code: document_options.document_type_code
11
+ },
12
+ LabelsPerPage: document_options.labels_per_page,
13
+ Format: {
14
+ Code: document_options.format_code
15
+ },
16
+ PrintFormat: {
17
+ Code: document_options.thermal_code,
18
+ },
19
+ PrintSize: {
20
+ Length: document_options.length,
21
+ Width: document_options.width,
22
+ }
23
+ }
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FriendlyShipping
4
+ module Services
5
+ class UpsFreight
6
+ class GenerateEmailOptionsHash
7
+ def self.call(email_options:)
8
+ {
9
+ EMailInformation: {
10
+ EMailType: {
11
+ Code: email_options.email_type_code,
12
+ },
13
+ EMail: {
14
+ EMailAddress: email_options.email,
15
+ UndeliverableEMailAddress: email_options.undeliverable_email,
16
+ EMailText: email_options.body,
17
+ Subject: email_options.subject
18
+ }.compact
19
+ }
20
+ }
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'friendly_shipping/services/ups_freight/generate_location_hash'
4
+ require 'friendly_shipping/services/ups_freight/generate_pickup_request_hash'
4
5
 
5
6
  module FriendlyShipping
6
7
  module Services
@@ -20,22 +21,13 @@ module FriendlyShipping
20
21
  },
21
22
  Commodity: options.commodity_information_generator.call(shipment: shipment, options: options),
22
23
  TimeInTransitIndicator: 'true',
23
- PickupRequest: pickup_request(options)
24
+ PickupRequest: GeneratePickupRequestHash.call(pickup_request_options: options.pickup_request_options),
24
25
  }.compact.merge(handling_units(shipment, options).reduce(&:merge).to_h)
25
26
  }
26
27
  end
27
28
 
28
29
  private
29
30
 
30
- def pickup_request(options)
31
- return unless options.pickup_date
32
-
33
- {
34
- PickupDate: options.pickup_date.strftime('%Y%m%d'),
35
- AdditionalComments: options.pickup_comments
36
- }
37
- end
38
-
39
31
  def handling_units(shipment, options)
40
32
  all_package_options = shipment.packages.map { |package| options.options_for_package(package) }
41
33
  all_package_options.group_by(&:handling_unit_code).map do |_handling_unit_code, options_group|
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'friendly_shipping/services/ups_freight/generate_location_hash'
4
+ require 'friendly_shipping/services/ups_freight/generate_document_options_hash'
5
+ require 'friendly_shipping/services/ups_freight/generate_email_options_hash'
6
+ require 'friendly_shipping/services/ups_freight/generate_pickup_options_hash'
7
+ require 'friendly_shipping/services/ups_freight/generate_delivery_options_hash'
8
+
9
+ module FriendlyShipping
10
+ module Services
11
+ class UpsFreight
12
+ class GenerateFreightShipRequestHash
13
+ class << self
14
+ def call(shipment:, options:)
15
+ {
16
+ FreightShipRequest: {
17
+ Shipment: {
18
+ ShipperNumber: options.shipper_number,
19
+ ShipFrom: GenerateLocationHash.call(location: shipment.origin),
20
+ ShipTo: GenerateLocationHash.call(location: shipment.destination),
21
+ PaymentInformation: payment_information(options),
22
+ Service: {
23
+ Code: options.shipping_method.service_code
24
+ },
25
+ Commodity: options.commodity_information_generator.call(shipment: shipment, options: options),
26
+ Documents: {
27
+ Image: options.document_options.map { |doc_opts| GenerateDocumentOptionsHash.call(document_options: doc_opts) }
28
+ },
29
+ ShipmentServiceOptions: shipment_service_options(options),
30
+ HandlingInstructions: options.handling_instructions,
31
+ PickupInstructions: options.pickup_instructions,
32
+ DeliveryInstructions: options.delivery_instructions,
33
+ PickupRequest: GeneratePickupRequestHash.call(pickup_request_options: options.pickup_request_options)
34
+ }.compact.merge(handling_units(shipment, options).reduce(&:merge).to_h)
35
+ }
36
+ }
37
+ end
38
+
39
+ private
40
+
41
+ def shipment_service_options(options)
42
+ email_options = options.email_options.map { |email_opts| GenerateEmailOptionsHash.call(email_options: email_opts) }.presence
43
+ pickup_options = options.pickup_options ? GeneratePickupOptionsHash.call(pickup_options: options.pickup_options) : nil
44
+ delivery_options = options.delivery_options ? GenerateDeliveryOptionsHash.call(delivery_options: options.delivery_options) : nil
45
+ [email_options, pickup_options, delivery_options].compact.presence
46
+ end
47
+
48
+ def handling_units(shipment, options)
49
+ all_package_options = shipment.packages.map { |package| options.options_for_package(package) }
50
+ all_package_options.group_by(&:handling_unit_code).map do |_handling_unit_code, options_group|
51
+ [options_group.first, options_group.length]
52
+ end.map { |package_options, quantity| handling_unit_hash(package_options, quantity) }
53
+ end
54
+
55
+ def handling_unit_hash(package_options, quantity)
56
+ {
57
+ package_options.handling_unit_tag => {
58
+ Quantity: quantity.to_s,
59
+ Type: {
60
+ Code: package_options.handling_unit_code,
61
+ Description: package_options.handling_unit_description
62
+ }
63
+ }
64
+ }
65
+ end
66
+
67
+ def payment_information(options)
68
+ payer_address = GenerateLocationHash.call(location: options.billing_address).
69
+ merge(ShipperNumber: options.shipper_number)
70
+ {
71
+ Payer: payer_address,
72
+ ShipmentBillingOption: {
73
+ Code: options.billing_code
74
+ }
75
+ }
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -17,8 +17,11 @@ module FriendlyShipping
17
17
  PostalCode: location.zip,
18
18
  CountryCode: location.country.code
19
19
  },
20
- AttentionName: location.name
21
- }
20
+ AttentionName: location.name,
21
+ Phone: {
22
+ Number: location.phone
23
+ }.compact.presence
24
+ }.compact
22
25
  end
23
26
 
24
27
  private
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FriendlyShipping
4
+ module Services
5
+ class UpsFreight
6
+ class GeneratePickupOptionsHash
7
+ def self.call(pickup_options:)
8
+ {
9
+ PickupOptions: {
10
+ LiftGateRequiredIndicator: pickup_options.lift_gate_required ? "" : nil,
11
+ WeekendPickupIndicator: pickup_options.weekend_pickup ? "" : nil,
12
+ InsidePickupIndicator: pickup_options.inside_pickup ? "" : nil,
13
+ HolidayPickupIndicator: pickup_options.holiday_pickup ? "" : nil,
14
+ LimitedAccessPickupIndicator: pickup_options.limited_access_pickup ? "" : nil
15
+ }.compact.presence
16
+ }.compact.presence
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FriendlyShipping
4
+ module Services
5
+ class UpsFreight
6
+ class GeneratePickupRequestHash
7
+ class << self
8
+ def call(pickup_request_options:)
9
+ return unless pickup_request_options
10
+
11
+ {
12
+ AdditionalComments: pickup_request_options.comments,
13
+ Requester: {
14
+ ThirdPartyRequester: pickup_request_options.third_party_requester ? '' : nil,
15
+ AttentionName: pickup_request_options.requester.name,
16
+ EMailAddress: pickup_request_options.requester_email,
17
+ Name: pickup_request_options.requester.company_name,
18
+ Phone: {
19
+ Number: pickup_request_options.requester.phone
20
+ }.compact
21
+ }.compact,
22
+ PickupDate: pickup_request_options.pickup_time_window.begin.strftime('%Y%m%d'),
23
+ EarliestTimeReady: pickup_request_options.pickup_time_window.begin.strftime('%H%M'),
24
+ LatestTimeReady: pickup_request_options.pickup_time_window.end.strftime('%H%M'),
25
+ }.compact
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FriendlyShipping
4
+ module Services
5
+ class UpsFreight
6
+ class LabelDeliveryOptions
7
+ attr_reader :holiday_delivery,
8
+ :inside_delivery,
9
+ :weekend_delivery,
10
+ :lift_gate_required,
11
+ :limited_access_delivery
12
+
13
+ def initialize(
14
+ holiday_delivery: nil,
15
+ inside_delivery: nil,
16
+ weekend_delivery: nil,
17
+ lift_gate_required: nil,
18
+ limited_access_delivery: nil
19
+ )
20
+ @holiday_delivery = holiday_delivery
21
+ @inside_delivery = inside_delivery
22
+ @weekend_delivery = weekend_delivery
23
+ @lift_gate_required = lift_gate_required
24
+ @limited_access_delivery = limited_access_delivery
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FriendlyShipping
4
+ module Services
5
+ class UpsFreight
6
+ class LabelDocumentOptions
7
+ attr_reader :format, :type, :length, :width, :thermal
8
+
9
+ DOCUMENT_TYPES = {
10
+ label: "30",
11
+ ups_bol: "20",
12
+ vics_bol: "21"
13
+ }.freeze
14
+
15
+ DOCUMENT_FORMATS = {
16
+ pdf: "01"
17
+ }.freeze
18
+
19
+ THERMAL_CODE = {
20
+ false => "01",
21
+ true => "02"
22
+ }.freeze
23
+
24
+ def initialize(
25
+ format: :pdf,
26
+ type: :label,
27
+ size: "4x6",
28
+ thermal: false,
29
+ labels_per_page: 1
30
+ )
31
+ @format = format
32
+ @type = type
33
+ @length, @width = size.split('x').sort
34
+ @thermal = thermal
35
+ @labels_per_page = labels_per_page
36
+ end
37
+
38
+ def format_code
39
+ DOCUMENT_FORMATS.fetch(format)
40
+ end
41
+
42
+ def document_type_code
43
+ DOCUMENT_TYPES.fetch(type)
44
+ end
45
+
46
+ def thermal_code
47
+ THERMAL_CODE.fetch(thermal)
48
+ end
49
+
50
+ def labels_per_page
51
+ @labels_per_page.to_s
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FriendlyShipping
4
+ module Services
5
+ class UpsFreight
6
+ class LabelEmailOptions
7
+ EMAIL_TYPES = {
8
+ ship_notification: '001',
9
+ delivery_notification: '002',
10
+ exception_notification: '003',
11
+ bol_labels: '004'
12
+ }.freeze
13
+
14
+ attr_reader :email_type,
15
+ :email,
16
+ :undeliverable_email,
17
+ :subject,
18
+ :body
19
+
20
+ def initialize(
21
+ email:,
22
+ email_type:,
23
+ undeliverable_email:,
24
+ subject: nil,
25
+ body: nil
26
+ )
27
+ @email = email
28
+ @email_type = email_type
29
+ @undeliverable_email = undeliverable_email
30
+ @subject = subject
31
+ @body = body
32
+ end
33
+
34
+ def email_type_code
35
+ EMAIL_TYPES.fetch(email_type)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end