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
@@ -12,6 +12,7 @@ module Stripe
12
12
  def initialize(id=nil, opts={})
13
13
  id, @retrieve_params = Util.normalize_id(id)
14
14
  @opts = Util.normalize_opts(opts)
15
+ @original_values = {}
15
16
  @values = {}
16
17
  # This really belongs in APIResource, but not putting it there allows us
17
18
  # to have a unified inspect method
@@ -31,7 +32,7 @@ module Stripe
31
32
  # considered to be equal if they have the same set of values and each one
32
33
  # of those values is the same.
33
34
  def ==(other)
34
- @values == other.instance_variable_get(:@values)
35
+ other.is_a?(StripeObject) && @values == other.instance_variable_get(:@values)
35
36
  end
36
37
 
37
38
  # Indicates whether or not the resource has been deleted on the server.
@@ -42,7 +43,7 @@ module Stripe
42
43
  end
43
44
 
44
45
  def to_s(*args)
45
- JSON.pretty_generate(@values)
46
+ JSON.pretty_generate(to_hash)
46
47
  end
47
48
 
48
49
  def inspect
@@ -71,13 +72,22 @@ module Stripe
71
72
  #
72
73
  # * +values+ - Hash of values to use to update the current attributes of
73
74
  # the object.
75
+ # * +opts+ - Options for +StripeObject+ like an API key that will be reused
76
+ # on subsequent API calls.
74
77
  #
75
78
  # ==== Options
76
79
  #
77
- # * +:opts+ Options for StripeObject like an API key.
78
- def update_attributes(values, opts = {})
80
+ # * +:dirty+ - Whether values should be initiated as "dirty" (unsaved) and
81
+ # which applies only to new StripeObjects being initiated under this
82
+ # StripeObject. Defaults to true.
83
+ def update_attributes(values, opts = {}, method_options = {})
84
+ # Default to true. TODO: Convert to optional arguments after we're off
85
+ # 1.9 which will make this quite a bit more clear.
86
+ dirty = method_options.fetch(:dirty, true)
79
87
  values.each do |k, v|
88
+ add_accessors([k], values) unless metaclass.method_defined?(k.to_sym)
80
89
  @values[k] = Util.convert_to_stripe_object(v, opts)
90
+ dirty_value!(@values[k]) if dirty
81
91
  @unsaved_values.add(k)
82
92
  end
83
93
  end
@@ -135,125 +145,109 @@ module Stripe
135
145
  construct_from(values, opts)
136
146
  end
137
147
 
138
- if RUBY_VERSION < '1.9.2'
139
- def respond_to?(symbol)
140
- @values.has_key?(symbol) || super
148
+ # Sets all keys within the StripeObject as unsaved so that they will be
149
+ # included with an update when #serialize_params is called. This method is
150
+ # also recursive, so any StripeObjects contained as values or which are
151
+ # values in a tenant array are also marked as dirty.
152
+ def dirty!
153
+ @unsaved_values = Set.new(@values.keys)
154
+ @values.each do |k, v|
155
+ dirty_value!(v)
141
156
  end
142
157
  end
143
158
 
144
- def serialize_nested_object(key)
145
- new_value = @values[key]
146
- if new_value.is_a?(APIResource)
147
- return {}
148
- end
149
-
150
- if @unsaved_values.include?(key)
151
- # the object has been reassigned
152
- # e.g. as object.key = {foo => bar}
153
- update = new_value
154
- new_keys = update.keys.map(&:to_sym)
155
-
156
- # remove keys at the server, but not known locally
157
- if @original_values[key]
158
- keys_to_unset = @original_values[key].keys - new_keys
159
- keys_to_unset.each {|key| update[key] = ''}
159
+ def serialize_params(options = {})
160
+ update_hash = {}
161
+
162
+ @values.each do |k, v|
163
+ # There are a few reasons that we may want to add in a parameter for
164
+ # update:
165
+ #
166
+ # 1. The `force` option has been set.
167
+ # 2. We know that it was modified.
168
+ # 3. Its value is a StripeObject. A StripeObject may contain modified
169
+ # values within in that its parent StripeObject doesn't know about.
170
+ #
171
+ unsaved = @unsaved_values.include?(k)
172
+ if options[:force] || unsaved || v.is_a?(StripeObject)
173
+ update_hash[k.to_sym] =
174
+ serialize_params_value(@values[k], @original_values[k], unsaved, options[:force])
160
175
  end
161
-
162
- update
163
- else
164
- # can be serialized normally
165
- self.class.serialize_params(new_value)
166
176
  end
167
- end
168
-
169
- def self.serialize_params(obj, original_value=nil)
170
- case obj
171
- when nil
172
- ''
173
- when Array
174
- update = obj.map { |v| serialize_params(v) }
175
- if original_value != update
176
- update
177
- else
178
- nil
179
- end
180
- when StripeObject
181
- unsaved_keys = obj.instance_variable_get(:@unsaved_values)
182
- obj_values = obj.instance_variable_get(:@values)
183
- update_hash = {}
184
177
 
185
- unsaved_keys.each do |k|
186
- update_hash[k] = serialize_params(obj_values[k])
187
- end
178
+ # a `nil` that makes it out of `#serialize_params_value` signals an empty
179
+ # value that we shouldn't appear in the serialized form of the object
180
+ update_hash.reject! { |_, v| v == nil }
188
181
 
189
- obj_values.each do |k, v|
190
- if v.is_a?(Array)
191
- original_value = obj.instance_variable_get(:@original_values)[k]
192
-
193
- # the conditional here tests whether the old and new values are
194
- # different (and therefore needs an update), or the same (meaning
195
- # we can leave it out of the request)
196
- if updated = serialize_params(v, original_value)
197
- update_hash[k] = updated
198
- else
199
- update_hash.delete(k)
200
- end
201
- elsif v.is_a?(StripeObject) || v.is_a?(Hash)
202
- update_hash[k] = obj.serialize_nested_object(k)
203
- end
204
- end
182
+ update_hash
183
+ end
205
184
 
206
- update_hash
207
- else
208
- obj
185
+ class << self
186
+ # This class method has been deprecated in favor of the instance method
187
+ # of the same name.
188
+ def serialize_params(obj, options = {})
189
+ obj.serialize_params(options)
209
190
  end
191
+ extend Gem::Deprecate
192
+ deprecate :serialize_params, "#serialize_params", 2016, 9
210
193
  end
211
194
 
212
195
  protected
213
196
 
214
- def metaclass
215
- class << self; self; end
197
+ # A protected field is one that doesn't get an accessor assigned to it
198
+ # (i.e. `obj.public = ...`) and one which is not allowed to be updated via
199
+ # the class level `Model.update(id, { ... })`.
200
+ def self.protected_fields
201
+ []
216
202
  end
217
203
 
218
- def protected_fields
219
- []
204
+ def metaclass
205
+ class << self; self; end
220
206
  end
221
207
 
222
208
  def remove_accessors(keys)
223
- f = protected_fields
209
+ # not available in the #instance_eval below
210
+ protected_fields = self.class.protected_fields
211
+
224
212
  metaclass.instance_eval do
225
213
  keys.each do |k|
226
- next if f.include?(k)
214
+ next if protected_fields.include?(k)
227
215
  next if @@permanent_attributes.include?(k)
228
- k_eq = :"#{k}="
229
- remove_method(k) if method_defined?(k)
230
- remove_method(k_eq) if method_defined?(k_eq)
216
+
217
+ # Remove methods for the accessor's reader and writer.
218
+ [k, :"#{k}=", :"#{k}?"].each do |method_name|
219
+ if method_defined?(method_name)
220
+ remove_method(method_name)
221
+ end
222
+ end
231
223
  end
232
224
  end
233
225
  end
234
226
 
235
227
  def add_accessors(keys, values)
236
- f = protected_fields
228
+ # not available in the #instance_eval below
229
+ protected_fields = self.class.protected_fields
230
+
237
231
  metaclass.instance_eval do
238
232
  keys.each do |k|
239
- next if f.include?(k)
233
+ next if protected_fields.include?(k)
240
234
  next if @@permanent_attributes.include?(k)
241
- k_eq = :"#{k}="
235
+
242
236
  define_method(k) { @values[k] }
243
- define_method(k_eq) do |v|
237
+ define_method(:"#{k}=") do |v|
244
238
  if v == ""
245
239
  raise ArgumentError.new(
246
- "You cannot set #{k} to an empty string." \
247
- "We interpret empty strings as nil in requests." \
248
- "You may set #{self}.#{k} = nil to delete the property.")
240
+ "You cannot set #{k} to an empty string. " \
241
+ "We interpret empty strings as nil in requests. " \
242
+ "You may set (object).#{k} = nil to delete the property.")
249
243
  end
250
- @values[k] = v
244
+ @values[k] = Util.convert_to_stripe_object(v, @opts)
245
+ dirty_value!(@values[k])
251
246
  @unsaved_values.add(k)
252
247
  end
253
248
 
254
249
  if [FalseClass, TrueClass].include?(values[k].class)
255
- k_bool = :"#{k}?"
256
- define_method(k_bool) { @values[k] }
250
+ define_method(:"#{k}?") { @values[k] }
257
251
  end
258
252
  end
259
253
  end
@@ -315,10 +309,8 @@ module Stripe
315
309
  # customer, where there is no persistent card parameter. Mark those values
316
310
  # which don't persist as transient
317
311
 
318
- instance_eval do
319
- remove_accessors(removed)
320
- add_accessors(added, values)
321
- end
312
+ remove_accessors(removed)
313
+ add_accessors(added, values)
322
314
 
323
315
  removed.each do |k|
324
316
  @values.delete(k)
@@ -326,7 +318,7 @@ module Stripe
326
318
  @unsaved_values.delete(k)
327
319
  end
328
320
 
329
- update_attributes(values, opts)
321
+ update_attributes(values, opts, :dirty => false)
330
322
  values.each do |k, _|
331
323
  @transient_values.delete(k)
332
324
  @unsaved_values.delete(k)
@@ -334,5 +326,89 @@ module Stripe
334
326
 
335
327
  self
336
328
  end
329
+
330
+ def serialize_params_value(value, original, unsaved, force)
331
+ case true
332
+ when value == nil
333
+ ''
334
+
335
+ # The logic here is that essentially any object embedded in another
336
+ # object that had a `type` is actually an API resource of a different
337
+ # type that's been included in the response. These other resources must
338
+ # be updated from their proper endpoints, and therefore they are not
339
+ # included when serializing even if they've been modified.
340
+ #
341
+ # There are _some_ known exceptions though. For example, to save on API
342
+ # calls it's sometimes desirable to update a customer's default source by
343
+ # setting a new card (or other) object with `#source=` and then saving
344
+ # the customer. The `#save_with_parent` flag to override the default
345
+ # behavior allows us to handle these exceptions.
346
+ when value.is_a?(APIResource) && !value.save_with_parent
347
+ nil
348
+
349
+ when value.is_a?(Array)
350
+ update = value.map { |v| serialize_params_value(v, nil, true, force) }
351
+
352
+ # This prevents an array that's unchanged from being resent.
353
+ if update != serialize_params_value(original, nil, true, force)
354
+ update
355
+ else
356
+ nil
357
+ end
358
+
359
+ # Handle a Hash for now, but in the long run we should be able to
360
+ # eliminate all places where hashes are stored as values internally by
361
+ # making sure any time one is set, we convert it to a StripeObject. This
362
+ # will simplify our model by making data within an object more
363
+ # consistent.
364
+ #
365
+ # For now, you can still run into a hash if someone appends one to an
366
+ # existing array being held by a StripeObject. This could happen for
367
+ # example by appending a new hash onto `additional_owners` for an
368
+ # account.
369
+ when value.is_a?(Hash)
370
+ Util.convert_to_stripe_object(value, @opts).serialize_params
371
+
372
+ when value.is_a?(StripeObject)
373
+ update = value.serialize_params(:force => force)
374
+
375
+ # If the entire object was replaced, then we need blank each field of
376
+ # the old object that held a value. The new serialized values will
377
+ # override any of these empty values.
378
+ update = empty_values(original).merge(update) if original && unsaved
379
+
380
+ update
381
+
382
+ else
383
+ value
384
+ end
385
+ end
386
+
387
+ private
388
+
389
+ def dirty_value!(value)
390
+ case value
391
+ when Array
392
+ value.map { |v| dirty_value!(v) }
393
+ when StripeObject
394
+ value.dirty!
395
+ end
396
+ end
397
+
398
+ # Returns a hash of empty values for all the values that are in the given
399
+ # StripeObject.
400
+ def empty_values(obj)
401
+ values = case obj
402
+ when Hash then obj
403
+ when StripeObject then obj.instance_variable_get(:@values)
404
+ else
405
+ raise ArgumentError, "#empty_values got unexpected object type: #{obj.class.name}"
406
+ end
407
+
408
+ values.inject({}) do |update, (k, _)|
409
+ update[k] = ''
410
+ update
411
+ end
412
+ end
337
413
  end
338
414
  end
@@ -0,0 +1,48 @@
1
+ module Stripe
2
+ # StripeResponse encapsulates some vitals of a response that came back from
3
+ # the Stripe API.
4
+ class StripeResponse
5
+ # The data contained by the HTTP body of the response deserialized from
6
+ # JSON.
7
+ attr_accessor :data
8
+
9
+ # The raw HTTP body of the response.
10
+ attr_accessor :http_body
11
+
12
+ # A Hash of the HTTP headers of the response.
13
+ attr_accessor :http_headers
14
+
15
+ # The integer HTTP status code of the response.
16
+ attr_accessor :http_status
17
+
18
+ # The Stripe request ID of the response.
19
+ attr_accessor :request_id
20
+
21
+ # Initializes a StripeResponse object from a Hash like the kind returned as
22
+ # part of a Faraday exception.
23
+ #
24
+ # This may throw JSON::ParserError if the response body is not valid JSON.
25
+ def self.from_faraday_hash(http_resp)
26
+ resp = StripeResponse.new
27
+ resp.data = JSON.parse(http_resp[:body], symbolize_names: true)
28
+ resp.http_body = http_resp[:body]
29
+ resp.http_headers = http_resp[:headers]
30
+ resp.http_status = http_resp[:status]
31
+ resp.request_id = http_resp[:headers]["Request-Id"]
32
+ resp
33
+ end
34
+
35
+ # Initializes a StripeResponse object from a Faraday HTTP response object.
36
+ #
37
+ # This may throw JSON::ParserError if the response body is not valid JSON.
38
+ def self.from_faraday_response(http_resp)
39
+ resp = StripeResponse.new
40
+ resp.data = JSON.parse(http_resp.body, symbolize_names: true)
41
+ resp.http_body = http_resp.body
42
+ resp.http_headers = http_resp.headers
43
+ resp.http_status = http_resp.status
44
+ resp.request_id = http_resp.headers["Request-Id"]
45
+ resp
46
+ end
47
+ end
48
+ end
@@ -1,25 +1,31 @@
1
1
  module Stripe
2
2
  class Subscription < APIResource
3
- include Stripe::APIOperations::Update
3
+ extend Stripe::APIOperations::List
4
+ extend Stripe::APIOperations::Create
5
+ include Stripe::APIOperations::Save
4
6
  include Stripe::APIOperations::Delete
5
7
 
6
- def url
7
- "#{Customer.url}/#{CGI.escape(customer)}/subscriptions/#{CGI.escape(id)}"
8
+ save_nested_resource :source
9
+
10
+ def delete_discount
11
+ _, opts = request(:delete, discount_url)
12
+ initialize_from({ :discount => nil }, opts, true)
8
13
  end
9
14
 
10
- def self.retrieve(id, opts=nil)
11
- raise NotImplementedError.new("Subscriptions cannot be retrieved without a customer ID. Retrieve a subscription using customer.subscriptions.retrieve('subscription_id')")
15
+ def self.update(id, params={}, opts={})
16
+ params[:items] = Util.array_to_hash(params[:items]) if params[:items]
17
+ super(id, params, opts)
12
18
  end
13
19
 
14
- def delete_discount
15
- response, opts = request(:delete, discount_url)
16
- initialize_from({ :discount => nil }, opts, true)
20
+ def self.create(params={}, opts={})
21
+ params[:items] = Util.array_to_hash(params[:items]) if params[:items]
22
+ super(params, opts)
17
23
  end
18
24
 
19
25
  private
20
26
 
21
27
  def discount_url
22
- url + '/discount'
28
+ resource_url + '/discount'
23
29
  end
24
30
  end
25
31
  end
@@ -0,0 +1,12 @@
1
+ module Stripe
2
+ class SubscriptionItem < APIResource
3
+ extend Stripe::APIOperations::Create
4
+ include Stripe::APIOperations::Delete
5
+ extend Stripe::APIOperations::List
6
+ include Stripe::APIOperations::Save
7
+
8
+ def self.resource_url
9
+ '/v1/subscription_items'
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,9 @@
1
+ module Stripe
2
+ class ThreeDSecure < APIResource
3
+ extend Stripe::APIOperations::Create
4
+
5
+ def self.resource_url
6
+ "/v1/3d_secure"
7
+ end
8
+ end
9
+ end
@@ -2,16 +2,15 @@ module Stripe
2
2
  class Transfer < APIResource
3
3
  extend Stripe::APIOperations::List
4
4
  extend Stripe::APIOperations::Create
5
- include Stripe::APIOperations::Update
5
+ include Stripe::APIOperations::Save
6
6
 
7
7
  def cancel
8
- response, api_key = Stripe.request(:post, cancel_url, @api_key)
9
- initialize_from(response, api_key)
8
+ resp, api_key = self.request(:post, cancel_url)
9
+ initialize_from(resp.data, api_key)
10
10
  end
11
11
 
12
12
  def cancel_url
13
- url + '/cancel'
13
+ resource_url + '/cancel'
14
14
  end
15
-
16
15
  end
17
16
  end
data/lib/stripe/util.rb CHANGED
@@ -23,45 +23,63 @@ module Stripe
23
23
  'list' => ListObject,
24
24
 
25
25
  # business objects
26
- 'account' => Account,
27
- 'application_fee' => ApplicationFee,
28
- 'balance' => Balance,
29
- 'balance_transaction' => BalanceTransaction,
30
- 'bank_account' => BankAccount,
31
- 'card' => Card,
32
- 'charge' => Charge,
33
- 'coupon' => Coupon,
34
- 'customer' => Customer,
35
- 'event' => Event,
36
- 'fee_refund' => ApplicationFeeRefund,
37
- 'invoiceitem' => InvoiceItem,
38
- 'invoice' => Invoice,
39
- 'plan' => Plan,
40
- 'recipient' => Recipient,
41
- 'refund' => Refund,
42
- 'subscription' => Subscription,
43
- 'file_upload' => FileUpload,
44
- 'token' => Token,
45
- 'transfer' => Transfer,
46
- 'transfer_reversal' => Reversal,
47
- 'bitcoin_receiver' => BitcoinReceiver,
48
- 'bitcoin_transaction' => BitcoinTransaction,
49
- 'dispute' => Dispute,
50
- 'product' => Product,
51
- 'sku' => SKU,
52
- 'order' => Order,
26
+ 'account' => Account,
27
+ 'alipay_account' => AlipayAccount,
28
+ 'apple_pay_domain' => ApplePayDomain,
29
+ 'application_fee' => ApplicationFee,
30
+ 'balance' => Balance,
31
+ 'balance_transaction' => BalanceTransaction,
32
+ 'bank_account' => BankAccount,
33
+ 'bitcoin_receiver' => BitcoinReceiver,
34
+ 'bitcoin_transaction' => BitcoinTransaction,
35
+ 'card' => Card,
36
+ 'charge' => Charge,
37
+ 'country_spec' => CountrySpec,
38
+ 'coupon' => Coupon,
39
+ 'customer' => Customer,
40
+ 'dispute' => Dispute,
41
+ 'event' => Event,
42
+ 'fee_refund' => ApplicationFeeRefund,
43
+ 'file_upload' => FileUpload,
44
+ 'invoice' => Invoice,
45
+ 'invoiceitem' => InvoiceItem,
46
+ 'order' => Order,
47
+ 'order_return' => OrderReturn,
48
+ 'plan' => Plan,
49
+ 'product' => Product,
50
+ 'recipient' => Recipient,
51
+ 'refund' => Refund,
52
+ 'sku' => SKU,
53
+ 'subscription' => Subscription,
54
+ 'subscription_item' => SubscriptionItem,
55
+ 'three_d_secure' => ThreeDSecure,
56
+ 'token' => Token,
57
+ 'transfer' => Transfer,
58
+ 'transfer_reversal' => Reversal,
53
59
  }
54
60
  end
55
61
 
56
- def self.convert_to_stripe_object(resp, opts)
57
- case resp
62
+ # Converts a hash of fields or an array of hashes into a +StripeObject+ or
63
+ # array of +StripeObject+s. These new objects will be created as a concrete
64
+ # type as dictated by their `object` field (e.g. an `object` value of
65
+ # `charge` would create an instance of +Charge+), but if `object` is not
66
+ # present or of an unknown type, the newly created instance will fall back
67
+ # to being a +StripeObject+.
68
+ #
69
+ # ==== Attributes
70
+ #
71
+ # * +data+ - Hash of fields and values to be converted into a StripeObject.
72
+ # * +opts+ - Options for +StripeObject+ like an API key that will be reused
73
+ # on subsequent API calls.
74
+ def self.convert_to_stripe_object(data, opts)
75
+ case data
58
76
  when Array
59
- resp.map { |i| convert_to_stripe_object(i, opts) }
77
+ data.map { |i| convert_to_stripe_object(i, opts) }
60
78
  when Hash
61
79
  # Try converting to a known object class. If none available, fall back to generic StripeObject
62
- object_classes.fetch(resp[:object], StripeObject).construct_from(resp, opts)
80
+ object_classes.fetch(data[:object], StripeObject).construct_from(data, opts)
63
81
  else
64
- resp
82
+ data
65
83
  end
66
84
  end
67
85
 
@@ -103,6 +121,20 @@ module Stripe
103
121
  map { |k,v| "#{url_encode(k)}=#{url_encode(v)}" }.join('&')
104
122
  end
105
123
 
124
+ # Transforms an array into a hash with integer keys. Used for a small
125
+ # number of API endpoints. If the argument is not an Array, return it
126
+ # unchanged. Example: [{foo: 'bar'}] => {"0" => {foo: "bar"}}
127
+ def self.array_to_hash(array)
128
+ case array
129
+ when Array
130
+ hash = {}
131
+ array.each_with_index { |v,i| hash[i.to_s] = v }
132
+ hash
133
+ else
134
+ array
135
+ end
136
+ end
137
+
106
138
  # Encodes a string in a way that makes it suitable for use in a set of
107
139
  # query parameters in a URI or in a set of form parameters in a request
108
140
  # body.
@@ -119,11 +151,12 @@ module Stripe
119
151
 
120
152
  # do not sort the final output because arrays (and arrays of hashes
121
153
  # especially) can be order sensitive, but do sort incoming parameters
122
- params.sort_by { |(k, v)| k.to_s }.each do |key, value|
154
+ params.each do |key, value|
123
155
  calculated_key = parent_key ? "#{parent_key}[#{key}]" : "#{key}"
124
156
  if value.is_a?(Hash)
125
157
  result += flatten_params(value, calculated_key)
126
158
  elsif value.is_a?(Array)
159
+ check_array_of_maps_start_keys!(value)
127
160
  result += flatten_params_array(value, calculated_key)
128
161
  else
129
162
  result << [calculated_key, value]
@@ -180,5 +213,44 @@ module Stripe
180
213
  raise TypeError.new("api_key must be a string") unless key.is_a?(String)
181
214
  key
182
215
  end
216
+
217
+ private
218
+
219
+ # We use a pretty janky version of form encoding (Rack's) that supports
220
+ # more complex data structures like maps and arrays through the use of
221
+ # specialized syntax. To encode an array of maps like:
222
+ #
223
+ # [{a: 1, b: 2}, {a: 3, b: 4}]
224
+ #
225
+ # We have to produce something that looks like this:
226
+ #
227
+ # arr[][a]=1&arr[][b]=2&arr[][a]=3&arr[][b]=4
228
+ #
229
+ # The only way for the server to recognize that this is a two item array is
230
+ # that it notices the repetition of element "a", so it's key that these
231
+ # repeated elements are encoded first.
232
+ #
233
+ # This method is invoked for any arrays being encoded and checks that if
234
+ # the array contains all non-empty maps, that each of those maps must start
235
+ # with the same key so that their boundaries can be properly encoded.
236
+ def self.check_array_of_maps_start_keys!(arr)
237
+ expected_key = nil
238
+ arr.each do |item|
239
+ return if !item.is_a?(Hash)
240
+ return if item.count == 0
241
+
242
+ first_key = item.first[0]
243
+
244
+ if expected_key
245
+ if expected_key != first_key
246
+ raise ArgumentError,
247
+ "All maps nested in an array should start with the same key " +
248
+ "(expected starting key '#{expected_key}', got '#{first_key}')"
249
+ end
250
+ else
251
+ expected_key = first_key
252
+ end
253
+ end
254
+ end
183
255
  end
184
256
  end
@@ -1,3 +1,3 @@
1
1
  module Stripe
2
- VERSION = '1.30.3'
2
+ VERSION = '2.0.0'
3
3
  end