stripe 1.30.3 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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