stripe 1.5.11

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,564 @@
1
+ # Stripe Ruby bindings
2
+ # API spec at http://stripe.com/api/spec
3
+ # Authors: Ross Boucher <boucher@stripe.com> and Greg Brockman <gdb@stripe.com>
4
+ require 'cgi'
5
+ require 'set'
6
+
7
+ require 'rubygems'
8
+ require 'openssl'
9
+ require 'rest_client'
10
+
11
+ begin
12
+ require 'json'
13
+ rescue LoadError
14
+ raise if defined?(JSON)
15
+ require File.join(File.dirname(__FILE__), '../vendor/stripe-json/lib/json/pure')
16
+ end
17
+
18
+ require File.join(File.dirname(__FILE__), 'stripe/version')
19
+
20
+ module Stripe
21
+ @@ssl_bundle_path = File.join(File.dirname(__FILE__), 'data/ca-certificates.crt')
22
+ @@api_key = nil
23
+ @@api_base = 'https://api.stripe.com/v1'
24
+ @@verify_ssl_certs = true
25
+
26
+ module Util
27
+ def self.objects_to_ids(h)
28
+ case h
29
+ when APIResource
30
+ h.id
31
+ when Hash
32
+ res = {}
33
+ h.each { |k, v| res[k] = objects_to_ids(v) }
34
+ res
35
+ when Array
36
+ h.map { |v| objects_to_ids(v) }
37
+ else
38
+ h
39
+ end
40
+ end
41
+
42
+ def self.convert_to_stripe_object(resp, api_key)
43
+ types = {
44
+ 'charge' => Charge,
45
+ 'customer' => Customer,
46
+ 'invoiceitem' => InvoiceItem,
47
+ 'invoice' => Invoice
48
+ }
49
+ case resp
50
+ when Array
51
+ resp.map { |i| convert_to_stripe_object(i, api_key) }
52
+ when Hash
53
+ # Try converting to a known object class. If none available, fall back to generic APIResource
54
+ if klass_name = resp[:object]
55
+ klass = types[klass_name]
56
+ end
57
+ klass ||= StripeObject
58
+ klass.construct_from(resp, api_key)
59
+ else
60
+ resp
61
+ end
62
+ end
63
+
64
+ def self.file_readable(file)
65
+ begin
66
+ File.open(file) { |f| }
67
+ rescue
68
+ false
69
+ else
70
+ true
71
+ end
72
+ end
73
+ end
74
+
75
+ module APIOperations
76
+ module Create
77
+ module ClassMethods
78
+ def create(params={}, api_key=nil)
79
+ response, api_key = Stripe.request(:post, self.url, api_key, params)
80
+ Util.convert_to_stripe_object(response, api_key)
81
+ end
82
+ end
83
+ def self.included(base)
84
+ base.extend(ClassMethods)
85
+ end
86
+ end
87
+
88
+ module Update
89
+ def save
90
+ if @unsaved_values.length > 0
91
+ values = {}
92
+ @unsaved_values.each { |k| values[k] = @values[k] }
93
+ response, api_key = Stripe.request(:post, url, @api_key, values)
94
+ refresh_from(response, api_key)
95
+ end
96
+ self
97
+ end
98
+ end
99
+
100
+ module Delete
101
+ def delete
102
+ response, api_key = Stripe.request(:delete, url, @api_key)
103
+ refresh_from(response, api_key)
104
+ self
105
+ end
106
+ end
107
+
108
+ module List
109
+ module ClassMethods
110
+ def all(filters={}, api_key=nil)
111
+ response, api_key = Stripe.request(:get, url, api_key, filters)
112
+ Util.convert_to_stripe_object(response, api_key)
113
+ end
114
+ end
115
+
116
+ def self.included(base)
117
+ base.extend(ClassMethods)
118
+ end
119
+ end
120
+ end
121
+
122
+ class StripeObject
123
+ attr_accessor :api_key
124
+ @@permanent_attributes = Set.new([:api_key])
125
+ @@ignored_attributes = Set.new([:id, :api_key, :object])
126
+
127
+ # The default :id method is deprecated and isn't useful to us
128
+ if method_defined?(:id)
129
+ undef :id
130
+ end
131
+
132
+ def initialize(id=nil, api_key=nil)
133
+ @api_key = api_key
134
+ @values = {}
135
+ # This really belongs in APIResource, but not putting it there allows us
136
+ # to have a unified inspect method
137
+ @unsaved_values = Set.new
138
+ @transient_values = Set.new
139
+ self.id = id if id
140
+ end
141
+
142
+ def self.construct_from(values, api_key=nil)
143
+ obj = self.new(values[:id], api_key)
144
+ obj.refresh_from(values, api_key)
145
+ obj
146
+ end
147
+
148
+ def to_s(*args); inspect(*args); end
149
+ def inspect(verbose=false, nested=false)
150
+ str = ["#<#{self.class}"]
151
+ if (desc = ident.compact).length > 0
152
+ str << "[#{desc.join(', ')}]"
153
+ end
154
+
155
+ if !verbose and nested
156
+ str << ' ...'
157
+ else
158
+ content = @values.keys.sort { |a, b| a.to_s <=> b.to_s }.map do |k|
159
+ if @@ignored_attributes.include?(k)
160
+ nil
161
+ else
162
+ v = @values[k]
163
+ v = v.kind_of?(StripeObject) ? v.inspect(verbose, true) : v.inspect
164
+ v = @unsaved_values.include?(k) ? "#{v} (unsaved)" : v
165
+ "#{k}=#{v}"
166
+ end
167
+ end.compact.join(', ')
168
+
169
+ str << ' '
170
+ if content.length > 0
171
+ str << content
172
+ else
173
+ str << '(no attributes)'
174
+ end
175
+ end
176
+
177
+ str << ">"
178
+ str.join
179
+ end
180
+
181
+ def refresh_from(values, api_key, partial=false)
182
+ @api_key = api_key
183
+
184
+ removed = partial ? Set.new : Set.new(@values.keys - values.keys)
185
+ added = Set.new(values.keys - @values.keys)
186
+ # Wipe old state before setting new. This is useful for e.g. updating a
187
+ # customer, where there is no persistent card parameter. Mark those values
188
+ # which don't persist as transient
189
+
190
+ instance_eval do
191
+ remove_accessors(removed)
192
+ add_accessors(added)
193
+ end
194
+ removed.each do |k|
195
+ @values.delete(k)
196
+ @transient_values.add(k)
197
+ @unsaved_values.delete(k)
198
+ end
199
+ values.each do |k, v|
200
+ @values[k] = Util.convert_to_stripe_object(v, api_key)
201
+ @transient_values.delete(k)
202
+ @unsaved_values.delete(k)
203
+ end
204
+ end
205
+
206
+ def [](k)
207
+ k = k.to_sym if k.kind_of?(String)
208
+ @values[k]
209
+ end
210
+ def []=(k, v)
211
+ send(:"#{k}=", v)
212
+ end
213
+ def keys; @values.keys; end
214
+ def values; @values.values; end
215
+ def to_json(*a); @values.to_json(*a); end
216
+
217
+ protected
218
+
219
+ def ident
220
+ [@values[:object], @values[:id]]
221
+ end
222
+
223
+ def metaclass
224
+ class << self; self; end
225
+ end
226
+
227
+ def remove_accessors(keys)
228
+ metaclass.instance_eval do
229
+ keys.each do |k|
230
+ next if @@permanent_attributes.include?(k)
231
+ k_eq = :"#{k}="
232
+ remove_method(k) if method_defined?(k)
233
+ remove_method(k_eq) if method_defined?(k_eq)
234
+ end
235
+ end
236
+ end
237
+
238
+ def add_accessors(keys)
239
+ metaclass.instance_eval do
240
+ keys.each do |k|
241
+ next if @@permanent_attributes.include?(k)
242
+ k_eq = :"#{k}="
243
+ define_method(k) { @values[k] }
244
+ define_method(k_eq) do |v|
245
+ @values[k] = v
246
+ @unsaved_values.add(k) unless @@ignored_attributes.include?(k)
247
+ end
248
+ end
249
+ end
250
+ end
251
+
252
+ def method_missing(name, *args)
253
+ # TODO: only allow setting in updateable classes.
254
+ if name.to_s.end_with?('=')
255
+ attr = name.to_s[0...-1].to_sym
256
+ @values[attr] = args[0]
257
+ @unsaved_values.add(attr) unless @@ignored_attributes.include?(attr)
258
+ add_accessors([attr])
259
+ return
260
+ else
261
+ return @values[name] if @values.has_key?(name)
262
+ end
263
+
264
+ begin
265
+ super
266
+ rescue NoMethodError => e
267
+ if @transient_values.include?(name)
268
+ raise NoMethodError.new(e.message + ". HINT: The '#{name}' attribute was set in the past, however. It was then wiped when refreshing the object with the result returned by Stripe's API, probably as a result of a save(). The attributes currently available on this object are: #{@values.keys.join(', ')}")
269
+ else
270
+ raise
271
+ end
272
+ end
273
+ end
274
+ end
275
+
276
+ class APIResource < StripeObject
277
+ def self.url
278
+ if self == APIResource
279
+ raise NotImplementedError.new("APIResource is an abstract class. You should perform actions on its subclasses (Charge, Customer, etc.)")
280
+ end
281
+ shortname = self.name.split('::')[-1]
282
+ "/#{CGI.escape(shortname.downcase)}s"
283
+ end
284
+ def url
285
+ id = self['id']
286
+ raise InvalidRequestError.new("Could not determine which URL to request: #{self.class} instance has invalid ID: #{id.inspect}", 'id') unless id
287
+ "#{self.class.url}/#{CGI.escape(id)}"
288
+ end
289
+
290
+ def refresh
291
+ response, api_key = Stripe.request(:get, url, @api_key)
292
+ refresh_from(response, api_key)
293
+ self
294
+ end
295
+
296
+ def self.retrieve(id, api_key=nil)
297
+ instance = self.new(id, api_key)
298
+ instance.refresh
299
+ instance
300
+ end
301
+
302
+ protected
303
+
304
+ def ident
305
+ [@values[:id]]
306
+ end
307
+ end
308
+
309
+ class Customer < APIResource
310
+ include Stripe::APIOperations::Create
311
+ include Stripe::APIOperations::Delete
312
+ include Stripe::APIOperations::Update
313
+ include Stripe::APIOperations::List
314
+
315
+ def add_invoice_item(params)
316
+ InvoiceItem.create(params.merge(:customer => id), @api_key)
317
+ end
318
+
319
+ def invoices
320
+ Invoice.all({ :customer => id }, @api_key)
321
+ end
322
+
323
+ def invoice_items
324
+ InvoiceItem.all({ :customer => id }, @api_key)
325
+ end
326
+
327
+ def charges
328
+ Charge.all({ :customer => id }, @api_key)
329
+ end
330
+
331
+ def cancel_subscription(params={})
332
+ response, api_key = Stripe.request(:delete, subscription_url, @api_key, params)
333
+ refresh_from({ :subscription => response }, api_key, true)
334
+ subscription
335
+ end
336
+
337
+ def update_subscription(params)
338
+ response, api_key = Stripe.request(:post, subscription_url, @api_key, params)
339
+ refresh_from({ :subscription => response }, api_key, true)
340
+ subscription
341
+ end
342
+
343
+ private
344
+
345
+ def subscription_url
346
+ url + '/subscription'
347
+ end
348
+ end
349
+
350
+ class Invoice < APIResource
351
+ include Stripe::APIOperations::List
352
+
353
+ def self.upcoming(params)
354
+ response, api_key = Stripe.request(:get, upcoming_url, @api_key, params)
355
+ Util.convert_to_stripe_object(response, api_key)
356
+ end
357
+
358
+ private
359
+
360
+ def self.upcoming_url
361
+ url + '/upcoming'
362
+ end
363
+ end
364
+
365
+ class InvoiceItem < APIResource
366
+ include Stripe::APIOperations::List
367
+ include Stripe::APIOperations::Create
368
+ include Stripe::APIOperations::Delete
369
+ include Stripe::APIOperations::Update
370
+ end
371
+
372
+ class Charge < APIResource
373
+ include Stripe::APIOperations::List
374
+ include Stripe::APIOperations::Create
375
+
376
+ def refund(params={})
377
+ response, api_key = Stripe.request(:post, refund_url, @api_key, params)
378
+ refresh_from(response, api_key)
379
+ self
380
+ end
381
+
382
+ def capture(params={})
383
+ response, api_key = Stripe.request(:post, capture_url, @api_key, params)
384
+ refresh_from(response, api_key)
385
+ self
386
+ end
387
+
388
+ private
389
+
390
+ def refund_url
391
+ url + '/refund'
392
+ end
393
+
394
+ def capture_url
395
+ url + '/capture'
396
+ end
397
+ end
398
+
399
+ class StripeError < StandardError; end
400
+ class APIError < StripeError; end
401
+ class APIConnectionError < StripeError; end
402
+ class CardError < StripeError
403
+ attr_reader :param, :code
404
+
405
+ def initialize(message, param, code)
406
+ super(message)
407
+ @param = param
408
+ @code = code
409
+ end
410
+ end
411
+ class InvalidRequestError < StripeError
412
+ attr_accessor :param
413
+
414
+ def initialize(message, param)
415
+ super(message)
416
+ @param = param
417
+ end
418
+ end
419
+ class AuthenticationError < StripeError; end
420
+
421
+ def self.api_url(url=''); @@api_base + url; end
422
+ def self.api_key=(api_key); @@api_key = api_key; end
423
+ def self.api_key; @@api_key; end
424
+ def self.api_base=(api_base); @@api_base = api_base; end
425
+ def self.api_base; @@api_base; end
426
+ def self.verify_ssl_certs=(verify); @@verify_ssl_certs = verify; end
427
+ def self.verify_ssl_certs; @@verify_ssl_certs; end
428
+
429
+ def self.request(method, url, api_key, params=nil, headers={})
430
+ api_key ||= @@api_key
431
+ raise AuthenticationError.new('No API key provided. (HINT: set your API key using "Stripe.api_key = <API-KEY>". You can generate API keys from the Stripe web interface. See https://stripe.com/api for details, or email support@stripe.com if you have any questions.)') unless api_key
432
+
433
+ if !verify_ssl_certs
434
+ unless @no_verify
435
+ $stderr.puts "WARNING: Running without SSL cert verification. Execute 'Stripe.verify_ssl_certs = true' to enable verification."
436
+ @no_verify = true
437
+ end
438
+ ssl_opts = { :verify_ssl => false }
439
+ elsif !Util.file_readable(@@ssl_bundle_path)
440
+ unless @no_bundle
441
+ $stderr.puts "WARNING: Running without SSL cert verification because #{@@ssl_bundle_path} isn't readable"
442
+ @no_bundle = true
443
+ end
444
+ ssl_opts = { :verify_ssl => false }
445
+ else
446
+ ssl_opts = {
447
+ :verify_ssl => OpenSSL::SSL::VERIFY_PEER,
448
+ :ssl_ca_file => @@ssl_bundle_path
449
+ }
450
+ end
451
+ uname = (@@uname ||= RUBY_PLATFORM =~ /linux|darwin/i ? `uname -a 2>/dev/null`.strip : nil)
452
+ lang_version = "#{RUBY_VERSION} p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE})"
453
+ ua = {
454
+ :bindings_version => Stripe::VERSION,
455
+ :lang => 'ruby',
456
+ :lang_version => lang_version,
457
+ :platform => RUBY_PLATFORM,
458
+ :publisher => 'stripe',
459
+ :uname => uname
460
+ }
461
+
462
+ params = Util.objects_to_ids(params)
463
+ case method.to_s.downcase.to_sym
464
+ when :get, :head, :delete
465
+ # Make params into GET parameters
466
+ headers = { :params => params }.merge(headers)
467
+ payload = nil
468
+ else
469
+ payload = params
470
+ end
471
+
472
+ headers = {
473
+ :x_stripe_client_user_agent => JSON.dump(ua),
474
+ :user_agent => "Stripe/v1 RubyBindings/#{Stripe::VERSION}"
475
+ }.merge(headers)
476
+ opts = {
477
+ :method => method,
478
+ :url => self.api_url(url),
479
+ :user => api_key,
480
+ :headers => headers,
481
+ :open_timeout => 30,
482
+ :payload => payload,
483
+ :timeout => 80
484
+ }.merge(ssl_opts)
485
+
486
+ begin
487
+ response = execute_request(opts)
488
+ rescue SocketError => e
489
+ self.handle_restclient_error(e)
490
+ rescue NoMethodError => e
491
+ # Work around RestClient bug
492
+ if e.message =~ /\WRequestFailed\W/
493
+ e = APIConnectionError.new('Unexpected HTTP response code')
494
+ self.handle_restclient_error(e)
495
+ else
496
+ raise
497
+ end
498
+ rescue RestClient::ExceptionWithResponse => e
499
+ if rcode = e.http_code and rbody = e.http_body
500
+ self.handle_api_error(rcode, rbody)
501
+ else
502
+ self.handle_restclient_error(e)
503
+ end
504
+ rescue RestClient::Exception, Errno::ECONNREFUSED => e
505
+ self.handle_restclient_error(e)
506
+ end
507
+
508
+ rbody = response.body
509
+ rcode = response.code
510
+ begin
511
+ resp = JSON.parse(rbody, :symbolize_names => true)
512
+ rescue JSON::ParseError
513
+ raise APIError.new("Invalid response object from API: #{rbody.inspect} (HTTP response code was #{rcode})")
514
+ end
515
+
516
+ [resp, api_key]
517
+ end
518
+
519
+ private
520
+
521
+ def self.execute_request(opts)
522
+ RestClient::Request.execute(opts)
523
+ end
524
+
525
+ def self.handle_api_error(rcode, rbody)
526
+ begin
527
+ error_obj = JSON.parse(rbody, :symbolize_names => true)
528
+ error = error_obj[:error] or raise StripeError.new
529
+ rescue JSON::ParserError, StripeError
530
+ raise APIError.new("Invalid response object from API: #{rbody.inspect} (HTTP response code was #{rcode})")
531
+ end
532
+
533
+ case rcode
534
+ when 400, 404 then
535
+ raise invalid_request_error(error)
536
+ when 401
537
+ raise authentication_error(error)
538
+ when 402
539
+ raise card_error(error)
540
+ else
541
+ raise api_error(error)
542
+ end
543
+ end
544
+
545
+ def self.invalid_request_error(error); InvalidRequestError.new(error[:message], error[:param]); end
546
+ def self.authentication_error(error); AuthenticationError.new(error[:message]); end
547
+ def self.card_error(error); CardError.new(error[:message], error[:param], error[:code]); end
548
+ def self.api_error(error); APIError.new(error[:message]); end
549
+
550
+ def self.handle_restclient_error(e)
551
+ case e
552
+ when RestClient::ServerBrokeConnection, RestClient::RequestTimeout
553
+ message = "Could not connect to Stripe (#{@@api_base}). Please check your internet connection and try again. If this problem persists, you should check Stripe's service status at https://twitter.com/stripe, or let us know at support@stripe.com."
554
+ when RestClient::SSLCertificateNotVerified
555
+ message = "Could not verify Stripe's SSL certificate. Please make sure that your network is not intercepting certificates. (Try going to https://api.stripe.com/v1 in your browser.) If this problem persists, let us know at support@stripe.com."
556
+ when SocketError
557
+ message = "Unexpected error communicating when trying to connect to Stripe. HINT: You may be seeing this message because your DNS is not working. To check, try running 'host stripe.com' from the command line."
558
+ else
559
+ message = "Unexpected error communicating with Stripe. If this problem persists, let us know at support@stripe.com."
560
+ end
561
+ message += "\n\n(Network error: #{e.message})"
562
+ raise APIConnectionError.new(message)
563
+ end
564
+ end