stripe 1.31.0 → 1.58.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (103) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE.md +5 -0
  3. data/.travis.yml +2 -12
  4. data/Gemfile +29 -4
  5. data/History.txt +168 -0
  6. data/README.md +134 -0
  7. data/Rakefile +10 -0
  8. data/VERSION +1 -1
  9. data/bin/stripe-console +12 -5
  10. data/lib/data/ca-certificates.crt +3868 -5114
  11. data/lib/stripe/account.rb +41 -21
  12. data/lib/stripe/alipay_account.rb +20 -0
  13. data/lib/stripe/api_operations/create.rb +1 -1
  14. data/lib/stripe/api_operations/delete.rb +1 -1
  15. data/lib/stripe/api_operations/list.rb +1 -2
  16. data/lib/stripe/api_operations/save.rb +87 -0
  17. data/lib/stripe/api_resource.rb +37 -4
  18. data/lib/stripe/apple_pay_domain.rb +12 -0
  19. data/lib/stripe/application_fee.rb +8 -8
  20. data/lib/stripe/application_fee_refund.rb +7 -3
  21. data/lib/stripe/balance_transaction.rb +1 -1
  22. data/lib/stripe/bank_account.rb +9 -5
  23. data/lib/stripe/bitcoin_receiver.rb +6 -6
  24. data/lib/stripe/bitcoin_transaction.rb +1 -1
  25. data/lib/stripe/card.rb +9 -5
  26. data/lib/stripe/charge.rb +30 -12
  27. data/lib/stripe/country_spec.rb +9 -0
  28. data/lib/stripe/coupon.rb +1 -1
  29. data/lib/stripe/customer.rb +6 -4
  30. data/lib/stripe/dispute.rb +2 -2
  31. data/lib/stripe/errors.rb +82 -0
  32. data/lib/stripe/file_upload.rb +1 -1
  33. data/lib/stripe/invoice.rb +3 -3
  34. data/lib/stripe/invoice_item.rb +1 -1
  35. data/lib/stripe/list_object.rb +7 -6
  36. data/lib/stripe/order.rb +10 -2
  37. data/lib/stripe/order_return.rb +9 -0
  38. data/lib/stripe/plan.rb +1 -1
  39. data/lib/stripe/product.rb +2 -10
  40. data/lib/stripe/recipient.rb +1 -1
  41. data/lib/stripe/refund.rb +1 -1
  42. data/lib/stripe/reversal.rb +7 -3
  43. data/lib/stripe/singleton_api_resource.rb +3 -3
  44. data/lib/stripe/sku.rb +2 -2
  45. data/lib/stripe/source.rb +11 -0
  46. data/lib/stripe/stripe_object.rb +167 -91
  47. data/lib/stripe/subscription.rb +15 -9
  48. data/lib/stripe/subscription_item.rb +12 -0
  49. data/lib/stripe/three_d_secure.rb +9 -0
  50. data/lib/stripe/transfer.rb +3 -4
  51. data/lib/stripe/util.rb +100 -28
  52. data/lib/stripe/version.rb +1 -1
  53. data/lib/stripe.rb +283 -140
  54. data/stripe.gemspec +5 -18
  55. data/test/stripe/account_test.rb +55 -9
  56. data/test/stripe/alipay_account_test.rb +11 -0
  57. data/test/stripe/api_operations_test.rb +31 -0
  58. data/test/stripe/api_resource_test.rb +204 -10
  59. data/test/stripe/apple_pay_domain_test.rb +34 -0
  60. data/test/stripe/application_fee_test.rb +8 -5
  61. data/test/stripe/bitcoin_receiver_test.rb +2 -2
  62. data/test/stripe/charge_refund_test.rb +12 -0
  63. data/test/stripe/charge_test.rb +32 -4
  64. data/test/stripe/country_spec_test.rb +43 -0
  65. data/test/stripe/coupon_test.rb +9 -1
  66. data/test/stripe/customer_card_test.rb +2 -2
  67. data/test/stripe/customer_test.rb +24 -1
  68. data/test/stripe/dispute_test.rb +8 -0
  69. data/test/stripe/errors_test.rb +18 -0
  70. data/test/stripe/invoice_item_test.rb +19 -0
  71. data/test/stripe/invoice_test.rb +27 -1
  72. data/test/stripe/list_object_test.rb +36 -15
  73. data/test/stripe/order_return_test.rb +25 -0
  74. data/test/stripe/order_test.rb +21 -1
  75. data/test/stripe/plan_test.rb +31 -0
  76. data/test/stripe/product_test.rb +17 -7
  77. data/test/stripe/recipient_card_test.rb +2 -2
  78. data/test/stripe/recipient_test.rb +21 -0
  79. data/test/stripe/refund_test.rb +10 -1
  80. data/test/stripe/sku_test.rb +15 -6
  81. data/test/stripe/source_test.rb +83 -0
  82. data/test/stripe/stripe_object_test.rb +180 -11
  83. data/test/stripe/subscription_item_test.rb +76 -0
  84. data/test/stripe/subscription_test.rb +161 -37
  85. data/test/stripe/three_d_secure_test.rb +22 -0
  86. data/test/stripe/transfer_test.rb +8 -0
  87. data/test/stripe/util_test.rb +48 -16
  88. data/test/stripe_test.rb +58 -0
  89. data/test/test_data.rb +337 -27
  90. data/test/test_helper.rb +7 -3
  91. metadata +47 -133
  92. data/README.rdoc +0 -68
  93. data/gemfiles/default-with-activesupport.gemfile +0 -10
  94. data/gemfiles/json.gemfile +0 -12
  95. data/gemfiles/yajl.gemfile +0 -12
  96. data/lib/stripe/api_operations/update.rb +0 -58
  97. data/lib/stripe/errors/api_connection_error.rb +0 -4
  98. data/lib/stripe/errors/api_error.rb +0 -4
  99. data/lib/stripe/errors/authentication_error.rb +0 -4
  100. data/lib/stripe/errors/card_error.rb +0 -12
  101. data/lib/stripe/errors/invalid_request_error.rb +0 -11
  102. data/lib/stripe/errors/rate_limit_error.rb +0 -4
  103. data/lib/stripe/errors/stripe_error.rb +0 -26
@@ -0,0 +1,82 @@
1
+ module Stripe
2
+ # StripeError is the base error from which all other more specific Stripe
3
+ # errors derive.
4
+ class StripeError < StandardError
5
+ attr_reader :message
6
+ attr_reader :http_status
7
+ attr_reader :http_body
8
+ attr_reader :http_headers
9
+ attr_reader :request_id
10
+ attr_reader :json_body
11
+
12
+ def initialize(message=nil, http_status=nil, http_body=nil, json_body=nil,
13
+ http_headers=nil)
14
+ @message = message
15
+ @http_status = http_status
16
+ @http_body = http_body
17
+ @http_headers = http_headers || {}
18
+ @json_body = json_body
19
+ @request_id = @http_headers[:request_id]
20
+ end
21
+
22
+ def to_s
23
+ status_string = @http_status.nil? ? "" : "(Status #{@http_status}) "
24
+ id_string = @request_id.nil? ? "" : "(Request #{@request_id}) "
25
+ "#{status_string}#{id_string}#{@message}"
26
+ end
27
+ end
28
+
29
+ # AuthenticationError is raised when invalid credentials are used to connect
30
+ # to Stripe's servers.
31
+ class AuthenticationError < StripeError
32
+ end
33
+
34
+ # APIConnectionError is raised in the event that the SDK can't connect to
35
+ # Stripe's servers. That can be for a variety of different reasons from a
36
+ # downed network to a bad TLS certificate.
37
+ class APIConnectionError < StripeError
38
+ end
39
+
40
+ # APIError is a generic error that may be raised in cases where none of the
41
+ # other named errors cover the problem. It could also be raised in the case
42
+ # that a new error has been introduced in the API, but this version of the
43
+ # Ruby SDK doesn't know how to handle it.
44
+ class APIError < StripeError
45
+ end
46
+
47
+ # CardError is raised when a user enters a card that can't be charged for
48
+ # some reason.
49
+ class CardError < StripeError
50
+ attr_reader :param, :code
51
+
52
+ def initialize(message, param, code, http_status=nil, http_body=nil, json_body=nil,
53
+ http_headers=nil)
54
+ super(message, http_status, http_body, json_body, http_headers)
55
+ @param = param
56
+ @code = code
57
+ end
58
+ end
59
+
60
+ # InvalidRequestError is raised when a request is initiated with invalid
61
+ # parameters.
62
+ class InvalidRequestError < StripeError
63
+ attr_accessor :param
64
+
65
+ def initialize(message, param, http_status=nil, http_body=nil, json_body=nil,
66
+ http_headers=nil)
67
+ super(message, http_status, http_body, json_body, http_headers)
68
+ @param = param
69
+ end
70
+ end
71
+
72
+ # PermissionError is raised in cases where access was attempted on a resource
73
+ # that wasn't allowed.
74
+ class PermissionError < StripeError
75
+ end
76
+
77
+ # RateLimitError is raised in cases where an account is putting too much load
78
+ # on Stripe's API servers (usually by performing too many requests). Please
79
+ # back off on request rate.
80
+ class RateLimitError < StripeError
81
+ end
82
+ end
@@ -3,7 +3,7 @@ module Stripe
3
3
  extend Stripe::APIOperations::Create
4
4
  extend Stripe::APIOperations::List
5
5
 
6
- def self.url
6
+ def self.resource_url
7
7
  "/v1/files"
8
8
  end
9
9
 
@@ -1,7 +1,7 @@
1
1
  module Stripe
2
2
  class Invoice < APIResource
3
3
  extend Stripe::APIOperations::List
4
- include Stripe::APIOperations::Update
4
+ include Stripe::APIOperations::Save
5
5
  extend Stripe::APIOperations::Create
6
6
 
7
7
  def self.upcoming(params, opts={})
@@ -17,11 +17,11 @@ module Stripe
17
17
  private
18
18
 
19
19
  def self.upcoming_url
20
- url + '/upcoming'
20
+ resource_url + '/upcoming'
21
21
  end
22
22
 
23
23
  def pay_url
24
- url + '/pay'
24
+ resource_url + '/pay'
25
25
  end
26
26
  end
27
27
  end
@@ -3,6 +3,6 @@ module Stripe
3
3
  extend Stripe::APIOperations::List
4
4
  extend Stripe::APIOperations::Create
5
5
  include Stripe::APIOperations::Delete
6
- include Stripe::APIOperations::Update
6
+ include Stripe::APIOperations::Save
7
7
  end
8
8
  end
@@ -3,6 +3,7 @@ module Stripe
3
3
  include Enumerable
4
4
  include Stripe::APIOperations::List
5
5
  include Stripe::APIOperations::Request
6
+ include Stripe::APIOperations::Create
6
7
 
7
8
  # This accessor allows a `ListObject` to inherit various filters that were
8
9
  # given to a predecessor. This allows for things like consistent limits,
@@ -63,12 +64,7 @@ module Stripe
63
64
 
64
65
  def retrieve(id, opts={})
65
66
  id, retrieve_params = Util.normalize_id(id)
66
- response, opts = request(:get,"#{url}/#{CGI.escape(id)}", retrieve_params, opts)
67
- Util.convert_to_stripe_object(response, opts)
68
- end
69
-
70
- def create(params={}, opts={})
71
- response, opts = request(:post, url, params, opts)
67
+ response, opts = request(:get,"#{resource_url}/#{CGI.escape(id)}", retrieve_params, opts)
72
68
  Util.convert_to_stripe_object(response, opts)
73
69
  end
74
70
 
@@ -100,5 +96,10 @@ module Stripe
100
96
 
101
97
  list(params, opts)
102
98
  end
99
+
100
+ def resource_url
101
+ self.url ||
102
+ raise(ArgumentError, "List object does not contain a 'url' field.")
103
+ end
103
104
  end
104
105
  end
data/lib/stripe/order.rb CHANGED
@@ -2,18 +2,26 @@ module Stripe
2
2
  class Order < 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 pay(params, opts={})
8
8
  response, opts = request(:post, pay_url, params, opts)
9
9
  initialize_from(response, opts)
10
10
  end
11
11
 
12
+ def return_order(params, opts={})
13
+ response, opts = request(:post, returns_url, params, opts)
14
+ Util.convert_to_stripe_object(response, opts)
15
+ end
16
+
12
17
  private
13
18
 
14
19
  def pay_url
15
- url + "/pay"
20
+ resource_url + '/pay'
16
21
  end
17
22
 
23
+ def returns_url
24
+ resource_url + '/returns'
25
+ end
18
26
  end
19
27
  end
@@ -0,0 +1,9 @@
1
+ module Stripe
2
+ class OrderReturn < APIResource
3
+ extend Stripe::APIOperations::List
4
+
5
+ def self.resource_url
6
+ "/v1/order_returns"
7
+ end
8
+ end
9
+ end
data/lib/stripe/plan.rb CHANGED
@@ -3,6 +3,6 @@ module Stripe
3
3
  extend Stripe::APIOperations::Create
4
4
  include Stripe::APIOperations::Delete
5
5
  extend Stripe::APIOperations::List
6
- include Stripe::APIOperations::Update
6
+ include Stripe::APIOperations::Save
7
7
  end
8
8
  end
@@ -2,15 +2,7 @@ module Stripe
2
2
  class Product < APIResource
3
3
  extend Stripe::APIOperations::List
4
4
  extend Stripe::APIOperations::Create
5
- include Stripe::APIOperations::Update
6
-
7
- # Keep APIResource#url as `api_url` to avoid letting the external URL
8
- # replace the Stripe URL.
9
- alias_method :api_url, :url
10
-
11
- # Override Stripe::APIOperations::Update#save to explicitly pass URL.
12
- def save
13
- super(:req_url => api_url)
14
- end
5
+ include Stripe::APIOperations::Save
6
+ include Stripe::APIOperations::Delete
15
7
  end
16
8
  end
@@ -2,7 +2,7 @@ module Stripe
2
2
  class Recipient < APIResource
3
3
  extend Stripe::APIOperations::Create
4
4
  include Stripe::APIOperations::Delete
5
- include Stripe::APIOperations::Update
5
+ include Stripe::APIOperations::Save
6
6
  extend Stripe::APIOperations::List
7
7
 
8
8
  def transfers
data/lib/stripe/refund.rb CHANGED
@@ -2,6 +2,6 @@ module Stripe
2
2
  class Refund < APIResource
3
3
  extend Stripe::APIOperations::Create
4
4
  extend Stripe::APIOperations::List
5
- include Stripe::APIOperations::Update
5
+ include Stripe::APIOperations::Save
6
6
  end
7
7
  end
@@ -1,10 +1,14 @@
1
1
  module Stripe
2
2
  class Reversal < APIResource
3
- include Stripe::APIOperations::Update
3
+ include Stripe::APIOperations::Save
4
4
  extend Stripe::APIOperations::List
5
5
 
6
- def url
7
- "#{Transfer.url}/#{CGI.escape(transfer)}/reversals/#{CGI.escape(id)}"
6
+ def resource_url
7
+ "#{Transfer.resource_url}/#{CGI.escape(transfer)}/reversals/#{CGI.escape(id)}"
8
+ end
9
+
10
+ def self.update(id, params=nil, opts=nil)
11
+ raise NotImplementedError.new("Reversals cannot be updated without a transfer ID. Update a reversal using `r = transfer.reversals.retrieve('reversal_id'); r.save`")
8
12
  end
9
13
 
10
14
  def self.retrieve(id, opts={})
@@ -1,14 +1,14 @@
1
1
  module Stripe
2
2
  class SingletonAPIResource < APIResource
3
- def self.url
3
+ def self.resource_url
4
4
  if self == SingletonAPIResource
5
5
  raise NotImplementedError.new('SingletonAPIResource is an abstract class. You should perform actions on its subclasses (Account, etc.)')
6
6
  end
7
7
  "/v1/#{CGI.escape(class_name.downcase)}"
8
8
  end
9
9
 
10
- def url
11
- self.class.url
10
+ def resource_url
11
+ self.class.resource_url
12
12
  end
13
13
 
14
14
  def self.retrieve(opts={})
data/lib/stripe/sku.rb CHANGED
@@ -2,7 +2,7 @@ module Stripe
2
2
  class SKU < APIResource
3
3
  extend Stripe::APIOperations::List
4
4
  extend Stripe::APIOperations::Create
5
- include Stripe::APIOperations::Update
6
-
5
+ include Stripe::APIOperations::Save
6
+ include Stripe::APIOperations::Delete
7
7
  end
8
8
  end
@@ -0,0 +1,11 @@
1
+ module Stripe
2
+ class Source < APIResource
3
+ extend Stripe::APIOperations::Create
4
+ include Stripe::APIOperations::Save
5
+
6
+ def verify(params={}, opts={})
7
+ response, opts = request(:post, resource_url + '/verify', params, opts)
8
+ initialize_from(response, opts)
9
+ end
10
+ end
11
+ end
@@ -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