stripe 1.30.3 → 2.0.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 (122) hide show
  1. checksums.yaml +4 -4
  2. data/.gitattributes +4 -0
  3. data/.github/ISSUE_TEMPLATE.md +5 -0
  4. data/.travis.yml +3 -14
  5. data/Gemfile +28 -4
  6. data/History.txt +180 -0
  7. data/README.md +147 -0
  8. data/Rakefile +10 -0
  9. data/VERSION +1 -1
  10. data/bin/stripe-console +12 -5
  11. data/lib/data/ca-certificates.crt +3868 -5114
  12. data/lib/stripe/account.rb +43 -23
  13. data/lib/stripe/alipay_account.rb +20 -0
  14. data/lib/stripe/api_operations/create.rb +2 -2
  15. data/lib/stripe/api_operations/delete.rb +2 -2
  16. data/lib/stripe/api_operations/list.rb +2 -3
  17. data/lib/stripe/api_operations/request.rb +9 -3
  18. data/lib/stripe/api_operations/save.rb +85 -0
  19. data/lib/stripe/api_resource.rb +38 -5
  20. data/lib/stripe/apple_pay_domain.rb +12 -0
  21. data/lib/stripe/application_fee.rb +8 -8
  22. data/lib/stripe/application_fee_refund.rb +7 -3
  23. data/lib/stripe/balance_transaction.rb +1 -1
  24. data/lib/stripe/bank_account.rb +13 -4
  25. data/lib/stripe/bitcoin_receiver.rb +6 -6
  26. data/lib/stripe/bitcoin_transaction.rb +1 -1
  27. data/lib/stripe/card.rb +9 -5
  28. data/lib/stripe/charge.rb +38 -20
  29. data/lib/stripe/country_spec.rb +9 -0
  30. data/lib/stripe/coupon.rb +1 -1
  31. data/lib/stripe/customer.rb +12 -10
  32. data/lib/stripe/dispute.rb +4 -5
  33. data/lib/stripe/errors.rb +92 -0
  34. data/lib/stripe/file_upload.rb +1 -1
  35. data/lib/stripe/invoice.rb +7 -7
  36. data/lib/stripe/invoice_item.rb +1 -1
  37. data/lib/stripe/list_object.rb +8 -7
  38. data/lib/stripe/order.rb +12 -4
  39. data/lib/stripe/order_return.rb +9 -0
  40. data/lib/stripe/plan.rb +1 -1
  41. data/lib/stripe/product.rb +2 -10
  42. data/lib/stripe/recipient.rb +1 -1
  43. data/lib/stripe/refund.rb +1 -1
  44. data/lib/stripe/reversal.rb +7 -3
  45. data/lib/stripe/singleton_api_resource.rb +3 -3
  46. data/lib/stripe/sku.rb +2 -2
  47. data/lib/stripe/source.rb +11 -0
  48. data/lib/stripe/stripe_client.rb +396 -0
  49. data/lib/stripe/stripe_object.rb +167 -91
  50. data/lib/stripe/stripe_response.rb +48 -0
  51. data/lib/stripe/subscription.rb +15 -9
  52. data/lib/stripe/subscription_item.rb +12 -0
  53. data/lib/stripe/three_d_secure.rb +9 -0
  54. data/lib/stripe/transfer.rb +4 -5
  55. data/lib/stripe/util.rb +105 -33
  56. data/lib/stripe/version.rb +1 -1
  57. data/lib/stripe.rb +69 -266
  58. data/spec/fixtures.json +1409 -0
  59. data/spec/fixtures.yaml +1153 -0
  60. data/spec/spec.json +19949 -0
  61. data/spec/spec.yaml +15504 -0
  62. data/stripe.gemspec +5 -18
  63. data/test/api_fixtures.rb +29 -0
  64. data/test/api_stub_helpers.rb +125 -0
  65. data/test/stripe/account_test.rb +163 -211
  66. data/test/stripe/alipay_account_test.rb +19 -0
  67. data/test/stripe/api_operations_test.rb +31 -0
  68. data/test/stripe/api_resource_test.rb +174 -340
  69. data/test/stripe/apple_pay_domain_test.rb +33 -0
  70. data/test/stripe/application_fee_refund_test.rb +22 -31
  71. data/test/stripe/application_fee_test.rb +6 -14
  72. data/test/stripe/balance_test.rb +3 -3
  73. data/test/stripe/bank_account_test.rb +41 -0
  74. data/test/stripe/bitcoin_receiver_test.rb +51 -42
  75. data/test/stripe/bitcoin_transaction_test.rb +11 -19
  76. data/test/stripe/charge_test.rb +39 -98
  77. data/test/stripe/country_spec_test.rb +20 -0
  78. data/test/stripe/coupon_test.rb +35 -11
  79. data/test/stripe/customer_card_test.rb +25 -46
  80. data/test/stripe/customer_test.rb +89 -61
  81. data/test/stripe/dispute_test.rb +28 -31
  82. data/test/stripe/errors_test.rb +18 -0
  83. data/test/stripe/file_upload_test.rb +32 -24
  84. data/test/stripe/invoice_item_test.rb +55 -0
  85. data/test/stripe/invoice_test.rb +50 -24
  86. data/test/stripe/list_object_test.rb +57 -45
  87. data/test/stripe/order_return_test.rb +21 -0
  88. data/test/stripe/order_test.rb +41 -34
  89. data/test/stripe/plan_test.rb +52 -0
  90. data/test/stripe/product_test.rb +31 -25
  91. data/test/stripe/recipient_card_test.rb +23 -40
  92. data/test/stripe/recipient_test.rb +50 -0
  93. data/test/stripe/refund_test.rb +20 -36
  94. data/test/stripe/reversal_test.rb +27 -31
  95. data/test/stripe/sku_test.rb +39 -13
  96. data/test/stripe/source_test.rb +43 -0
  97. data/test/stripe/stripe_client_test.rb +428 -0
  98. data/test/stripe/stripe_object_test.rb +186 -13
  99. data/test/stripe/stripe_response_test.rb +46 -0
  100. data/test/stripe/subscription_item_test.rb +54 -0
  101. data/test/stripe/subscription_test.rb +40 -52
  102. data/test/stripe/three_d_secure_test.rb +23 -0
  103. data/test/stripe/transfer_test.rb +38 -13
  104. data/test/stripe/util_test.rb +48 -16
  105. data/test/stripe_test.rb +25 -0
  106. data/test/test_data.rb +5 -621
  107. data/test/test_helper.rb +24 -24
  108. metadata +60 -139
  109. data/README.rdoc +0 -68
  110. data/gemfiles/default-with-activesupport.gemfile +0 -10
  111. data/gemfiles/json.gemfile +0 -12
  112. data/gemfiles/yajl.gemfile +0 -12
  113. data/lib/stripe/api_operations/update.rb +0 -58
  114. data/lib/stripe/errors/api_connection_error.rb +0 -4
  115. data/lib/stripe/errors/api_error.rb +0 -4
  116. data/lib/stripe/errors/authentication_error.rb +0 -4
  117. data/lib/stripe/errors/card_error.rb +0 -12
  118. data/lib/stripe/errors/invalid_request_error.rb +0 -11
  119. data/lib/stripe/errors/rate_limit_error.rb +0 -4
  120. data/lib/stripe/errors/stripe_error.rb +0 -26
  121. data/test/stripe/charge_refund_test.rb +0 -55
  122. data/test/stripe/metadata_test.rb +0 -129
@@ -0,0 +1,396 @@
1
+ module Stripe
2
+ # StripeClient executes requests against the Stripe API and allows a user to
3
+ # recover both a resource a call returns as well as a response object that
4
+ # contains information on the HTTP call.
5
+ class StripeClient
6
+ attr_accessor :conn
7
+
8
+ # Initializes a new StripeClient. Expects a Faraday connection object, and
9
+ # uses a default connection unless one is passed.
10
+ def initialize(conn = nil)
11
+ self.conn = conn || self.class.default_conn
12
+ @system_profiler = SystemProfiler.new
13
+ end
14
+
15
+ def self.active_client
16
+ Thread.current[:stripe_client] || default_client
17
+ end
18
+
19
+ def self.default_client
20
+ @default_client ||= StripeClient.new(default_conn)
21
+ end
22
+
23
+ # A default Faraday connection to be used when one isn't configured. This
24
+ # object should never be mutated, and instead instantiating your own
25
+ # connection and wrapping it in a StripeClient object should be preferred.
26
+ def self.default_conn
27
+ # We're going to keep connections around so that we can take advantage
28
+ # of connection re-use, so make sure that we have a separate connection
29
+ # object per thread.
30
+ Thread.current[:stripe_client_default_conn] ||= begin
31
+ conn = Faraday.new do |c|
32
+ c.use Faraday::Request::UrlEncoded
33
+ c.use Faraday::Response::RaiseError
34
+ c.adapter Faraday.default_adapter
35
+ end
36
+
37
+ if Stripe.verify_ssl_certs
38
+ conn.ssl.verify = true
39
+ conn.ssl.cert_store = Stripe.ca_store
40
+ else
41
+ conn.ssl.verify = false
42
+
43
+ unless @verify_ssl_warned
44
+ @verify_ssl_warned = true
45
+ $stderr.puts("WARNING: Running without SSL cert verification. " \
46
+ "You should never do this in production. " \
47
+ "Execute 'Stripe.verify_ssl_certs = true' to enable verification.")
48
+ end
49
+ end
50
+
51
+ conn
52
+ end
53
+ end
54
+
55
+ # Checks if an error is a problem that we should retry on. This includes both
56
+ # socket errors that may represent an intermittent problem and some special
57
+ # HTTP statuses.
58
+ def self.should_retry?(e, retry_count)
59
+ return false if retry_count >= Stripe.max_network_retries
60
+
61
+ # Retry on timeout-related problems (either on open or read).
62
+ return true if e.is_a?(Faraday::TimeoutError)
63
+
64
+ # Destination refused the connection, the connection was reset, or a
65
+ # variety of other connection failures. This could occur from a single
66
+ # saturated server, so retry in case it's intermittent.
67
+ return true if e.is_a?(Faraday::ConnectionFailed)
68
+
69
+ if e.is_a?(Faraday::ClientError) && e.response
70
+ # 409 conflict
71
+ return true if e.response[:status] == 409
72
+ end
73
+
74
+ false
75
+ end
76
+
77
+ def self.sleep_time(retry_count)
78
+ # Apply exponential backoff with initial_network_retry_delay on the
79
+ # number of attempts so far as inputs. Do not allow the number to exceed
80
+ # max_network_retry_delay.
81
+ sleep_seconds = [Stripe.initial_network_retry_delay * (2 ** (retry_count - 1)), Stripe.max_network_retry_delay].min
82
+
83
+ # Apply some jitter by randomizing the value in the range of (sleep_seconds
84
+ # / 2) to (sleep_seconds).
85
+ sleep_seconds = sleep_seconds * (0.5 * (1 + rand()))
86
+
87
+ # But never sleep less than the base sleep seconds.
88
+ sleep_seconds = [Stripe.initial_network_retry_delay, sleep_seconds].max
89
+
90
+ sleep_seconds
91
+ end
92
+
93
+ # Executes the API call within the given block. Usage looks like:
94
+ #
95
+ # client = StripeClient.new
96
+ # charge, resp = client.request { Charge.create }
97
+ #
98
+ def request(&block)
99
+ @last_response = nil
100
+ old_stripe_client = Thread.current[:stripe_client]
101
+ Thread.current[:stripe_client] = self
102
+
103
+ begin
104
+ res = block.call
105
+ [res, @last_response]
106
+ ensure
107
+ Thread.current[:stripe_client] = old_stripe_client
108
+ end
109
+ end
110
+
111
+ def execute_request(method, url,
112
+ api_base: nil, api_key: nil, headers: {}, params: {})
113
+
114
+ api_base ||= Stripe.api_base
115
+ api_key ||= Stripe.api_key
116
+
117
+ check_api_key!(api_key)
118
+
119
+ params = Util.objects_to_ids(params)
120
+ url = api_url(url, api_base)
121
+
122
+ case method.to_s.downcase.to_sym
123
+ when :get, :head, :delete
124
+ # Make params into GET parameters
125
+ url += "#{URI.parse(url).query ? '&' : '?'}#{Util.encode_parameters(params)}" if params && params.any?
126
+ payload = nil
127
+ else
128
+ if headers[:content_type] && headers[:content_type] == "multipart/form-data"
129
+ payload = params
130
+ else
131
+ payload = Util.encode_parameters(params)
132
+ end
133
+ end
134
+
135
+ http_resp = execute_request_with_rescues(api_base, 0) do
136
+ conn.run_request(
137
+ method,
138
+ url,
139
+ payload,
140
+ # TODO: Convert RestClient-style parameters.
141
+ request_headers(api_key, method).update(headers)
142
+ ) do |req|
143
+ req.options.open_timeout = Stripe.open_timeout
144
+ req.options.timeout = Stripe.read_timeout
145
+ end
146
+ end
147
+
148
+ begin
149
+ resp = StripeResponse.from_faraday_response(http_resp)
150
+ rescue JSON::ParserError
151
+ raise general_api_error(http_resp.code, http_resp.body)
152
+ end
153
+
154
+ # Allows StripeClient#request to return a response object to a caller.
155
+ @last_response = resp
156
+ [resp, api_key]
157
+ end
158
+
159
+ private
160
+
161
+ def api_url(url='', api_base=nil)
162
+ (api_base || Stripe.api_base) + url
163
+ end
164
+
165
+ def check_api_key!(api_key)
166
+ unless api_key
167
+ raise AuthenticationError.new('No API key provided. ' \
168
+ 'Set your API key using "Stripe.api_key = <API-KEY>". ' \
169
+ 'You can generate API keys from the Stripe web interface. ' \
170
+ 'See https://stripe.com/api for details, or email support@stripe.com ' \
171
+ 'if you have any questions.')
172
+ end
173
+
174
+ if api_key =~ /\s/
175
+ raise AuthenticationError.new('Your API key is invalid, as it contains ' \
176
+ 'whitespace. (HINT: You can double-check your API key from the ' \
177
+ 'Stripe web interface. See https://stripe.com/api for details, or ' \
178
+ 'email support@stripe.com if you have any questions.)')
179
+ end
180
+ end
181
+
182
+ def execute_request_with_rescues(api_base, retry_count, &block)
183
+ begin
184
+ resp = block.call
185
+
186
+ # We rescue all exceptions from a request so that we have an easy spot to
187
+ # implement our retry logic across the board. We'll re-raise if it's a type
188
+ # of exception that we didn't expect to handle.
189
+ rescue => e
190
+ if self.class.should_retry?(e, retry_count)
191
+ retry_count = retry_count + 1
192
+ sleep self.class.sleep_time(retry_count)
193
+ retry
194
+ end
195
+
196
+ case e
197
+ when Faraday::ClientError
198
+ if e.response
199
+ handle_api_error(e.response)
200
+ else
201
+ handle_network_error(e, retry_count, api_base)
202
+ end
203
+
204
+ # Only handle errors when we know we can do so, and re-raise otherwise.
205
+ # This should be pretty infrequent.
206
+ else
207
+ raise
208
+ end
209
+ end
210
+
211
+ resp
212
+ end
213
+
214
+ def general_api_error(status, body)
215
+ APIError.new("Invalid response object from API: #{body.inspect} " +
216
+ "(HTTP response code was #{status})",
217
+ http_status: status, http_body: body)
218
+ end
219
+
220
+
221
+ def handle_api_error(http_resp)
222
+ begin
223
+ resp = StripeResponse.from_faraday_hash(http_resp)
224
+ error = resp.data[:error]
225
+
226
+ unless error && error.is_a?(Hash)
227
+ raise StripeError.new("Indeterminate error")
228
+ end
229
+
230
+ rescue JSON::ParserError, StripeError
231
+ raise general_api_error(http_resp[:status], http_resp[:body])
232
+ end
233
+
234
+ case resp.http_status
235
+ when 400, 404
236
+ error = InvalidRequestError.new(
237
+ error[:message], error[:param],
238
+ http_status: resp.http_status, http_body: resp.http_body,
239
+ json_body: resp.data, http_headers: resp.http_headers
240
+ )
241
+ when 401
242
+ error = AuthenticationError.new(
243
+ error[:message],
244
+ http_status: resp.http_status, http_body: resp.http_body,
245
+ json_body: resp.data, http_headers: resp.http_headers
246
+ )
247
+ when 402
248
+ error = CardError.new(
249
+ error[:message], error[:param], error[:code],
250
+ http_status: resp.http_status, http_body: resp.http_body,
251
+ json_body: resp.data, http_headers: resp.http_headers
252
+ )
253
+ when 403
254
+ error = PermissionError.new(
255
+ error[:message],
256
+ http_status: resp.http_status, http_body: resp.http_body,
257
+ json_body: resp.data, http_headers: resp.http_headers
258
+ )
259
+ when 429
260
+ error = RateLimitError.new(
261
+ error[:message],
262
+ http_status: resp.http_status, http_body: resp.http_body,
263
+ json_body: resp.data, http_headers: resp.http_headers
264
+ )
265
+ else
266
+ error = APIError.new(
267
+ error[:message],
268
+ http_status: resp.http_status, http_body: resp.http_body,
269
+ json_body: resp.data, http_headers: resp.http_headers
270
+ )
271
+ end
272
+
273
+ error.response = resp
274
+ raise(error)
275
+ end
276
+
277
+ def handle_network_error(e, retry_count, api_base=nil)
278
+ case e
279
+ when Faraday::ConnectionFailed
280
+ message = "Unexpected error communicating when trying to connect to Stripe. " \
281
+ "You may be seeing this message because your DNS is not working. " \
282
+ "To check, try running 'host stripe.com' from the command line."
283
+
284
+ when Faraday::SSLError
285
+ message = "Could not establish a secure connection to Stripe, you may " \
286
+ "need to upgrade your OpenSSL version. To check, try running " \
287
+ "'openssl s_client -connect api.stripe.com:443' from the " \
288
+ "command line."
289
+
290
+ when Faraday::TimeoutError
291
+ api_base = Stripe.api_base unless api_base
292
+ message = "Could not connect to Stripe (#{api_base}). " \
293
+ "Please check your internet connection and try again. " \
294
+ "If this problem persists, you should check Stripe's service status at " \
295
+ "https://twitter.com/stripestatus, or let us know at support@stripe.com."
296
+
297
+ else
298
+ message = "Unexpected error communicating with Stripe. " \
299
+ "If this problem persists, let us know at support@stripe.com."
300
+
301
+ end
302
+
303
+ if retry_count > 0
304
+ message += " Request was retried #{retry_count} times."
305
+ end
306
+
307
+ raise APIConnectionError.new(message + "\n\n(Network error: #{e.message})")
308
+ end
309
+
310
+ def request_headers(api_key, method)
311
+ headers = {
312
+ 'User-Agent' => "Stripe/v1 RubyBindings/#{Stripe::VERSION}",
313
+ 'Authorization' => "Bearer #{api_key}",
314
+ 'Content-Type' => 'application/x-www-form-urlencoded'
315
+ }
316
+
317
+ # It is only safe to retry network failures on post and delete
318
+ # requests if we add an Idempotency-Key header
319
+ if [:post, :delete].include?(method) && Stripe.max_network_retries > 0
320
+ headers['Idempotency-Key'] ||= SecureRandom.uuid
321
+ end
322
+
323
+ headers['Stripe-Version'] = Stripe.api_version if Stripe.api_version
324
+ headers['Stripe-Account'] = Stripe.stripe_account if Stripe.stripe_account
325
+
326
+ user_agent = @system_profiler.user_agent
327
+ begin
328
+ headers.update(
329
+ 'X-Stripe-Client-User-Agent' => JSON.generate(user_agent)
330
+ )
331
+ rescue => e
332
+ headers.update(
333
+ 'X-Stripe-Client-Raw-User-Agent' => user_agent.inspect,
334
+ :error => "#{e} (#{e.class})"
335
+ )
336
+ end
337
+
338
+ headers
339
+ end
340
+
341
+ # SystemProfiler extracts information about the system that we're running
342
+ # in so that we can generate a rich user agent header to help debug
343
+ # integrations.
344
+ class SystemProfiler
345
+ def self.get_uname
346
+ if File.exist?('/proc/version')
347
+ File.read('/proc/version').strip
348
+ else
349
+ case RbConfig::CONFIG['host_os']
350
+ when /linux|darwin|bsd|sunos|solaris|cygwin/i
351
+ get_uname_from_system
352
+ when /mswin|mingw/i
353
+ get_uname_from_system_ver
354
+ else
355
+ "unknown platform"
356
+ end
357
+ end
358
+ end
359
+
360
+ def self.get_uname_from_system
361
+ (`uname -a 2>/dev/null` || '').strip
362
+ rescue Errno::ENOENT
363
+ "uname executable not found"
364
+ rescue Errno::ENOMEM # couldn't create subprocess
365
+ "uname lookup failed"
366
+ end
367
+
368
+ def self.get_uname_from_system_ver
369
+ (`ver` || '').strip
370
+ rescue Errno::ENOENT
371
+ "ver executable not found"
372
+ rescue Errno::ENOMEM # couldn't create subprocess
373
+ "uname lookup failed"
374
+ end
375
+
376
+ def initialize
377
+ @uname = self.class.get_uname
378
+ end
379
+
380
+ def user_agent
381
+ lang_version = "#{RUBY_VERSION} p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE})"
382
+
383
+ {
384
+ :bindings_version => Stripe::VERSION,
385
+ :lang => 'ruby',
386
+ :lang_version => lang_version,
387
+ :platform => RUBY_PLATFORM,
388
+ :engine => defined?(RUBY_ENGINE) ? RUBY_ENGINE : '',
389
+ :publisher => 'stripe',
390
+ :uname => @uname,
391
+ :hostname => Socket.gethostname,
392
+ }
393
+ end
394
+ end
395
+ end
396
+ end