friendly_shipping 0.5.1 → 0.6.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop-relaxed.yml +3 -4
  3. data/.rubocop.yml +9 -0
  4. data/CHANGELOG.md +28 -1
  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 +8 -0
  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 +6 -2
  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 +5 -1
  49. data/lib/friendly_shipping/services/usps/parse_xml_response.rb +15 -5
  50. data/lib/friendly_shipping/services/usps/rate_estimate_package_options.rb +4 -1
  51. data/lib/friendly_shipping/services/usps/serialize_rate_request.rb +9 -3
  52. data/lib/friendly_shipping/services/usps/shipping_methods.rb +1 -2
  53. data/lib/friendly_shipping/version.rb +1 -1
  54. metadata +75 -40
  55. data/lib/friendly_shipping/services/ups_freight/generate_ups_security_hash.rb +0 -23
  56. data/lib/friendly_shipping/services/ups_freight/parse_json_response.rb +0 -38
@@ -8,19 +8,18 @@ module FriendlyShipping
8
8
  SUCCESSFUL_RESPONSE_STATUS_CODE = '1'
9
9
 
10
10
  class << self
11
- def call(response_body, expected_root_tag)
12
- xml = Nokogiri.XML(response_body, &:strict)
11
+ def call(request:, response:, expected_root_tag:)
12
+ xml = Nokogiri.XML(response.body, &:strict)
13
13
 
14
14
  if xml.root.nil? || xml.root.name != expected_root_tag
15
- Failure('Invalid document')
16
- end
17
- if request_successful?(xml)
15
+ wrap_failure('Invalid document', request, response)
16
+ elsif request_successful?(xml)
18
17
  Success(xml)
19
18
  else
20
- Failure(error_message(xml))
19
+ wrap_failure(error_message(xml), request, response)
21
20
  end
22
21
  rescue Nokogiri::XML::SyntaxError => e
23
- Failure(e)
22
+ wrap_failure(e, request, response)
24
23
  end
25
24
 
26
25
  private
@@ -34,6 +33,16 @@ module FriendlyShipping
34
33
  desc = xml.root.at_xpath('Response/Error/ErrorDescription')&.text
35
34
  [status, desc].compact.join(": ").presence || 'UPS could not process the request.'
36
35
  end
36
+
37
+ def wrap_failure(failure, request, response)
38
+ Failure(
39
+ FriendlyShipping::ApiFailure.new(
40
+ failure,
41
+ original_request: request,
42
+ original_response: response
43
+ )
44
+ )
45
+ end
37
46
  end
38
47
  end
39
48
  end
@@ -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