easypost 4.8.1 → 5.1.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 (146) hide show
  1. checksums.yaml +4 -4
  2. data/.gitattributes +5 -0
  3. data/.github/workflows/ci.yml +34 -5
  4. data/.gitignore +27 -20
  5. data/CHANGELOG.md +56 -0
  6. data/Makefile +30 -11
  7. data/README.md +111 -45
  8. data/UPGRADE_GUIDE.md +119 -0
  9. data/VERSION +1 -1
  10. data/easypost.gemspec +14 -10
  11. data/lib/easypost/client.rb +178 -0
  12. data/lib/easypost/connection.rb +2 -4
  13. data/lib/easypost/constants.rb +15 -0
  14. data/lib/easypost/errors/api/api_error.rb +108 -0
  15. data/lib/easypost/errors/api/bad_request_error.rb +6 -0
  16. data/lib/easypost/errors/api/connection_error.rb +6 -0
  17. data/lib/easypost/errors/api/external_api_error.rb +18 -0
  18. data/lib/easypost/errors/api/forbidden_error.rb +6 -0
  19. data/lib/easypost/errors/api/gateway_timeout_error.rb +6 -0
  20. data/lib/easypost/errors/api/internal_server_error.rb +6 -0
  21. data/lib/easypost/errors/api/invalid_request_error.rb +6 -0
  22. data/lib/easypost/errors/api/method_not_allowed_error.rb +6 -0
  23. data/lib/easypost/errors/api/not_found_error.rb +6 -0
  24. data/lib/easypost/errors/api/payment_error.rb +6 -0
  25. data/lib/easypost/errors/api/proxy_error.rb +6 -0
  26. data/lib/easypost/errors/api/rate_limit_error.rb +6 -0
  27. data/lib/easypost/errors/api/redirect_error.rb +6 -0
  28. data/lib/easypost/errors/api/retry_error.rb +6 -0
  29. data/lib/easypost/errors/api/service_unavailable_error.rb +6 -0
  30. data/lib/easypost/errors/api/ssl_error.rb +6 -0
  31. data/lib/easypost/errors/api/timeout_error.rb +6 -0
  32. data/lib/easypost/errors/api/unauthorized_error.rb +6 -0
  33. data/lib/easypost/errors/api/unknown_api_error.rb +6 -0
  34. data/lib/easypost/errors/easy_post_error.rb +7 -0
  35. data/lib/easypost/errors/end_of_pagination_error.rb +7 -0
  36. data/lib/easypost/errors/filtering_error.rb +4 -0
  37. data/lib/easypost/errors/invalid_object_error.rb +4 -0
  38. data/lib/easypost/errors/invalid_parameter_error.rb +11 -0
  39. data/lib/easypost/errors/missing_parameter_error.rb +9 -0
  40. data/lib/easypost/errors/signature_verification_error.rb +4 -0
  41. data/lib/easypost/errors.rb +32 -0
  42. data/lib/easypost/hooks/request_context.rb +16 -0
  43. data/lib/easypost/hooks/response_context.rb +23 -0
  44. data/lib/easypost/hooks.rb +34 -0
  45. data/lib/easypost/http_client.rb +117 -0
  46. data/lib/easypost/internal_utilities.rb +66 -0
  47. data/lib/easypost/models/address.rb +5 -0
  48. data/lib/easypost/models/api_key.rb +5 -0
  49. data/lib/easypost/models/base.rb +58 -0
  50. data/lib/easypost/models/batch.rb +5 -0
  51. data/lib/easypost/models/brand.rb +5 -0
  52. data/lib/easypost/{carbon_offset.rb → models/carbon_offset.rb} +1 -1
  53. data/lib/easypost/models/carrier_account.rb +5 -0
  54. data/lib/easypost/models/customs_info.rb +5 -0
  55. data/lib/easypost/models/customs_item.rb +5 -0
  56. data/lib/easypost/models/end_shipper.rb +5 -0
  57. data/lib/easypost/models/error.rb +21 -0
  58. data/lib/easypost/models/event.rb +5 -0
  59. data/lib/easypost/{insurance.rb → models/insurance.rb} +1 -1
  60. data/lib/easypost/models/order.rb +9 -0
  61. data/lib/easypost/models/parcel.rb +5 -0
  62. data/lib/easypost/models/payload.rb +5 -0
  63. data/lib/easypost/models/payment_method.rb +5 -0
  64. data/lib/easypost/models/pickup.rb +9 -0
  65. data/lib/easypost/{pickup_rate.rb → models/pickup_rate.rb} +1 -1
  66. data/lib/easypost/{postage_label.rb → models/postage_label.rb} +1 -1
  67. data/lib/easypost/models/rate.rb +5 -0
  68. data/lib/easypost/models/referral.rb +5 -0
  69. data/lib/easypost/{refund.rb → models/refund.rb} +1 -1
  70. data/lib/easypost/models/report.rb +5 -0
  71. data/lib/easypost/models/scan_form.rb +6 -0
  72. data/lib/easypost/models/shipment.rb +10 -0
  73. data/lib/easypost/{tax_identifier.rb → models/tax_identifier.rb} +1 -1
  74. data/lib/easypost/models/tracker.rb +5 -0
  75. data/lib/easypost/models/user.rb +5 -0
  76. data/lib/easypost/models/webhook.rb +6 -0
  77. data/lib/easypost/models.rb +35 -0
  78. data/lib/easypost/services/address.rb +50 -0
  79. data/lib/easypost/services/api_key.rb +8 -0
  80. data/lib/easypost/services/base.rb +27 -0
  81. data/lib/easypost/services/batch.rb +53 -0
  82. data/lib/easypost/services/beta_rate.rb +12 -0
  83. data/lib/easypost/services/beta_referral_customer.rb +40 -0
  84. data/lib/easypost/services/billing.rb +75 -0
  85. data/lib/easypost/services/carrier_account.rb +44 -0
  86. data/lib/easypost/services/carrier_metadata.rb +22 -0
  87. data/lib/easypost/services/customs_info.rb +17 -0
  88. data/lib/easypost/services/customs_item.rb +15 -0
  89. data/lib/easypost/services/end_shipper.rb +31 -0
  90. data/lib/easypost/services/event.rb +32 -0
  91. data/lib/easypost/services/insurance.rb +26 -0
  92. data/lib/easypost/services/order.rb +30 -0
  93. data/lib/easypost/services/parcel.rb +16 -0
  94. data/lib/easypost/services/pickup.rb +40 -0
  95. data/lib/easypost/services/rate.rb +8 -0
  96. data/lib/easypost/services/referral_customer.rb +103 -0
  97. data/lib/easypost/services/refund.rb +26 -0
  98. data/lib/easypost/services/report.rb +42 -0
  99. data/lib/easypost/services/scan_form.rb +25 -0
  100. data/lib/easypost/services/shipment.rb +106 -0
  101. data/lib/easypost/services/tracker.rb +38 -0
  102. data/lib/easypost/services/user.rb +66 -0
  103. data/lib/easypost/services/webhook.rb +34 -0
  104. data/lib/easypost/services.rb +32 -0
  105. data/lib/easypost/util.rb +116 -161
  106. data/lib/easypost/utilities/constants.rb +5 -0
  107. data/lib/easypost/utilities/json.rb +23 -0
  108. data/lib/easypost/utilities/static_mapper.rb +73 -0
  109. data/lib/easypost/utilities/system.rb +36 -0
  110. data/lib/easypost.rb +14 -136
  111. metadata +177 -65
  112. data/.rubocop.yml +0 -11
  113. data/CODE_OF_CONDUCT.md +0 -16
  114. data/CONTRIBUTING.md +0 -47
  115. data/SECURITY.md +0 -7
  116. data/SUPPORT.md +0 -3
  117. data/easycop.yml +0 -180
  118. data/lib/easypost/address.rb +0 -40
  119. data/lib/easypost/api_key.rb +0 -5
  120. data/lib/easypost/batch.rb +0 -50
  121. data/lib/easypost/beta/end_shipper.rb +0 -44
  122. data/lib/easypost/beta/referral.rb +0 -110
  123. data/lib/easypost/beta.rb +0 -7
  124. data/lib/easypost/billing.rb +0 -72
  125. data/lib/easypost/brand.rb +0 -13
  126. data/lib/easypost/carrier_account.rb +0 -9
  127. data/lib/easypost/carrier_type.rb +0 -5
  128. data/lib/easypost/customs_info.rb +0 -9
  129. data/lib/easypost/customs_item.rb +0 -9
  130. data/lib/easypost/end_shipper.rb +0 -24
  131. data/lib/easypost/error.rb +0 -32
  132. data/lib/easypost/event.rb +0 -11
  133. data/lib/easypost/object.rb +0 -171
  134. data/lib/easypost/order.rb +0 -37
  135. data/lib/easypost/parcel.rb +0 -9
  136. data/lib/easypost/payment_method.rb +0 -11
  137. data/lib/easypost/pickup.rb +0 -37
  138. data/lib/easypost/rate.rb +0 -9
  139. data/lib/easypost/referral.rb +0 -102
  140. data/lib/easypost/report.rb +0 -23
  141. data/lib/easypost/resource.rb +0 -106
  142. data/lib/easypost/scan_form.rb +0 -11
  143. data/lib/easypost/shipment.rb +0 -155
  144. data/lib/easypost/tracker.rb +0 -12
  145. data/lib/easypost/user.rb +0 -71
  146. data/lib/easypost/webhook.rb +0 -57
@@ -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,32 @@
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/customs_info'
17
+ require_relative 'services/customs_item'
18
+ require_relative 'services/end_shipper'
19
+ require_relative 'services/event'
20
+ require_relative 'services/insurance'
21
+ require_relative 'services/order'
22
+ require_relative 'services/parcel'
23
+ require_relative 'services/pickup'
24
+ require_relative 'services/rate'
25
+ require_relative 'services/referral_customer'
26
+ require_relative 'services/refund'
27
+ require_relative 'services/report'
28
+ require_relative 'services/scan_form'
29
+ require_relative 'services/shipment'
30
+ require_relative 'services/tracker'
31
+ require_relative 'services/user'
32
+ require_relative 'services/webhook'
data/lib/easypost/util.rb CHANGED
@@ -1,198 +1,89 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Internal utilities helpful for this libraries operation.
4
- module EasyPost::Util
5
- attr_accessor :os_name, :os_version, :os_arch
6
-
7
- BY_PREFIX = {
8
- 'adr' => EasyPost::Address,
9
- 'batch' => EasyPost::Batch,
10
- 'brd' => EasyPost::Brand,
11
- 'ca' => EasyPost::CarrierAccount,
12
- 'cstinfo' => EasyPost::CustomsInfo,
13
- 'cstitem' => EasyPost::CustomsItem,
14
- 'es' => EasyPost::EndShipper,
15
- 'evt' => EasyPost::Event,
16
- 'hook' => EasyPost::Webhook,
17
- 'ins' => EasyPost::Insurance,
18
- 'order' => EasyPost::Order,
19
- 'pickup' => EasyPost::Pickup,
20
- 'pickuprate' => EasyPost::PickupRate,
21
- 'pl' => EasyPost::PostageLabel,
22
- 'plrep' => EasyPost::Report,
23
- 'prcl' => EasyPost::Parcel,
24
- 'rate' => EasyPost::Rate,
25
- 'refrep' => EasyPost::Report,
26
- 'rfnd' => EasyPost::Refund,
27
- 'sf' => EasyPost::ScanForm,
28
- 'shp' => EasyPost::Shipment,
29
- 'shpinvrep' => EasyPost::Report,
30
- 'shprep' => EasyPost::Report,
31
- 'trk' => EasyPost::Tracker,
32
- 'trkrep' => EasyPost::Report,
33
- 'user' => EasyPost::User,
34
- }.freeze
35
-
36
- BY_TYPE = {
37
- 'Address' => EasyPost::Address,
38
- 'Batch' => EasyPost::Batch,
39
- 'Brand' => EasyPost::Brand,
40
- 'CarbonOffset' => EasyPost::CarbonOffset,
41
- 'CarrierAccount' => EasyPost::CarrierAccount,
42
- 'CustomsInfo' => EasyPost::CustomsInfo,
43
- 'CustomsItem' => EasyPost::CustomsItem,
44
- 'EndShipper' => EasyPost::EndShipper,
45
- 'Event' => EasyPost::Event,
46
- 'Insurance' => EasyPost::Insurance,
47
- 'Order' => EasyPost::Order,
48
- 'Parcel' => EasyPost::Parcel,
49
- 'PaymentLogReport' => EasyPost::Report,
50
- 'Pickup' => EasyPost::Pickup,
51
- 'PickupRate' => EasyPost::PickupRate,
52
- 'PostageLabel' => EasyPost::PostageLabel,
53
- 'Rate' => EasyPost::Rate,
54
- 'Referral' => EasyPost::Beta::Referral,
55
- 'Refund' => EasyPost::Refund,
56
- 'RefundReport' => EasyPost::Report,
57
- 'Report' => EasyPost::Report,
58
- 'ScanForm' => EasyPost::ScanForm,
59
- 'Shipment' => EasyPost::Shipment,
60
- 'ShipmentInvoiceReport' => EasyPost::Report,
61
- 'ShipmentReport' => EasyPost::Report,
62
- 'TaxIdentifier' => EasyPost::TaxIdentifier,
63
- 'Tracker' => EasyPost::Tracker,
64
- 'TrackerReport' => EasyPost::Report,
65
- 'User' => EasyPost::User,
66
- 'Webhook' => EasyPost::Webhook,
67
- }.freeze
68
-
69
- def self.os_name
70
- case RUBY_PLATFORM
71
- when /linux/i
72
- 'Linux'
73
- when /darwin/i
74
- 'Darwin'
75
- when /cygwin|mswin|mingw|bccwin|wince|emx/i
76
- 'Windows'
77
- else
78
- 'Unknown'
79
- end
80
- end
81
-
82
- def self.os_version
83
- Gem::Platform.local.version
84
- end
3
+ require 'easypost/constants'
85
4
 
86
- def self.os_arch
87
- Gem::Platform.local.cpu
88
- end
5
+ # Client Library helper functions
6
+ module EasyPost::Util
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
89
11
 
90
- # Form-encode a multi-layer dictionary to a one-layer dictionary.
91
- def self.form_encode_params(hash, parent_keys = [], parent_dict = {})
92
- result = parent_dict or {}
93
- keys = parent_keys or []
94
-
95
- hash.each do |key, value|
96
- if value.instance_of?(Hash)
97
- keys << key
98
- result = form_encode_params(value, keys, result)
99
- else
100
- dict_key = build_dict_key(keys + [key])
101
- result[dict_key] = value
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)
102
19
  end
103
20
  end
104
- result
105
- end
106
-
107
- # Build a dict key from a list of keys.
108
- # Example: [code, number] -> code[number]
109
- def self.build_dict_key(keys)
110
- result = keys[0].to_s
111
21
 
112
- keys[1..-1].each do |key|
113
- result += "[#{key}]"
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
114
30
  end
115
31
 
116
- result
117
- end
118
-
119
- # Converts an object to an object ID.
120
- def self.objects_to_ids(obj)
121
- case obj
122
- when EasyPost::Resource
123
- { id: obj.id }
124
- when Hash
125
- result = {}
126
- obj.each { |k, v| result[k] = objects_to_ids(v) unless v.nil? }
127
- result
128
- when Array
129
- obj.map { |v| objects_to_ids(v) }
130
- else
131
- obj
132
- end
133
- end
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
134
40
 
135
- # Normalizes a list of strings.
136
- def self.normalize_string_list(lst)
137
- lst = lst.is_a?(String) ? lst.split(',') : Array(lst)
138
- lst.map(&:to_s).map(&:downcase).map(&:strip)
139
- end
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
140
48
 
141
- # Convert data to an EasyPost Object.
142
- def self.convert_to_easypost_object(response, api_key, parent = nil, name = nil)
143
- case response
144
- when Array
145
- response.map { |i| convert_to_easypost_object(i, api_key, parent) }
146
- when Hash
147
- if (cls_name = response[:object])
148
- cls = BY_TYPE[cls_name]
149
- elsif response[:id]
150
- if response[:id].index('_').nil?
151
- cls = EasyPost::EasyPostObject
152
- elsif (cls_prefix = response[:id][0..response[:id].index('_')])
153
- cls = BY_PREFIX[cls_prefix[0..-2]]
154
- end
155
- elsif response['id']
156
- if response['id'].index('_').nil?
157
- cls = EasyPost::EasyPostObject
158
- elsif (cls_prefix = response['id'][0..response['id'].index('_')])
159
- cls = BY_PREFIX[cls_prefix[0..-2]]
160
- end
49
+ if lowest_rate.nil? || rate.rate.to_f < lowest_rate.rate.to_f
50
+ lowest_rate = rate
161
51
  end
52
+ end
162
53
 
163
- cls ||= EasyPost::EasyPostObject
164
- cls.construct_from(response, api_key, parent, name)
165
- else
166
- response
54
+ if lowest_rate.nil?
55
+ raise EasyPost::Errors::FilteringError.new(EasyPost::Constants::NO_MATCHING_RATES)
167
56
  end
57
+
58
+ lowest_rate
168
59
  end
169
60
 
170
- # Gets the lowest rate of an EasyPost object such as a Shipment, Order, or Pickup.
61
+ # Gets the lowest stateless rate.
171
62
  # You can exclude by having `'!'` as the first element of your optional filter lists
172
- def self.get_lowest_object_rate(easypost_object, carriers = [], services = [], rates_key = 'rates')
63
+ def self.get_lowest_stateless_rate(stateless_rates, carriers = [], services = [])
173
64
  lowest_rate = nil
174
65
 
175
- carriers = EasyPost::Util.normalize_string_list(carriers)
66
+ carriers = EasyPost::InternalUtilities.normalize_string_list(carriers)
176
67
  negative_carriers = []
177
68
  carriers_copy = carriers.clone
178
69
  carriers_copy.each do |carrier|
179
70
  if carrier[0, 1] == '!'
180
- negative_carriers << carrier[1..-1]
71
+ negative_carriers << carrier[1..]
181
72
  carriers.delete(carrier)
182
73
  end
183
74
  end
184
75
 
185
- services = EasyPost::Util.normalize_string_list(services)
76
+ services = EasyPost::InternalUtilities.normalize_string_list(services)
186
77
  negative_services = []
187
78
  services_copy = services.clone
188
79
  services_copy.each do |service|
189
80
  if service[0, 1] == '!'
190
- negative_services << service[1..-1]
81
+ negative_services << service[1..]
191
82
  services.delete(service)
192
83
  end
193
84
  end
194
85
 
195
- easypost_object[rates_key].each do |rate|
86
+ stateless_rates.each do |rate|
196
87
  rate_carrier = rate.carrier.downcase
197
88
  if carriers.size.positive? && !carriers.include?(rate_carrier)
198
89
  next
@@ -214,8 +105,72 @@ module EasyPost::Util
214
105
  end
215
106
  end
216
107
 
217
- raise EasyPost::Error.new('No rates found.') if lowest_rate.nil?
108
+ if lowest_rate.nil?
109
+ raise EasyPost::Errors::FilteringError.new(EasyPost::Constants::NO_MATCHING_RATES)
110
+ end
218
111
 
219
112
  lowest_rate
220
113
  end
114
+
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)
118
+ end
119
+
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
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')
167
+
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)
172
+ end
173
+
174
+ JSON.parse(event_body)
175
+ end
221
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