easypost 3.5.1 → 5.2.0

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 (151) hide show
  1. checksums.yaml +4 -4
  2. data/.gitattributes +9 -0
  3. data/.github/CODEOWNERS +2 -0
  4. data/.github/ISSUE_TEMPLATE/bug_report.yml +81 -0
  5. data/.github/ISSUE_TEMPLATE/feature_request.yml +37 -0
  6. data/.github/PULL_REQUEST_TEMPLATE.md +22 -0
  7. data/.github/workflows/ci.yml +54 -5
  8. data/.gitignore +27 -17
  9. data/.gitmodules +3 -0
  10. data/CHANGELOG.md +295 -119
  11. data/Gemfile +2 -0
  12. data/Makefile +70 -0
  13. data/README.md +184 -72
  14. data/Rakefile +2 -1
  15. data/UPGRADE_GUIDE.md +181 -0
  16. data/VERSION +1 -1
  17. data/bin/easypost-irb +5 -3
  18. data/easypost.gemspec +27 -20
  19. data/lib/easypost/client.rb +179 -0
  20. data/lib/easypost/connection.rb +64 -0
  21. data/lib/easypost/constants.rb +15 -0
  22. data/lib/easypost/errors/api/api_error.rb +108 -0
  23. data/lib/easypost/errors/api/bad_request_error.rb +6 -0
  24. data/lib/easypost/errors/api/connection_error.rb +6 -0
  25. data/lib/easypost/errors/api/external_api_error.rb +18 -0
  26. data/lib/easypost/errors/api/forbidden_error.rb +6 -0
  27. data/lib/easypost/errors/api/gateway_timeout_error.rb +6 -0
  28. data/lib/easypost/errors/api/internal_server_error.rb +6 -0
  29. data/lib/easypost/errors/api/invalid_request_error.rb +6 -0
  30. data/lib/easypost/errors/api/method_not_allowed_error.rb +6 -0
  31. data/lib/easypost/errors/api/not_found_error.rb +6 -0
  32. data/lib/easypost/errors/api/payment_error.rb +6 -0
  33. data/lib/easypost/errors/api/proxy_error.rb +6 -0
  34. data/lib/easypost/errors/api/rate_limit_error.rb +6 -0
  35. data/lib/easypost/errors/api/redirect_error.rb +6 -0
  36. data/lib/easypost/errors/api/retry_error.rb +6 -0
  37. data/lib/easypost/errors/api/service_unavailable_error.rb +6 -0
  38. data/lib/easypost/errors/api/ssl_error.rb +6 -0
  39. data/lib/easypost/errors/api/timeout_error.rb +6 -0
  40. data/lib/easypost/errors/api/unauthorized_error.rb +6 -0
  41. data/lib/easypost/errors/api/unknown_api_error.rb +6 -0
  42. data/lib/easypost/errors/easy_post_error.rb +7 -0
  43. data/lib/easypost/errors/end_of_pagination_error.rb +7 -0
  44. data/lib/easypost/errors/filtering_error.rb +4 -0
  45. data/lib/easypost/errors/invalid_object_error.rb +4 -0
  46. data/lib/easypost/errors/invalid_parameter_error.rb +11 -0
  47. data/lib/easypost/errors/missing_parameter_error.rb +9 -0
  48. data/lib/easypost/errors/signature_verification_error.rb +4 -0
  49. data/lib/easypost/errors.rb +32 -0
  50. data/lib/easypost/hooks/request_context.rb +16 -0
  51. data/lib/easypost/hooks/response_context.rb +23 -0
  52. data/lib/easypost/hooks.rb +34 -0
  53. data/lib/easypost/http_client.rb +117 -0
  54. data/lib/easypost/internal_utilities.rb +66 -0
  55. data/lib/easypost/models/address.rb +5 -0
  56. data/lib/easypost/models/api_key.rb +5 -0
  57. data/lib/easypost/models/base.rb +58 -0
  58. data/lib/easypost/models/batch.rb +5 -0
  59. data/lib/easypost/models/brand.rb +5 -0
  60. data/lib/easypost/models/carbon_offset.rb +5 -0
  61. data/lib/easypost/models/carrier_account.rb +5 -0
  62. data/lib/easypost/models/carrier_type.rb +5 -0
  63. data/lib/easypost/models/customs_info.rb +5 -0
  64. data/lib/easypost/models/customs_item.rb +5 -0
  65. data/lib/easypost/models/end_shipper.rb +5 -0
  66. data/lib/easypost/models/error.rb +21 -0
  67. data/lib/easypost/models/event.rb +5 -0
  68. data/lib/easypost/models/insurance.rb +6 -0
  69. data/lib/easypost/models/order.rb +9 -0
  70. data/lib/easypost/models/parcel.rb +5 -0
  71. data/lib/easypost/models/payload.rb +5 -0
  72. data/lib/easypost/models/payment_method.rb +5 -0
  73. data/lib/easypost/models/pickup.rb +9 -0
  74. data/lib/easypost/models/pickup_rate.rb +5 -0
  75. data/lib/easypost/models/postage_label.rb +5 -0
  76. data/lib/easypost/models/rate.rb +5 -0
  77. data/lib/easypost/models/referral.rb +5 -0
  78. data/lib/easypost/models/refund.rb +5 -0
  79. data/lib/easypost/models/report.rb +5 -0
  80. data/lib/easypost/models/scan_form.rb +6 -0
  81. data/lib/easypost/models/shipment.rb +10 -0
  82. data/lib/easypost/models/tax_identifier.rb +6 -0
  83. data/lib/easypost/models/tracker.rb +5 -0
  84. data/lib/easypost/models/user.rb +5 -0
  85. data/lib/easypost/models/webhook.rb +6 -0
  86. data/lib/easypost/models.rb +36 -0
  87. data/lib/easypost/services/address.rb +50 -0
  88. data/lib/easypost/services/api_key.rb +8 -0
  89. data/lib/easypost/services/base.rb +27 -0
  90. data/lib/easypost/services/batch.rb +53 -0
  91. data/lib/easypost/services/beta_rate.rb +12 -0
  92. data/lib/easypost/services/beta_referral_customer.rb +40 -0
  93. data/lib/easypost/services/billing.rb +75 -0
  94. data/lib/easypost/services/carrier_account.rb +44 -0
  95. data/lib/easypost/services/carrier_metadata.rb +22 -0
  96. data/lib/easypost/services/carrier_type.rb +10 -0
  97. data/lib/easypost/services/customs_info.rb +17 -0
  98. data/lib/easypost/services/customs_item.rb +15 -0
  99. data/lib/easypost/services/end_shipper.rb +31 -0
  100. data/lib/easypost/services/event.rb +32 -0
  101. data/lib/easypost/services/insurance.rb +26 -0
  102. data/lib/easypost/services/order.rb +30 -0
  103. data/lib/easypost/services/parcel.rb +16 -0
  104. data/lib/easypost/services/pickup.rb +40 -0
  105. data/lib/easypost/services/rate.rb +8 -0
  106. data/lib/easypost/services/referral_customer.rb +103 -0
  107. data/lib/easypost/services/refund.rb +26 -0
  108. data/lib/easypost/services/report.rb +42 -0
  109. data/lib/easypost/services/scan_form.rb +25 -0
  110. data/lib/easypost/services/shipment.rb +106 -0
  111. data/lib/easypost/services/tracker.rb +38 -0
  112. data/lib/easypost/services/user.rb +66 -0
  113. data/lib/easypost/services/webhook.rb +34 -0
  114. data/lib/easypost/services.rb +33 -0
  115. data/lib/easypost/util.rb +160 -116
  116. data/lib/easypost/utilities/constants.rb +5 -0
  117. data/lib/easypost/utilities/json.rb +23 -0
  118. data/lib/easypost/utilities/static_mapper.rb +73 -0
  119. data/lib/easypost/utilities/system.rb +36 -0
  120. data/lib/easypost/version.rb +3 -1
  121. data/lib/easypost.rb +20 -143
  122. metadata +249 -46
  123. data/lib/easypost/address.rb +0 -58
  124. data/lib/easypost/api_key.rb +0 -2
  125. data/lib/easypost/batch.rb +0 -49
  126. data/lib/easypost/brand.rb +0 -9
  127. data/lib/easypost/carrier_account.rb +0 -5
  128. data/lib/easypost/carrier_type.rb +0 -2
  129. data/lib/easypost/customs_info.rb +0 -5
  130. data/lib/easypost/customs_item.rb +0 -5
  131. data/lib/easypost/error.rb +0 -31
  132. data/lib/easypost/event.rb +0 -7
  133. data/lib/easypost/insurance.rb +0 -2
  134. data/lib/easypost/object.rb +0 -151
  135. data/lib/easypost/order.rb +0 -28
  136. data/lib/easypost/parcel.rb +0 -2
  137. data/lib/easypost/pickup.rb +0 -26
  138. data/lib/easypost/pickup_rate.rb +0 -3
  139. data/lib/easypost/postage_label.rb +0 -2
  140. data/lib/easypost/print_job.rb +0 -2
  141. data/lib/easypost/printer.rb +0 -24
  142. data/lib/easypost/rate.rb +0 -2
  143. data/lib/easypost/refund.rb +0 -2
  144. data/lib/easypost/report.rb +0 -29
  145. data/lib/easypost/resource.rb +0 -75
  146. data/lib/easypost/scan_form.rb +0 -6
  147. data/lib/easypost/shipment.rb +0 -129
  148. data/lib/easypost/tax_identifier.rb +0 -2
  149. data/lib/easypost/tracker.rb +0 -7
  150. data/lib/easypost/user.rb +0 -56
  151. data/lib/easypost/webhook.rb +0 -29
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ class EasyPost::Services::ScanForm < EasyPost::Services::Service
4
+ MODEL_CLASS = EasyPost::Models::ScanForm
5
+
6
+ # Create a ScanForm.
7
+ def create(params = {})
8
+ @client.make_request(:post, 'scan_forms', MODEL_CLASS, params)
9
+ end
10
+
11
+ # Retrieve a ScanForm.
12
+ def retrieve(id)
13
+ @client.make_request(:get, "scan_forms/#{id}", MODEL_CLASS)
14
+ end
15
+
16
+ # Retrieve a list of ScanForms
17
+ def all(params = {})
18
+ @client.make_request(:get, 'scan_forms', MODEL_CLASS, params)
19
+ end
20
+
21
+ # Get the next page of ScanForms.
22
+ def get_next_page(collection, page_size = nil)
23
+ get_next_page_helper(collection, collection.scan_forms, 'scan_forms', MODEL_CLASS, page_size)
24
+ end
25
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+
5
+ class EasyPost::Services::Shipment < EasyPost::Services::Service
6
+ MODEL_CLASS = EasyPost::Models::Shipment
7
+
8
+ # Create a Shipment.
9
+ def create(params = {}, with_carbon_offset = false)
10
+ wrapped_params = {
11
+ shipment: params,
12
+ carbon_offset: with_carbon_offset,
13
+ }
14
+
15
+ @client.make_request(:post, 'shipments', MODEL_CLASS, wrapped_params)
16
+ end
17
+
18
+ # Retrieve a Shipment.
19
+ def retrieve(id)
20
+ @client.make_request(:get, "shipments/#{id}", MODEL_CLASS)
21
+ end
22
+
23
+ # Retrieve a list of Shipments
24
+ def all(params = {})
25
+ response = @client.make_request(:get, 'shipments', MODEL_CLASS, params)
26
+ response.define_singleton_method(:purchased) { params[:purchased] }
27
+ response.define_singleton_method(:include_children) { params[:include_children] }
28
+ response
29
+ end
30
+
31
+ # Get the next page of shipments.
32
+ def get_next_page(collection, page_size = nil)
33
+ get_next_page_helper(collection, collection.shipments, 'shipments', MODEL_CLASS, page_size)
34
+ end
35
+
36
+ # Regenerate the rates of a Shipment.
37
+ def regenerate_rates(id, with_carbon_offset = false)
38
+ params = { carbon_offset: with_carbon_offset }
39
+
40
+ @client.make_request(:post, "shipments/#{id}/rerate", MODEL_CLASS, params)
41
+ end
42
+
43
+ # Get the SmartRates of a Shipment.
44
+ def get_smart_rates(id)
45
+ @client.make_request(:get, "shipments/#{id}/smartrate", MODEL_CLASS).result || []
46
+ end
47
+
48
+ # Buy a Shipment.
49
+ def buy(id, params = {}, with_carbon_offset = false, end_shipper_id = nil)
50
+ if params.instance_of?(EasyPost::Models::Rate)
51
+ params = { rate: params.clone }
52
+ end
53
+
54
+ params[:carbon_offset] = params[:with_carbon_offset] || with_carbon_offset
55
+ params.delete(:with_carbon_offset)
56
+
57
+ params[:end_shipper_id] = end_shipper_id if end_shipper_id
58
+
59
+ @client.make_request(:post, "shipments/#{id}/buy", MODEL_CLASS, params)
60
+ end
61
+
62
+ # Insure a Shipment.
63
+ def insure(id, params = {})
64
+ params = { amount: params } if params.is_a?(Integer) || params.is_a?(Float)
65
+
66
+ @client.make_request(:post, "shipments/#{id}/insure", MODEL_CLASS, params)
67
+ end
68
+
69
+ # Refund a Shipment.
70
+ def refund(id, params = {})
71
+ @client.make_request(:post, "shipments/#{id}/refund", MODEL_CLASS, params)
72
+ end
73
+
74
+ # Convert the label format of a Shipment.
75
+ def label(id, params = {})
76
+ params = { file_format: params } if params.is_a?(String)
77
+
78
+ @client.make_request(:get, "shipments/#{id}/label", MODEL_CLASS, params)
79
+ end
80
+
81
+ # Get the lowest SmartRate of a Shipment.
82
+ def lowest_smart_rate(id, delivery_days, delivery_accuracy)
83
+ smart_rates = get_smart_rates(id)
84
+ EasyPost::Util.get_lowest_smart_rate(smart_rates, delivery_days, delivery_accuracy)
85
+ end
86
+
87
+ # Generate a form for a Shipment.
88
+ def generate_form(id, form_type, form_options = {})
89
+ params = {}
90
+ params[:type] = form_type
91
+ merged_params = params.merge(form_options)
92
+ wrapped_params = {
93
+ form: merged_params,
94
+ }
95
+
96
+ @client.make_request(:post, "shipments/#{id}/forms", MODEL_CLASS, wrapped_params)
97
+ end
98
+
99
+ # Retrieves the estimated delivery date of each Rate via SmartRate.
100
+ def retrieve_estimated_delivery_date(id, planned_ship_date)
101
+ url = "shipments/#{id}/smartrate/delivery_date"
102
+ params = { planned_ship_date: planned_ship_date }
103
+
104
+ @client.make_request(:get, url, MODEL_CLASS, params).rates
105
+ end
106
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ class EasyPost::Services::Tracker < EasyPost::Services::Service
4
+ MODEL_CLASS = EasyPost::Models::Tracker
5
+
6
+ # Create a Tracker
7
+ def create(params = {})
8
+ wrapped_params = { tracker: params }
9
+
10
+ @client.make_request(:post, 'trackers', MODEL_CLASS, wrapped_params)
11
+ end
12
+
13
+ # Retrieve a Tracker
14
+ def retrieve(id)
15
+ @client.make_request(:get, "trackers/#{id}", MODEL_CLASS)
16
+ end
17
+
18
+ # Retrieve a list of Trackers
19
+ def all(params)
20
+ response = @client.make_request(:get, 'trackers', MODEL_CLASS, params)
21
+ response.define_singleton_method(:tracking_code) { params[:tracking_code] }
22
+ response.define_singleton_method(:carrier) { params[:carrier] }
23
+ response
24
+ end
25
+
26
+ # Create multiple Tracker objects in bulk.
27
+ def create_list(params = {})
28
+ wrapped_params = { 'trackers' => params }
29
+
30
+ @client.make_request(:post, 'trackers/create_list', MODEL_CLASS, wrapped_params)
31
+ true # This endpoint does not return a response so we return true here instead
32
+ end
33
+
34
+ # Get the next page of trackers.
35
+ def get_next_page(collection, page_size = nil)
36
+ get_next_page_helper(collection, collection.trackers, 'trackers', MODEL_CLASS, page_size)
37
+ end
38
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ class EasyPost::Services::User < EasyPost::Services::Service
4
+ MODEL_CLASS = EasyPost::Models::User
5
+
6
+ # Create a child User.
7
+ def create(params = {})
8
+ @client.make_request(:post, 'users', MODEL_CLASS, params)
9
+ end
10
+
11
+ # Retrieve a user
12
+ def retrieve(id)
13
+ @client.make_request(:get, "users/#{id}", MODEL_CLASS)
14
+ end
15
+
16
+ # Retrieve the authenticated User.
17
+ def retrieve_me
18
+ @client.make_request(:get, 'users', MODEL_CLASS)
19
+ end
20
+
21
+ # Update a User
22
+ def update(id, params = {})
23
+ @client.make_request(:put, "users/#{id}", MODEL_CLASS, params)
24
+ end
25
+
26
+ # Delete a User
27
+ def delete(id)
28
+ @client.make_request(:delete, "users/#{id}")
29
+
30
+ # Return true if succeeds, an error will be thrown if it fails
31
+ true
32
+ end
33
+
34
+ # Retrieve a list of all ApiKey objects.
35
+ def all_api_keys
36
+ @client.make_request(:get, 'api_keys', EasyPost::Models::ApiKey)
37
+ end
38
+
39
+ # Retrieve a list of ApiKey objects (works for the authenticated user or a child user).
40
+ def api_keys(id)
41
+ api_keys = all_api_keys
42
+
43
+ if api_keys.id == id
44
+ # This function was called on the authenticated user
45
+ my_api_keys = api_keys.keys
46
+ else
47
+ # This function was called on a child user (authenticated as parent, only return this child user's details).
48
+ my_api_keys = []
49
+ api_keys.children.each do |child|
50
+ if child.id == id
51
+ my_api_keys = child.keys
52
+ break
53
+ end
54
+ end
55
+ end
56
+
57
+ my_api_keys
58
+ end
59
+
60
+ # Update the Brand of a User.
61
+ def update_brand(id, params = {})
62
+ wrapped_params = { brand: params }
63
+
64
+ @client.make_request(:get, "users/#{id}/brand", EasyPost::Models::Brand, wrapped_params)
65
+ end
66
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ class EasyPost::Services::Webhook < EasyPost::Services::Service
4
+ MODEL_CLASS = EasyPost::Models::Webhook
5
+
6
+ # Create a Webhook.
7
+ def create(params = {})
8
+ wrapped_params = { webhook: params }
9
+ @client.make_request(:post, 'webhooks', MODEL_CLASS, wrapped_params)
10
+ end
11
+
12
+ # Retrieve a Webhook
13
+ def retrieve(id)
14
+ @client.make_request(:get, "webhooks/#{id}", MODEL_CLASS)
15
+ end
16
+
17
+ # Retrieve a list of Webhooks
18
+ def all(params = {})
19
+ @client.make_request(:get, 'webhooks', MODEL_CLASS, params)
20
+ end
21
+
22
+ # Update a Webhook.
23
+ def update(id, params = {})
24
+ @client.make_request(:patch, "webhooks/#{id}", MODEL_CLASS, params)
25
+ end
26
+
27
+ # Delete a Webhook.
28
+ def delete(id)
29
+ @client.make_request(:delete, "webhooks/#{id}")
30
+
31
+ # Return true if succeeds, an error will be thrown if it fails
32
+ true
33
+ end
34
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EasyPost::Services
4
+ end
5
+
6
+ require_relative 'models'
7
+ require_relative 'services/base' # Must be imported first before the rest of child services
8
+ require_relative 'services/address'
9
+ require_relative 'services/api_key'
10
+ require_relative 'services/batch'
11
+ require_relative 'services/beta_rate'
12
+ require_relative 'services/beta_referral_customer'
13
+ require_relative 'services/billing'
14
+ require_relative 'services/carrier_account'
15
+ require_relative 'services/carrier_metadata'
16
+ require_relative 'services/carrier_type'
17
+ require_relative 'services/customs_info'
18
+ require_relative 'services/customs_item'
19
+ require_relative 'services/end_shipper'
20
+ require_relative 'services/event'
21
+ require_relative 'services/insurance'
22
+ require_relative 'services/order'
23
+ require_relative 'services/parcel'
24
+ require_relative 'services/pickup'
25
+ require_relative 'services/rate'
26
+ require_relative 'services/referral_customer'
27
+ require_relative 'services/refund'
28
+ require_relative 'services/report'
29
+ require_relative 'services/scan_form'
30
+ require_relative 'services/shipment'
31
+ require_relative 'services/tracker'
32
+ require_relative 'services/user'
33
+ require_relative 'services/webhook'
data/lib/easypost/util.rb CHANGED
@@ -1,132 +1,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'easypost/constants'
4
+
5
+ # Client Library helper functions
1
6
  module EasyPost::Util
2
- attr_accessor :os_name, :os_version, :os_arch
3
-
4
- def self.os_name
5
- case RUBY_PLATFORM
6
- when /linux/i
7
- 'Linux'
8
- when /darwin/i
9
- 'Darwin'
10
- when /cygwin|mswin|mingw|bccwin|wince|emx/i
11
- 'Windows'
12
- else
13
- 'Unknown'
7
+ # Gets the lowest rate of an EasyPost object such as a Shipment, Order, or Pickup.
8
+ # You can exclude by having `'!'` as the first element of your optional filter lists
9
+ def self.get_lowest_object_rate(easypost_object, carriers = [], services = [], rates_key = 'rates')
10
+ lowest_rate = nil
11
+
12
+ carriers = EasyPost::InternalUtilities.normalize_string_list(carriers)
13
+ negative_carriers = []
14
+ carriers_copy = carriers.clone
15
+ carriers_copy.each do |carrier|
16
+ if carrier[0, 1] == '!'
17
+ negative_carriers << carrier[1..]
18
+ carriers.delete(carrier)
19
+ end
14
20
  end
15
- end
16
21
 
17
- def self.os_version
18
- Gem::Platform.local.version
19
- end
22
+ services = EasyPost::InternalUtilities.normalize_string_list(services)
23
+ negative_services = []
24
+ services_copy = services.clone
25
+ services_copy.each do |service|
26
+ if service[0, 1] == '!'
27
+ negative_services << service[1..]
28
+ services.delete(service)
29
+ end
30
+ end
31
+
32
+ easypost_object.send(rates_key).each do |rate|
33
+ rate_carrier = rate.carrier.downcase
34
+ if carriers.size.positive? && !carriers.include?(rate_carrier)
35
+ next
36
+ end
37
+ if negative_carriers.size.positive? && negative_carriers.include?(rate_carrier)
38
+ next
39
+ end
20
40
 
21
- def self.os_arch
22
- Gem::Platform.local.cpu
41
+ rate_service = rate.service.downcase
42
+ if services.size.positive? && !services.include?(rate_service)
43
+ next
44
+ end
45
+ if negative_services.size.positive? && negative_services.include?(rate_service)
46
+ next
47
+ end
48
+
49
+ if lowest_rate.nil? || rate.rate.to_f < lowest_rate.rate.to_f
50
+ lowest_rate = rate
51
+ end
52
+ end
53
+
54
+ if lowest_rate.nil?
55
+ raise EasyPost::Errors::FilteringError.new(EasyPost::Constants::NO_MATCHING_RATES)
56
+ end
57
+
58
+ lowest_rate
23
59
  end
24
60
 
25
- def self.objects_to_ids(obj)
26
- case obj
27
- when EasyPost::Resource
28
- return {:id => obj.id}
29
- when Hash
30
- result = {}
31
- obj.each { |k, v| result[k] = objects_to_ids(v) unless v.nil? }
32
- return result
33
- when Array
34
- return obj.map { |v| objects_to_ids(v) }
35
- else
36
- return obj
61
+ # Gets the lowest stateless rate.
62
+ # You can exclude by having `'!'` as the first element of your optional filter lists
63
+ def self.get_lowest_stateless_rate(stateless_rates, carriers = [], services = [])
64
+ lowest_rate = nil
65
+
66
+ carriers = EasyPost::InternalUtilities.normalize_string_list(carriers)
67
+ negative_carriers = []
68
+ carriers_copy = carriers.clone
69
+ carriers_copy.each do |carrier|
70
+ if carrier[0, 1] == '!'
71
+ negative_carriers << carrier[1..]
72
+ carriers.delete(carrier)
73
+ end
74
+ end
75
+
76
+ services = EasyPost::InternalUtilities.normalize_string_list(services)
77
+ negative_services = []
78
+ services_copy = services.clone
79
+ services_copy.each do |service|
80
+ if service[0, 1] == '!'
81
+ negative_services << service[1..]
82
+ services.delete(service)
83
+ end
84
+ end
85
+
86
+ stateless_rates.each do |rate|
87
+ rate_carrier = rate.carrier.downcase
88
+ if carriers.size.positive? && !carriers.include?(rate_carrier)
89
+ next
90
+ end
91
+ if negative_carriers.size.positive? && negative_carriers.include?(rate_carrier)
92
+ next
93
+ end
94
+
95
+ rate_service = rate.service.downcase
96
+ if services.size.positive? && !services.include?(rate_service)
97
+ next
98
+ end
99
+ if negative_services.size.positive? && negative_services.include?(rate_service)
100
+ next
101
+ end
102
+
103
+ if lowest_rate.nil? || rate.rate.to_f < lowest_rate.rate.to_f
104
+ lowest_rate = rate
105
+ end
106
+ end
107
+
108
+ if lowest_rate.nil?
109
+ raise EasyPost::Errors::FilteringError.new(EasyPost::Constants::NO_MATCHING_RATES)
37
110
  end
111
+
112
+ lowest_rate
38
113
  end
39
114
 
40
- def self.normalize_string_list(lst)
41
- lst = lst.is_a?(String) ? lst.split(',') : Array(lst)
42
- lst.map(&:to_s).map(&:downcase).map(&:strip)
115
+ # Converts a raw webhook event into an EasyPost object.
116
+ def self.receive_event(raw_input)
117
+ EasyPost::InternalUtilities::Json.convert_json_to_object(JSON.parse(raw_input), EasyPost::Models::EasyPostObject)
43
118
  end
44
119
 
45
- def self.convert_to_easypost_object(response, api_key, parent=nil, name=nil)
46
- types = {
47
- 'Address' => EasyPost::Address,
48
- 'Batch' => EasyPost::Batch,
49
- 'CarrierAccount' => EasyPost::CarrierAccount,
50
- 'CustomsInfo' => EasyPost::CustomsInfo,
51
- 'CustomsItem' => EasyPost::CustomsItem,
52
- 'Event' => EasyPost::Event,
53
- 'Insurance' => EasyPost::Insurance,
54
- 'Order' => EasyPost::Order,
55
- 'Parcel' => EasyPost::Parcel,
56
- 'PaymentLogReport' => EasyPost::Report,
57
- 'Pickup' => EasyPost::Pickup,
58
- 'PickupRate' => EasyPost::PickupRate,
59
- 'PostageLabel' => EasyPost::PostageLabel,
60
- 'Printer' => EasyPost::Printer,
61
- 'PrintJob' => EasyPost::PrintJob,
62
- 'Rate' => EasyPost::Rate,
63
- 'Refund' => EasyPost::Refund,
64
- 'RefundReport' => EasyPost::Report,
65
- 'Report' => EasyPost::Report,
66
- 'ScanForm' => EasyPost::ScanForm,
67
- 'Shipment' => EasyPost::Shipment,
68
- 'TaxIdentifier' => EasyPost::TaxIdentifier,
69
- 'ShipmentInvoiceReport' => EasyPost::Report,
70
- 'ShipmentReport' => EasyPost::Report,
71
- 'Tracker' => EasyPost::Tracker,
72
- 'TrackerReport' => EasyPost::Report,
73
- 'User' => EasyPost::User,
74
- 'Webhook' => EasyPost::Webhook
75
- }
76
-
77
- prefixes = {
78
- 'adr' => EasyPost::Address,
79
- 'batch' => EasyPost::Batch,
80
- 'ca' => EasyPost::CarrierAccount,
81
- 'cstinfo' => EasyPost::CustomsInfo,
82
- 'cstitem' => EasyPost::CustomsItem,
83
- 'evt' => EasyPost::Event,
84
- 'hook' => EasyPost::Webhook,
85
- 'ins' => EasyPost::Insurance,
86
- 'order' => EasyPost::Order,
87
- 'pickup' => EasyPost::Pickup,
88
- 'pickuprate' => EasyPost::PickupRate,
89
- 'pl' => EasyPost::PostageLabel,
90
- 'plrep' => EasyPost::Report,
91
- 'prcl' => EasyPost::Parcel,
92
- 'printer' => EasyPost::Printer,
93
- 'printjob' => EasyPost::PrintJob,
94
- 'rate' => EasyPost::Rate,
95
- 'refrep' => EasyPost::Report,
96
- 'rfnd' => EasyPost::Refund,
97
- 'sf' => EasyPost::ScanForm,
98
- 'shp' => EasyPost::Shipment,
99
- 'shpinvrep' => EasyPost::Report,
100
- 'shprep' => EasyPost::Report,
101
- 'trk' => EasyPost::Tracker,
102
- 'trkrep' => EasyPost::Report,
103
- 'user' => EasyPost::User
104
- }
105
-
106
- case response
107
- when Array
108
- return response.map { |i| convert_to_easypost_object(i, api_key, parent) }
109
- when Hash
110
- if cls_name = response[:object]
111
- cls = types[cls_name]
112
- elsif response[:id]
113
- if response[:id].index('_').nil?
114
- cls = EasyPost::EasyPostObject
115
- elsif cls_prefix = response[:id][0..response[:id].index('_')]
116
- cls = prefixes[cls_prefix[0..-2]]
117
- end
118
- elsif response['id']
119
- if response['id'].index('_').nil?
120
- cls = EasyPost::EasyPostObject
121
- elsif cls_prefix = response['id'][0..response['id'].index('_')]
122
- cls = prefixes[cls_prefix[0..-2]]
123
- end
120
+ # Get the lowest SmartRate from a list of SmartRate.
121
+ def self.get_lowest_smart_rate(smart_rates, delivery_days, delivery_accuracy)
122
+ valid_delivery_accuracy_values = Set[
123
+ 'percentile_50',
124
+ 'percentile_75',
125
+ 'percentile_85',
126
+ 'percentile_90',
127
+ 'percentile_95',
128
+ 'percentile_97',
129
+ 'percentile_99',
130
+ ]
131
+ lowest_smart_rate = nil
132
+
133
+ unless valid_delivery_accuracy_values.include?(delivery_accuracy.downcase)
134
+ raise EasyPost::Errors::InvalidParameterError.new(
135
+ 'delivery_accuracy',
136
+ "Must be one of: #{valid_delivery_accuracy_values}",
137
+ )
138
+ end
139
+
140
+ smart_rates.each do |rate|
141
+ next if rate['time_in_transit'][delivery_accuracy] > delivery_days.to_i
142
+
143
+ if lowest_smart_rate.nil? || rate['rate'].to_f < lowest_smart_rate['rate'].to_f
144
+ lowest_smart_rate = rate
124
145
  end
146
+ end
147
+
148
+ if lowest_smart_rate.nil?
149
+ raise EasyPost::Errors::FilteringError.new(EasyPost::Constants::NO_MATCHING_RATES)
150
+ end
151
+
152
+ lowest_smart_rate
153
+ end
154
+
155
+ # Validate a webhook by comparing the HMAC signature header sent from EasyPost to your shared secret.
156
+ # If the signatures do not match, an error will be raised signifying the webhook either did not originate
157
+ # from EasyPost or the secrets do not match. If the signatures do match, the `event_body` will be returned
158
+ # as JSON.
159
+ def self.validate_webhook(event_body, headers, webhook_secret)
160
+ easypost_hmac_signature = headers['X-Hmac-Signature']
161
+
162
+ if easypost_hmac_signature.nil?
163
+ raise EasyPost::Errors::SignatureVerificationError.new(EasyPost::Constants::WEBHOOK_MISSING_SIGNATURE)
164
+ end
165
+
166
+ encoded_webhook_secret = webhook_secret.unicode_normalize(:nfkd).encode('utf-8')
125
167
 
126
- cls ||= EasyPost::EasyPostObject
127
- return cls.construct_from(response, api_key, parent, name)
128
- else
129
- return response
168
+ expected_signature = OpenSSL::HMAC.hexdigest('sha256', encoded_webhook_secret, event_body)
169
+ digest = "hmac-sha256-hex=#{expected_signature}"
170
+ unless digest == easypost_hmac_signature
171
+ raise EasyPost::Errors::SignatureVerificationError.new(EasyPost::Constants::WEBHOOK_SIGNATURE_MISMATCH)
130
172
  end
173
+
174
+ JSON.parse(event_body)
131
175
  end
132
176
  end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EasyPost::InternalUtilities::Constants
4
+ API_VERSION = 'v2'
5
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EasyPost::InternalUtilities::Json
4
+ def self.convert_json_to_object(data, cls = EasyPost::Models::EasyPostObject)
5
+ data = JSON.parse(data) if data.is_a?(String) # Parse JSON to a Hash or Array if it's a string
6
+ if data.is_a?(Array)
7
+ # Deserialize array data into an array of objects
8
+ data.map { |i| convert_json_to_object(i, cls) }
9
+ elsif data.is_a?(Hash)
10
+ # Deserialize hash data into a new object instance
11
+ cls.new(data)
12
+ else
13
+ # data is neither a Hash nor Array (but somehow was parsed as JSON? This should never happen)
14
+ data
15
+ end
16
+ rescue JSON::ParserError
17
+ data # Not JSON, return the original data (used mostly when dealing with final values like strings, booleans, etc.)
18
+ end
19
+
20
+ def self.http_response_is_json?(response)
21
+ response['Content-Type'] ? response['Content-Type'].start_with?('application/json') : false
22
+ end
23
+ end