bongloy 4.21.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (210) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +76 -0
  3. data/.editorconfig +10 -0
  4. data/.gitattributes +4 -0
  5. data/.github/ISSUE_TEMPLATE.md +5 -0
  6. data/.github/README.md +79 -0
  7. data/.github/pull.yml +5 -0
  8. data/.gitignore +8 -0
  9. data/.rubocop.yml +43 -0
  10. data/.rubocop_todo.yml +38 -0
  11. data/.travis.yml +43 -0
  12. data/.vscode/extensions.json +7 -0
  13. data/.vscode/settings.json +8 -0
  14. data/CHANGELOG.md +770 -0
  15. data/CONTRIBUTORS +3 -0
  16. data/Gemfile +39 -0
  17. data/History.txt +1 -0
  18. data/LICENSE +21 -0
  19. data/README.md +282 -0
  20. data/Rakefile +36 -0
  21. data/VERSION +1 -0
  22. data/bin/stripe-console +16 -0
  23. data/bongloy.gemspec +37 -0
  24. data/lib/bongloy.rb +7 -0
  25. data/lib/data/ca-certificates.crt +4043 -0
  26. data/lib/stripe.rb +208 -0
  27. data/lib/stripe/api_operations/create.rb +12 -0
  28. data/lib/stripe/api_operations/delete.rb +35 -0
  29. data/lib/stripe/api_operations/list.rb +30 -0
  30. data/lib/stripe/api_operations/nested_resource.rb +70 -0
  31. data/lib/stripe/api_operations/request.rb +53 -0
  32. data/lib/stripe/api_operations/save.rb +94 -0
  33. data/lib/stripe/api_resource.rb +107 -0
  34. data/lib/stripe/errors.rb +156 -0
  35. data/lib/stripe/list_object.rb +110 -0
  36. data/lib/stripe/oauth.rb +63 -0
  37. data/lib/stripe/object_types.rb +98 -0
  38. data/lib/stripe/resources.rb +79 -0
  39. data/lib/stripe/resources/account.rb +174 -0
  40. data/lib/stripe/resources/account_link.rb +9 -0
  41. data/lib/stripe/resources/alipay_account.rb +34 -0
  42. data/lib/stripe/resources/apple_pay_domain.rb +16 -0
  43. data/lib/stripe/resources/application_fee.rb +24 -0
  44. data/lib/stripe/resources/application_fee_refund.rb +30 -0
  45. data/lib/stripe/resources/balance.rb +7 -0
  46. data/lib/stripe/resources/balance_transaction.rb +13 -0
  47. data/lib/stripe/resources/bank_account.rb +42 -0
  48. data/lib/stripe/resources/bitcoin_receiver.rb +23 -0
  49. data/lib/stripe/resources/bitcoin_transaction.rb +15 -0
  50. data/lib/stripe/resources/capability.rb +33 -0
  51. data/lib/stripe/resources/card.rb +37 -0
  52. data/lib/stripe/resources/charge.rb +84 -0
  53. data/lib/stripe/resources/checkout/session.rb +11 -0
  54. data/lib/stripe/resources/country_spec.rb +9 -0
  55. data/lib/stripe/resources/coupon.rb +12 -0
  56. data/lib/stripe/resources/credit_note.rb +18 -0
  57. data/lib/stripe/resources/customer.rb +95 -0
  58. data/lib/stripe/resources/customer_balance_transaction.rb +30 -0
  59. data/lib/stripe/resources/discount.rb +7 -0
  60. data/lib/stripe/resources/dispute.rb +23 -0
  61. data/lib/stripe/resources/ephemeral_key.rb +19 -0
  62. data/lib/stripe/resources/event.rb +9 -0
  63. data/lib/stripe/resources/exchange_rate.rb +9 -0
  64. data/lib/stripe/resources/file.rb +44 -0
  65. data/lib/stripe/resources/file_link.rb +11 -0
  66. data/lib/stripe/resources/invoice.rb +48 -0
  67. data/lib/stripe/resources/invoice_item.rb +12 -0
  68. data/lib/stripe/resources/invoice_line_item.rb +7 -0
  69. data/lib/stripe/resources/issuer_fraud_record.rb +9 -0
  70. data/lib/stripe/resources/issuing/authorization.rb +25 -0
  71. data/lib/stripe/resources/issuing/card.rb +20 -0
  72. data/lib/stripe/resources/issuing/card_details.rb +9 -0
  73. data/lib/stripe/resources/issuing/cardholder.rb +13 -0
  74. data/lib/stripe/resources/issuing/dispute.rb +13 -0
  75. data/lib/stripe/resources/issuing/transaction.rb +12 -0
  76. data/lib/stripe/resources/login_link.rb +14 -0
  77. data/lib/stripe/resources/order.rb +32 -0
  78. data/lib/stripe/resources/order_return.rb +9 -0
  79. data/lib/stripe/resources/payment_intent.rb +30 -0
  80. data/lib/stripe/resources/payment_method.rb +24 -0
  81. data/lib/stripe/resources/payout.rb +24 -0
  82. data/lib/stripe/resources/person.rb +31 -0
  83. data/lib/stripe/resources/plan.rb +12 -0
  84. data/lib/stripe/resources/product.rb +12 -0
  85. data/lib/stripe/resources/radar/early_fraud_warning.rb +11 -0
  86. data/lib/stripe/resources/radar/value_list.rb +14 -0
  87. data/lib/stripe/resources/radar/value_list_item.rb +13 -0
  88. data/lib/stripe/resources/recipient.rb +17 -0
  89. data/lib/stripe/resources/recipient_transfer.rb +7 -0
  90. data/lib/stripe/resources/refund.rb +11 -0
  91. data/lib/stripe/resources/reporting/report_run.rb +12 -0
  92. data/lib/stripe/resources/reporting/report_type.rb +12 -0
  93. data/lib/stripe/resources/reversal.rb +29 -0
  94. data/lib/stripe/resources/review.rb +16 -0
  95. data/lib/stripe/resources/setup_intent.rb +24 -0
  96. data/lib/stripe/resources/sigma/scheduled_query_run.rb +15 -0
  97. data/lib/stripe/resources/sku.rb +12 -0
  98. data/lib/stripe/resources/source.rb +42 -0
  99. data/lib/stripe/resources/source_transaction.rb +7 -0
  100. data/lib/stripe/resources/subscription.rb +25 -0
  101. data/lib/stripe/resources/subscription_item.rb +17 -0
  102. data/lib/stripe/resources/subscription_schedule.rb +32 -0
  103. data/lib/stripe/resources/subscription_schedule_revision.rb +34 -0
  104. data/lib/stripe/resources/tax_id.rb +26 -0
  105. data/lib/stripe/resources/tax_rate.rb +11 -0
  106. data/lib/stripe/resources/terminal/connection_token.rb +11 -0
  107. data/lib/stripe/resources/terminal/location.rb +14 -0
  108. data/lib/stripe/resources/terminal/reader.rb +14 -0
  109. data/lib/stripe/resources/three_d_secure.rb +13 -0
  110. data/lib/stripe/resources/token.rb +9 -0
  111. data/lib/stripe/resources/topup.rb +18 -0
  112. data/lib/stripe/resources/transfer.rb +27 -0
  113. data/lib/stripe/resources/usage_record.rb +23 -0
  114. data/lib/stripe/resources/usage_record_summary.rb +7 -0
  115. data/lib/stripe/resources/webhook_endpoint.rb +12 -0
  116. data/lib/stripe/singleton_api_resource.rb +26 -0
  117. data/lib/stripe/stripe_client.rb +686 -0
  118. data/lib/stripe/stripe_object.rb +583 -0
  119. data/lib/stripe/stripe_response.rb +50 -0
  120. data/lib/stripe/util.rb +336 -0
  121. data/lib/stripe/version.rb +5 -0
  122. data/lib/stripe/webhook.rb +90 -0
  123. data/stripe.gemspec +37 -0
  124. data/test/api_stub_helpers.rb +1 -0
  125. data/test/openapi/README.md +9 -0
  126. data/test/stripe/account_link_test.rb +18 -0
  127. data/test/stripe/account_test.rb +428 -0
  128. data/test/stripe/alipay_account_test.rb +37 -0
  129. data/test/stripe/api_operations_test.rb +80 -0
  130. data/test/stripe/api_resource_test.rb +544 -0
  131. data/test/stripe/apple_pay_domain_test.rb +46 -0
  132. data/test/stripe/application_fee_refund_test.rb +37 -0
  133. data/test/stripe/application_fee_test.rb +58 -0
  134. data/test/stripe/balance_test.rb +13 -0
  135. data/test/stripe/bank_account_test.rb +36 -0
  136. data/test/stripe/capability_test.rb +45 -0
  137. data/test/stripe/charge_test.rb +80 -0
  138. data/test/stripe/checkout/session_test.rb +41 -0
  139. data/test/stripe/country_spec_test.rb +20 -0
  140. data/test/stripe/coupon_test.rb +61 -0
  141. data/test/stripe/credit_note_test.rb +61 -0
  142. data/test/stripe/customer_balance_transaction_test.rb +37 -0
  143. data/test/stripe/customer_card_test.rb +42 -0
  144. data/test/stripe/customer_test.rb +269 -0
  145. data/test/stripe/dispute_test.rb +51 -0
  146. data/test/stripe/ephemeral_key_test.rb +93 -0
  147. data/test/stripe/errors_test.rb +20 -0
  148. data/test/stripe/exchange_rate_test.rb +20 -0
  149. data/test/stripe/file_link_test.rb +41 -0
  150. data/test/stripe/file_test.rb +97 -0
  151. data/test/stripe/file_upload_test.rb +79 -0
  152. data/test/stripe/invoice_item_test.rb +66 -0
  153. data/test/stripe/invoice_line_item_test.rb +8 -0
  154. data/test/stripe/invoice_test.rb +213 -0
  155. data/test/stripe/issuer_fraud_record_test.rb +20 -0
  156. data/test/stripe/issuing/authorization_test.rb +72 -0
  157. data/test/stripe/issuing/card_test.rb +62 -0
  158. data/test/stripe/issuing/cardholder_test.rb +53 -0
  159. data/test/stripe/issuing/dispute_test.rb +45 -0
  160. data/test/stripe/issuing/transaction_test.rb +48 -0
  161. data/test/stripe/list_object_test.rb +156 -0
  162. data/test/stripe/login_link_test.rb +37 -0
  163. data/test/stripe/oauth_test.rb +88 -0
  164. data/test/stripe/order_return_test.rb +21 -0
  165. data/test/stripe/order_test.rb +82 -0
  166. data/test/stripe/payment_intent_test.rb +107 -0
  167. data/test/stripe/payment_method_test.rb +84 -0
  168. data/test/stripe/payout_test.rb +57 -0
  169. data/test/stripe/person_test.rb +46 -0
  170. data/test/stripe/plan_test.rb +98 -0
  171. data/test/stripe/product_test.rb +59 -0
  172. data/test/stripe/radar/early_fraud_warning_test.rb +22 -0
  173. data/test/stripe/radar/value_list_item_test.rb +48 -0
  174. data/test/stripe/radar/value_list_test.rb +61 -0
  175. data/test/stripe/recipient_test.rb +62 -0
  176. data/test/stripe/refund_test.rb +39 -0
  177. data/test/stripe/reporting/report_run_test.rb +33 -0
  178. data/test/stripe/reporting/report_type_test.rb +22 -0
  179. data/test/stripe/reversal_test.rb +43 -0
  180. data/test/stripe/review_test.rb +27 -0
  181. data/test/stripe/setup_intent_test.rb +84 -0
  182. data/test/stripe/sigma/scheduled_query_run_test.rb +22 -0
  183. data/test/stripe/sku_test.rb +60 -0
  184. data/test/stripe/source_test.rb +99 -0
  185. data/test/stripe/source_transaction_test.rb +19 -0
  186. data/test/stripe/stripe_client_test.rb +842 -0
  187. data/test/stripe/stripe_object_test.rb +525 -0
  188. data/test/stripe/stripe_response_test.rb +49 -0
  189. data/test/stripe/subscription_item_test.rb +63 -0
  190. data/test/stripe/subscription_schedule_revision_test.rb +37 -0
  191. data/test/stripe/subscription_schedule_test.rb +116 -0
  192. data/test/stripe/subscription_test.rb +80 -0
  193. data/test/stripe/tax_id_test.rb +31 -0
  194. data/test/stripe/tax_rate_test.rb +43 -0
  195. data/test/stripe/terminal/connection_token_test.rb +16 -0
  196. data/test/stripe/terminal/location_test.rb +68 -0
  197. data/test/stripe/terminal/reader_test.rb +62 -0
  198. data/test/stripe/three_d_secure_test.rb +23 -0
  199. data/test/stripe/topup_test.rb +62 -0
  200. data/test/stripe/transfer_test.rb +88 -0
  201. data/test/stripe/usage_record_summary_test.rb +19 -0
  202. data/test/stripe/usage_record_test.rb +28 -0
  203. data/test/stripe/util_test.rb +402 -0
  204. data/test/stripe/webhook_endpoint_test.rb +59 -0
  205. data/test/stripe/webhook_test.rb +96 -0
  206. data/test/stripe_mock.rb +77 -0
  207. data/test/stripe_test.rb +63 -0
  208. data/test/test_data.rb +61 -0
  209. data/test/test_helper.rb +71 -0
  210. metadata +372 -0
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stripe
4
+ class Transfer < APIResource
5
+ extend Stripe::APIOperations::Create
6
+ extend Stripe::APIOperations::List
7
+ include Stripe::APIOperations::Save
8
+ extend Stripe::APIOperations::NestedResource
9
+
10
+ OBJECT_NAME = "transfer".freeze
11
+
12
+ custom_method :cancel, http_verb: :post
13
+
14
+ nested_resource_class_methods :reversal,
15
+ operations: %i[create retrieve update list]
16
+
17
+ def cancel(params = {}, opts = {})
18
+ resp, opts = request(:post, resource_url + "/cancel", params, opts)
19
+ initialize_from(resp.data, opts)
20
+ end
21
+
22
+ def cancel_url
23
+ resource_url + "/cancel"
24
+ end
25
+ deprecate :cancel_url, :none, 2019, 11
26
+ end
27
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stripe
4
+ class UsageRecord < APIResource
5
+ OBJECT_NAME = "usage_record".freeze
6
+
7
+ def self.create(params = {}, opts = {})
8
+ unless params.key?(:subscription_item)
9
+ raise ArgumentError, "Params must have a subscription_item key"
10
+ end
11
+ req_params = params.clone.delete_if do |key, _value|
12
+ key == :subscription_item
13
+ end
14
+ resp, opts = request(
15
+ :post,
16
+ "/v1/subscription_items/#{params[:subscription_item]}/usage_records",
17
+ req_params,
18
+ opts
19
+ )
20
+ Util.convert_to_stripe_object(resp.data, opts)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stripe
4
+ class UsageRecordSummary < StripeObject
5
+ OBJECT_NAME = "usage_record_summary".freeze
6
+ end
7
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stripe
4
+ class WebhookEndpoint < APIResource
5
+ extend Stripe::APIOperations::Create
6
+ include Stripe::APIOperations::Delete
7
+ extend Stripe::APIOperations::List
8
+ include Stripe::APIOperations::Save
9
+
10
+ OBJECT_NAME = "webhook_endpoint".freeze
11
+ end
12
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stripe
4
+ class SingletonAPIResource < APIResource
5
+ def self.resource_url
6
+ if self == SingletonAPIResource
7
+ raise NotImplementedError,
8
+ "SingletonAPIResource is an abstract class. You should " \
9
+ "perform actions on its subclasses (Balance, etc.)"
10
+ end
11
+ # Namespaces are separated in object names with periods (.) and in URLs
12
+ # with forward slashes (/), so replace the former with the latter.
13
+ "/v1/#{self::OBJECT_NAME.downcase.tr('.', '/')}"
14
+ end
15
+
16
+ def resource_url
17
+ self.class.resource_url
18
+ end
19
+
20
+ def self.retrieve(opts = {})
21
+ instance = new(nil, Util.normalize_opts(opts))
22
+ instance.refresh
23
+ instance
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,686 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stripe
4
+ # StripeClient executes requests against the Stripe API and allows a user to
5
+ # recover both a resource a call returns as well as a response object that
6
+ # contains information on the HTTP call.
7
+ class StripeClient
8
+ attr_accessor :conn
9
+
10
+ # Initializes a new StripeClient. Expects a Faraday connection object, and
11
+ # uses a default connection unless one is passed.
12
+ def initialize(conn = nil)
13
+ self.conn = conn || self.class.default_conn
14
+ @system_profiler = SystemProfiler.new
15
+ @last_request_metrics = nil
16
+ end
17
+
18
+ def self.active_client
19
+ Thread.current[:stripe_client] || default_client
20
+ end
21
+
22
+ def self.default_client
23
+ Thread.current[:stripe_client_default_client] ||=
24
+ StripeClient.new(default_conn)
25
+ end
26
+
27
+ # A default Faraday connection to be used when one isn't configured. This
28
+ # object should never be mutated, and instead instantiating your own
29
+ # connection and wrapping it in a StripeClient object should be preferred.
30
+ def self.default_conn
31
+ # We're going to keep connections around so that we can take advantage
32
+ # of connection re-use, so make sure that we have a separate connection
33
+ # object per thread.
34
+ Thread.current[:stripe_client_default_conn] ||= begin
35
+ conn = Faraday.new do |builder|
36
+ builder.use Faraday::Request::Multipart
37
+ builder.use Faraday::Request::UrlEncoded
38
+ builder.use Faraday::Response::RaiseError
39
+
40
+ # Net::HTTP::Persistent doesn't seem to do well on Windows or JRuby,
41
+ # so fall back to default there.
42
+ if Gem.win_platform? || RUBY_PLATFORM == "java"
43
+ builder.adapter :net_http
44
+ else
45
+ builder.adapter :net_http_persistent
46
+ end
47
+ end
48
+
49
+ conn.proxy = Stripe.proxy if Stripe.proxy
50
+
51
+ if Stripe.verify_ssl_certs
52
+ conn.ssl.verify = true
53
+ conn.ssl.cert_store = Stripe.ca_store
54
+ else
55
+ conn.ssl.verify = false
56
+
57
+ unless @verify_ssl_warned
58
+ @verify_ssl_warned = true
59
+ warn("WARNING: Running without SSL cert verification. " \
60
+ "You should never do this in production. " \
61
+ "Execute `Stripe.verify_ssl_certs = true` to enable " \
62
+ "verification.")
63
+ end
64
+ end
65
+
66
+ conn
67
+ end
68
+ end
69
+
70
+ # Checks if an error is a problem that we should retry on. This includes
71
+ # both socket errors that may represent an intermittent problem and some
72
+ # special HTTP statuses.
73
+ def self.should_retry?(error, num_retries)
74
+ return false if num_retries >= Stripe.max_network_retries
75
+
76
+ # Retry on timeout-related problems (either on open or read).
77
+ return true if error.is_a?(Faraday::TimeoutError)
78
+
79
+ # Destination refused the connection, the connection was reset, or a
80
+ # variety of other connection failures. This could occur from a single
81
+ # saturated server, so retry in case it's intermittent.
82
+ return true if error.is_a?(Faraday::ConnectionFailed)
83
+
84
+ if error.is_a?(Faraday::ClientError) && error.response
85
+ # 409 conflict
86
+ return true if error.response[:status] == 409
87
+ end
88
+
89
+ false
90
+ end
91
+
92
+ def self.sleep_time(num_retries)
93
+ # Apply exponential backoff with initial_network_retry_delay on the
94
+ # number of num_retries so far as inputs. Do not allow the number to
95
+ # exceed max_network_retry_delay.
96
+ sleep_seconds = [
97
+ Stripe.initial_network_retry_delay * (2**(num_retries - 1)),
98
+ Stripe.max_network_retry_delay,
99
+ ].min
100
+
101
+ # Apply some jitter by randomizing the value in the range of
102
+ # (sleep_seconds / 2) to (sleep_seconds).
103
+ sleep_seconds *= (0.5 * (1 + rand))
104
+
105
+ # But never sleep less than the base sleep seconds.
106
+ sleep_seconds = [Stripe.initial_network_retry_delay, sleep_seconds].max
107
+
108
+ sleep_seconds
109
+ end
110
+
111
+ # Executes the API call within the given block. Usage looks like:
112
+ #
113
+ # client = StripeClient.new
114
+ # charge, resp = client.request { Charge.create }
115
+ #
116
+ def request
117
+ @last_response = nil
118
+ old_stripe_client = Thread.current[:stripe_client]
119
+ Thread.current[:stripe_client] = self
120
+
121
+ begin
122
+ res = yield
123
+ [res, @last_response]
124
+ ensure
125
+ Thread.current[:stripe_client] = old_stripe_client
126
+ end
127
+ end
128
+
129
+ def execute_request(method, path,
130
+ api_base: nil, api_key: nil, headers: {}, params: {})
131
+ api_base ||= Stripe.api_base
132
+ api_key ||= Stripe.api_key
133
+ params = Util.objects_to_ids(params)
134
+
135
+ check_api_key!(api_key)
136
+
137
+ body = nil
138
+ query_params = nil
139
+ case method.to_s.downcase.to_sym
140
+ when :get, :head, :delete
141
+ query_params = params
142
+ else
143
+ body = params
144
+ end
145
+
146
+ # This works around an edge case where we end up with both query
147
+ # parameters in `query_params` and query parameters that are appended
148
+ # onto the end of the given path. In this case, Faraday will silently
149
+ # discard the URL's parameters which may break a request.
150
+ #
151
+ # Here we decode any parameters that were added onto the end of a path
152
+ # and add them to `query_params` so that all parameters end up in one
153
+ # place and all of them are correctly included in the final request.
154
+ u = URI.parse(path)
155
+ unless u.query.nil?
156
+ query_params ||= {}
157
+ query_params = Hash[URI.decode_www_form(u.query)].merge(query_params)
158
+
159
+ # Reset the path minus any query parameters that were specified.
160
+ path = u.path
161
+ end
162
+
163
+ headers = request_headers(api_key, method)
164
+ .update(Util.normalize_headers(headers))
165
+ params_encoder = FaradayStripeEncoder.new
166
+ url = api_url(path, api_base)
167
+
168
+ # stores information on the request we're about to make so that we don't
169
+ # have to pass as many parameters around for logging.
170
+ context = RequestLogContext.new
171
+ context.account = headers["Stripe-Account"]
172
+ context.api_key = api_key
173
+ context.api_version = headers["Stripe-Version"]
174
+ context.body = body ? params_encoder.encode(body) : nil
175
+ context.idempotency_key = headers["Idempotency-Key"]
176
+ context.method = method
177
+ context.path = path
178
+ context.query_params = if query_params
179
+ params_encoder.encode(query_params)
180
+ end
181
+
182
+ # note that both request body and query params will be passed through
183
+ # `FaradayStripeEncoder`
184
+ http_resp = execute_request_with_rescues(api_base, context) do
185
+ conn.run_request(method, url, body, headers) do |req|
186
+ req.options.open_timeout = Stripe.open_timeout
187
+ req.options.params_encoder = params_encoder
188
+ req.options.timeout = Stripe.read_timeout
189
+ req.params = query_params unless query_params.nil?
190
+ end
191
+ end
192
+
193
+ begin
194
+ resp = StripeResponse.from_faraday_response(http_resp)
195
+ rescue JSON::ParserError
196
+ raise general_api_error(http_resp.status, http_resp.body)
197
+ end
198
+
199
+ # Allows StripeClient#request to return a response object to a caller.
200
+ @last_response = resp
201
+ [resp, api_key]
202
+ end
203
+
204
+ # Used to workaround buggy behavior in Faraday: the library will try to
205
+ # reshape anything that we pass to `req.params` with one of its default
206
+ # encoders. I don't think this process is supposed to be lossy, but it is
207
+ # -- in particular when we send our integer-indexed maps (i.e. arrays),
208
+ # Faraday ends up stripping out the integer indexes.
209
+ #
210
+ # We work around the problem by implementing our own simplified encoder and
211
+ # telling Faraday to use that.
212
+ #
213
+ # The class also performs simple caching so that we don't have to encode
214
+ # parameters twice for every request (once to build the request and once
215
+ # for logging).
216
+ #
217
+ # When initialized with `multipart: true`, the encoder just inspects the
218
+ # hash instead to get a decent representation for logging. In the case of a
219
+ # multipart request, Faraday won't use the result of this encoder.
220
+ class FaradayStripeEncoder
221
+ def initialize
222
+ @cache = {}
223
+ end
224
+
225
+ # This is quite subtle, but for a `multipart/form-data` request Faraday
226
+ # will throw away the result of this encoder and build its body.
227
+ def encode(hash)
228
+ @cache.fetch(hash) do |k|
229
+ @cache[k] = Util.encode_parameters(hash)
230
+ end
231
+ end
232
+
233
+ # We should never need to do this so it's not implemented.
234
+ def decode(_str)
235
+ raise NotImplementedError,
236
+ "#{self.class.name} does not implement #decode"
237
+ end
238
+ end
239
+
240
+ private def api_url(url = "", api_base = nil)
241
+ (api_base || Stripe.api_base) + url
242
+ end
243
+
244
+ private def check_api_key!(api_key)
245
+ unless api_key
246
+ raise AuthenticationError, "No API key provided. " \
247
+ 'Set your API key using "Stripe.api_key = <API-KEY>". ' \
248
+ "You can generate API keys from the Stripe web interface. " \
249
+ "See https://stripe.com/api for details, or email " \
250
+ "support@stripe.com if you have any questions."
251
+ end
252
+
253
+ return unless api_key =~ /\s/
254
+
255
+ raise AuthenticationError, "Your API key is invalid, as it contains " \
256
+ "whitespace. (HINT: You can double-check your API key from the " \
257
+ "Stripe web interface. See https://stripe.com/api for details, or " \
258
+ "email support@stripe.com if you have any questions.)"
259
+ end
260
+
261
+ private def execute_request_with_rescues(api_base, context)
262
+ num_retries = 0
263
+ begin
264
+ request_start = Time.now
265
+ log_request(context, num_retries)
266
+ resp = yield
267
+ context = context.dup_from_response(resp)
268
+ log_response(context, request_start, resp.status, resp.body)
269
+
270
+ if Stripe.enable_telemetry? && context.request_id
271
+ request_duration_ms = ((Time.now - request_start) * 1000).to_int
272
+ @last_request_metrics =
273
+ StripeRequestMetrics.new(context.request_id, request_duration_ms)
274
+ end
275
+
276
+ # We rescue all exceptions from a request so that we have an easy spot to
277
+ # implement our retry logic across the board. We'll re-raise if it's a
278
+ # type of exception that we didn't expect to handle.
279
+ rescue StandardError => e
280
+ # If we modify context we copy it into a new variable so as not to
281
+ # taint the original on a retry.
282
+ error_context = context
283
+
284
+ if e.respond_to?(:response) && e.response
285
+ error_context = context.dup_from_response(e.response)
286
+ log_response(error_context, request_start,
287
+ e.response[:status], e.response[:body])
288
+ else
289
+ log_response_error(error_context, request_start, e)
290
+ end
291
+
292
+ if self.class.should_retry?(e, num_retries)
293
+ num_retries += 1
294
+ sleep self.class.sleep_time(num_retries)
295
+ retry
296
+ end
297
+
298
+ case e
299
+ when Faraday::ClientError
300
+ if e.response
301
+ handle_error_response(e.response, error_context)
302
+ else
303
+ handle_network_error(e, error_context, num_retries, api_base)
304
+ end
305
+
306
+ # Only handle errors when we know we can do so, and re-raise otherwise.
307
+ # This should be pretty infrequent.
308
+ else
309
+ raise
310
+ end
311
+ end
312
+
313
+ resp
314
+ end
315
+
316
+ private def general_api_error(status, body)
317
+ APIError.new("Invalid response object from API: #{body.inspect} " \
318
+ "(HTTP response code was #{status})",
319
+ http_status: status, http_body: body)
320
+ end
321
+
322
+ # Formats a plugin "app info" hash into a string that we can tack onto the
323
+ # end of a User-Agent string where it'll be fairly prominent in places like
324
+ # the Dashboard. Note that this formatting has been implemented to match
325
+ # other libraries, and shouldn't be changed without universal consensus.
326
+ private def format_app_info(info)
327
+ str = info[:name]
328
+ str = "#{str}/#{info[:version]}" unless info[:version].nil?
329
+ str = "#{str} (#{info[:url]})" unless info[:url].nil?
330
+ str
331
+ end
332
+
333
+ private def handle_error_response(http_resp, context)
334
+ begin
335
+ resp = StripeResponse.from_faraday_hash(http_resp)
336
+ error_data = resp.data[:error]
337
+
338
+ raise StripeError, "Indeterminate error" unless error_data
339
+ rescue JSON::ParserError, StripeError
340
+ raise general_api_error(http_resp[:status], http_resp[:body])
341
+ end
342
+
343
+ error = if error_data.is_a?(String)
344
+ specific_oauth_error(resp, error_data, context)
345
+ else
346
+ specific_api_error(resp, error_data, context)
347
+ end
348
+
349
+ error.response = resp
350
+ raise(error)
351
+ end
352
+
353
+ private def specific_api_error(resp, error_data, context)
354
+ Util.log_error("Stripe API error",
355
+ status: resp.http_status,
356
+ error_code: error_data[:code],
357
+ error_message: error_data[:message],
358
+ error_param: error_data[:param],
359
+ error_type: error_data[:type],
360
+ idempotency_key: context.idempotency_key,
361
+ request_id: context.request_id)
362
+
363
+ # The standard set of arguments that can be used to initialize most of
364
+ # the exceptions.
365
+ opts = {
366
+ http_body: resp.http_body,
367
+ http_headers: resp.http_headers,
368
+ http_status: resp.http_status,
369
+ json_body: resp.data,
370
+ code: error_data[:code],
371
+ }
372
+
373
+ case resp.http_status
374
+ when 400, 404
375
+ case error_data[:type]
376
+ when "idempotency_error"
377
+ IdempotencyError.new(error_data[:message], opts)
378
+ else
379
+ InvalidRequestError.new(
380
+ error_data[:message], error_data[:param],
381
+ opts
382
+ )
383
+ end
384
+ when 401
385
+ AuthenticationError.new(error_data[:message], opts)
386
+ when 402
387
+ # TODO: modify CardError constructor to make code a keyword argument
388
+ # so we don't have to delete it from opts
389
+ opts.delete(:code)
390
+ CardError.new(
391
+ error_data[:message], error_data[:param], error_data[:code],
392
+ opts
393
+ )
394
+ when 403
395
+ PermissionError.new(error_data[:message], opts)
396
+ when 429
397
+ RateLimitError.new(error_data[:message], opts)
398
+ else
399
+ APIError.new(error_data[:message], opts)
400
+ end
401
+ end
402
+
403
+ # Attempts to look at a response's error code and return an OAuth error if
404
+ # one matches. Will return `nil` if the code isn't recognized.
405
+ private def specific_oauth_error(resp, error_code, context)
406
+ description = resp.data[:error_description] || error_code
407
+
408
+ Util.log_error("Stripe OAuth error",
409
+ status: resp.http_status,
410
+ error_code: error_code,
411
+ error_description: description,
412
+ idempotency_key: context.idempotency_key,
413
+ request_id: context.request_id)
414
+
415
+ args = [error_code, description, {
416
+ http_status: resp.http_status, http_body: resp.http_body,
417
+ json_body: resp.data, http_headers: resp.http_headers,
418
+ },]
419
+
420
+ case error_code
421
+ when "invalid_client"
422
+ OAuth::InvalidClientError.new(*args)
423
+ when "invalid_grant"
424
+ OAuth::InvalidGrantError.new(*args)
425
+ when "invalid_request"
426
+ OAuth::InvalidRequestError.new(*args)
427
+ when "invalid_scope"
428
+ OAuth::InvalidScopeError.new(*args)
429
+ when "unsupported_grant_type"
430
+ OAuth::UnsupportedGrantTypeError.new(*args)
431
+ when "unsupported_response_type"
432
+ OAuth::UnsupportedResponseTypeError.new(*args)
433
+ else
434
+ # We'd prefer that all errors are typed, but we create a generic
435
+ # OAuthError in case we run into a code that we don't recognize.
436
+ OAuth::OAuthError.new(*args)
437
+ end
438
+ end
439
+
440
+ private def handle_network_error(error, context, num_retries,
441
+ api_base = nil)
442
+ Util.log_error("Stripe network error",
443
+ error_message: error.message,
444
+ idempotency_key: context.idempotency_key,
445
+ request_id: context.request_id)
446
+
447
+ case error
448
+ when Faraday::ConnectionFailed
449
+ message = "Unexpected error communicating when trying to connect to " \
450
+ "Stripe. You may be seeing this message because your DNS is not " \
451
+ "working. To check, try running `host stripe.com` from the " \
452
+ "command line."
453
+
454
+ when Faraday::SSLError
455
+ message = "Could not establish a secure connection to Stripe, you " \
456
+ "may need to upgrade your OpenSSL version. To check, try running " \
457
+ "`openssl s_client -connect api.stripe.com:443` from the command " \
458
+ "line."
459
+
460
+ when Faraday::TimeoutError
461
+ api_base ||= Stripe.api_base
462
+ message = "Could not connect to Stripe (#{api_base}). " \
463
+ "Please check your internet connection and try again. " \
464
+ "If this problem persists, you should check Stripe's service " \
465
+ "status at https://status.stripe.com, or let us know at " \
466
+ "support@stripe.com."
467
+
468
+ else
469
+ message = "Unexpected error communicating with Stripe. " \
470
+ "If this problem persists, let us know at support@stripe.com."
471
+
472
+ end
473
+
474
+ message += " Request was retried #{num_retries} times." if num_retries > 0
475
+
476
+ raise APIConnectionError,
477
+ message + "\n\n(Network error: #{error.message})"
478
+ end
479
+
480
+ private def request_headers(api_key, method)
481
+ user_agent = "Stripe/v1 RubyBindings/#{Stripe::VERSION}"
482
+ unless Stripe.app_info.nil?
483
+ user_agent += " " + format_app_info(Stripe.app_info)
484
+ end
485
+
486
+ headers = {
487
+ "User-Agent" => user_agent,
488
+ "Authorization" => "Bearer #{api_key}",
489
+ "Content-Type" => "application/x-www-form-urlencoded",
490
+ }
491
+
492
+ if Stripe.enable_telemetry? && !@last_request_metrics.nil?
493
+ headers["X-Stripe-Client-Telemetry"] = JSON.generate(
494
+ last_request_metrics: @last_request_metrics.payload
495
+ )
496
+ end
497
+
498
+ # It is only safe to retry network failures on post and delete
499
+ # requests if we add an Idempotency-Key header
500
+ if %i[post delete].include?(method) && Stripe.max_network_retries > 0
501
+ headers["Idempotency-Key"] ||= SecureRandom.uuid
502
+ end
503
+
504
+ headers["Stripe-Version"] = Stripe.api_version if Stripe.api_version
505
+ headers["Stripe-Account"] = Stripe.stripe_account if Stripe.stripe_account
506
+
507
+ user_agent = @system_profiler.user_agent
508
+ begin
509
+ headers.update(
510
+ "X-Stripe-Client-User-Agent" => JSON.generate(user_agent)
511
+ )
512
+ rescue StandardError => e
513
+ headers.update(
514
+ "X-Stripe-Client-Raw-User-Agent" => user_agent.inspect,
515
+ :error => "#{e} (#{e.class})"
516
+ )
517
+ end
518
+
519
+ headers
520
+ end
521
+
522
+ private def log_request(context, num_retries)
523
+ Util.log_info("Request to Stripe API",
524
+ account: context.account,
525
+ api_version: context.api_version,
526
+ idempotency_key: context.idempotency_key,
527
+ method: context.method,
528
+ num_retries: num_retries,
529
+ path: context.path)
530
+ Util.log_debug("Request details",
531
+ body: context.body,
532
+ idempotency_key: context.idempotency_key,
533
+ query_params: context.query_params)
534
+ end
535
+
536
+ private def log_response(context, request_start, status, body)
537
+ Util.log_info("Response from Stripe API",
538
+ account: context.account,
539
+ api_version: context.api_version,
540
+ elapsed: Time.now - request_start,
541
+ idempotency_key: context.idempotency_key,
542
+ method: context.method,
543
+ path: context.path,
544
+ request_id: context.request_id,
545
+ status: status)
546
+ Util.log_debug("Response details",
547
+ body: body,
548
+ idempotency_key: context.idempotency_key,
549
+ request_id: context.request_id)
550
+
551
+ return unless context.request_id
552
+
553
+ Util.log_debug("Dashboard link for request",
554
+ idempotency_key: context.idempotency_key,
555
+ request_id: context.request_id,
556
+ url: Util.request_id_dashboard_url(context.request_id,
557
+ context.api_key))
558
+ end
559
+
560
+ private def log_response_error(context, request_start, error)
561
+ Util.log_error("Request error",
562
+ elapsed: Time.now - request_start,
563
+ error_message: error.message,
564
+ idempotency_key: context.idempotency_key,
565
+ method: context.method,
566
+ path: context.path)
567
+ end
568
+
569
+ # RequestLogContext stores information about a request that's begin made so
570
+ # that we can log certain information. It's useful because it means that we
571
+ # don't have to pass around as many parameters.
572
+ class RequestLogContext
573
+ attr_accessor :body
574
+ attr_accessor :account
575
+ attr_accessor :api_key
576
+ attr_accessor :api_version
577
+ attr_accessor :idempotency_key
578
+ attr_accessor :method
579
+ attr_accessor :path
580
+ attr_accessor :query_params
581
+ attr_accessor :request_id
582
+
583
+ # The idea with this method is that we might want to update some of
584
+ # context information because a response that we've received from the API
585
+ # contains information that's more authoritative than what we started
586
+ # with for a request. For example, we should trust whatever came back in
587
+ # a `Stripe-Version` header beyond what configuration information that we
588
+ # might have had available.
589
+ def dup_from_response(resp)
590
+ return self if resp.nil?
591
+
592
+ # Faraday's API is a little unusual. Normally it'll produce a response
593
+ # object with a `headers` method, but on error what it puts into
594
+ # `e.response` is an untyped `Hash`.
595
+ headers = if resp.is_a?(Faraday::Response)
596
+ resp.headers
597
+ else
598
+ resp[:headers]
599
+ end
600
+
601
+ context = dup
602
+ context.account = headers["Stripe-Account"]
603
+ context.api_version = headers["Stripe-Version"]
604
+ context.idempotency_key = headers["Idempotency-Key"]
605
+ context.request_id = headers["Request-Id"]
606
+ context
607
+ end
608
+ end
609
+
610
+ # SystemProfiler extracts information about the system that we're running
611
+ # in so that we can generate a rich user agent header to help debug
612
+ # integrations.
613
+ class SystemProfiler
614
+ def self.uname
615
+ if ::File.exist?("/proc/version")
616
+ ::File.read("/proc/version").strip
617
+ else
618
+ case RbConfig::CONFIG["host_os"]
619
+ when /linux|darwin|bsd|sunos|solaris|cygwin/i
620
+ uname_from_system
621
+ when /mswin|mingw/i
622
+ uname_from_system_ver
623
+ else
624
+ "unknown platform"
625
+ end
626
+ end
627
+ end
628
+
629
+ def self.uname_from_system
630
+ (`uname -a 2>/dev/null` || "").strip
631
+ rescue Errno::ENOENT
632
+ "uname executable not found"
633
+ rescue Errno::ENOMEM # couldn't create subprocess
634
+ "uname lookup failed"
635
+ end
636
+
637
+ def self.uname_from_system_ver
638
+ (`ver` || "").strip
639
+ rescue Errno::ENOENT
640
+ "ver executable not found"
641
+ rescue Errno::ENOMEM # couldn't create subprocess
642
+ "uname lookup failed"
643
+ end
644
+
645
+ def initialize
646
+ @uname = self.class.uname
647
+ end
648
+
649
+ def user_agent
650
+ lang_version = "#{RUBY_VERSION} p#{RUBY_PATCHLEVEL} " \
651
+ "(#{RUBY_RELEASE_DATE})"
652
+
653
+ {
654
+ application: Stripe.app_info,
655
+ bindings_version: Stripe::VERSION,
656
+ lang: "ruby",
657
+ lang_version: lang_version,
658
+ platform: RUBY_PLATFORM,
659
+ engine: defined?(RUBY_ENGINE) ? RUBY_ENGINE : "",
660
+ publisher: "stripe",
661
+ uname: @uname,
662
+ hostname: Socket.gethostname,
663
+ }.delete_if { |_k, v| v.nil? }
664
+ end
665
+ end
666
+
667
+ # StripeRequestMetrics tracks metadata to be reported to stripe for metrics
668
+ # collection
669
+ class StripeRequestMetrics
670
+ # The Stripe request ID of the response.
671
+ attr_accessor :request_id
672
+
673
+ # Request duration in milliseconds
674
+ attr_accessor :request_duration_ms
675
+
676
+ def initialize(request_id, request_duration_ms)
677
+ self.request_id = request_id
678
+ self.request_duration_ms = request_duration_ms
679
+ end
680
+
681
+ def payload
682
+ { request_id: request_id, request_duration_ms: request_duration_ms }
683
+ end
684
+ end
685
+ end
686
+ end