rainforest 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/.travis.yml +10 -0
  4. data/CONTRIBUTORS +1 -0
  5. data/Gemfile +2 -0
  6. data/History.txt +4 -0
  7. data/LICENSE +21 -0
  8. data/README.md +50 -0
  9. data/Rakefile +15 -0
  10. data/VERSION +1 -0
  11. data/bin/rainforest-console +7 -0
  12. data/gemfiles/default-with-activesupport.gemfile +3 -0
  13. data/gemfiles/json.gemfile +4 -0
  14. data/gemfiles/yajl.gemfile +4 -0
  15. data/lib/.DS_Store +0 -0
  16. data/lib/data/ca-certificates.crt +3918 -0
  17. data/lib/rainforest.rb +271 -0
  18. data/lib/rainforest/api_operations/create.rb +16 -0
  19. data/lib/rainforest/api_operations/delete.rb +11 -0
  20. data/lib/rainforest/api_operations/list.rb +18 -0
  21. data/lib/rainforest/api_operations/update.rb +61 -0
  22. data/lib/rainforest/api_resource.rb +33 -0
  23. data/lib/rainforest/errors/api_connection_error.rb +4 -0
  24. data/lib/rainforest/errors/api_error.rb +4 -0
  25. data/lib/rainforest/errors/authentication_error.rb +4 -0
  26. data/lib/rainforest/errors/invalid_request_error.rb +10 -0
  27. data/lib/rainforest/errors/rainforest_error.rb +20 -0
  28. data/lib/rainforest/json.rb +21 -0
  29. data/lib/rainforest/list_object.rb +35 -0
  30. data/lib/rainforest/rainforest_object.rb +168 -0
  31. data/lib/rainforest/run.rb +8 -0
  32. data/lib/rainforest/singleton_api_resource.rb +20 -0
  33. data/lib/rainforest/test.rb +14 -0
  34. data/lib/rainforest/util.rb +101 -0
  35. data/lib/rainforest/version.rb +3 -0
  36. data/rainforest.gemspec +26 -0
  37. data/test/stripe/account_test.rb +14 -0
  38. data/test/stripe/api_resource_test.rb +345 -0
  39. data/test/stripe/charge_test.rb +67 -0
  40. data/test/stripe/coupon_test.rb +11 -0
  41. data/test/stripe/customer_test.rb +70 -0
  42. data/test/stripe/invoice_test.rb +20 -0
  43. data/test/stripe/list_object_test.rb +16 -0
  44. data/test/stripe/metadata_test.rb +114 -0
  45. data/test/stripe/util_test.rb +29 -0
  46. data/test/test_helper.rb +356 -0
  47. metadata +191 -0
@@ -0,0 +1,345 @@
1
+ # -*- coding: utf-8 -*-
2
+ require File.expand_path('../../test_helper', __FILE__)
3
+
4
+ module Rainforest
5
+ class ApiResourceTest < Test::Unit::TestCase
6
+ should "creating a new APIResource should not fetch over the network" do
7
+ @mock.expects(:get).never
8
+ c = Rainforest::Customer.new("someid")
9
+ end
10
+
11
+ should "creating a new APIResource from a hash should not fetch over the network" do
12
+ @mock.expects(:get).never
13
+ c = Rainforest::Customer.construct_from({
14
+ :id => "somecustomer",
15
+ :card => {:id => "somecard", :object => "card"},
16
+ :object => "customer"
17
+ })
18
+ end
19
+
20
+ should "setting an attribute should not cause a network request" do
21
+ @mock.expects(:get).never
22
+ @mock.expects(:post).never
23
+ c = Rainforest::Customer.new("test_customer");
24
+ c.card = {:id => "somecard", :object => "card"}
25
+ end
26
+
27
+ should "accessing id should not issue a fetch" do
28
+ @mock.expects(:get).never
29
+ c = Rainforest::Customer.new("test_customer");
30
+ c.id
31
+ end
32
+
33
+ should "not specifying api credentials should raise an exception" do
34
+ Rainforest.api_key = nil
35
+ assert_raises Rainforest::AuthenticationError do
36
+ Rainforest::Customer.new("test_customer").refresh
37
+ end
38
+ end
39
+
40
+ should "specifying api credentials containing whitespace should raise an exception" do
41
+ Rainforest.api_key = "key "
42
+ assert_raises Rainforest::AuthenticationError do
43
+ Rainforest::Customer.new("test_customer").refresh
44
+ end
45
+ end
46
+
47
+ should "specifying invalid api credentials should raise an exception" do
48
+ Rainforest.api_key = "invalid"
49
+ response = test_response(test_invalid_api_key_error, 401)
50
+ assert_raises Rainforest::AuthenticationError do
51
+ @mock.expects(:get).once.raises(RestClient::ExceptionWithResponse.new(response, 401))
52
+ Rainforest::Customer.retrieve("failing_customer")
53
+ end
54
+ end
55
+
56
+ should "AuthenticationErrors should have an http status, http body, and JSON body" do
57
+ Rainforest.api_key = "invalid"
58
+ response = test_response(test_invalid_api_key_error, 401)
59
+ begin
60
+ @mock.expects(:get).once.raises(RestClient::ExceptionWithResponse.new(response, 401))
61
+ Rainforest::Customer.retrieve("failing_customer")
62
+ rescue Rainforest::AuthenticationError => e
63
+ assert_equal(401, e.http_status)
64
+ assert_equal(true, !!e.http_body)
65
+ assert_equal(true, !!e.json_body[:error][:message])
66
+ assert_equal(test_invalid_api_key_error['error']['message'], e.json_body[:error][:message])
67
+ end
68
+ end
69
+
70
+ context "when specifying per-object credentials" do
71
+ context "with no global API key set" do
72
+ should "use the per-object credential when creating" do
73
+ Rainforest.expects(:execute_request).with do |opts|
74
+ opts[:headers][:authorization] == 'Bearer sk_test_local'
75
+ end.returns(test_response(test_charge))
76
+
77
+ Rainforest::Charge.create({:card => {:number => '4242424242424242'}},
78
+ 'sk_test_local')
79
+ end
80
+ end
81
+
82
+ context "with a global API key set" do
83
+ setup do
84
+ Rainforest.api_key = "global"
85
+ end
86
+
87
+ teardown do
88
+ Rainforest.api_key = nil
89
+ end
90
+
91
+ should "use the per-object credential when creating" do
92
+ Rainforest.expects(:execute_request).with do |opts|
93
+ opts[:headers][:authorization] == 'Bearer local'
94
+ end.returns(test_response(test_charge))
95
+
96
+ Rainforest::Charge.create({:card => {:number => '4242424242424242'}},
97
+ 'local')
98
+ end
99
+
100
+ should "use the per-object credential when retrieving and making other calls" do
101
+ Rainforest.expects(:execute_request).with do |opts|
102
+ opts[:url] == "#{Rainforest.api_base}/v1/charges/ch_test_charge" &&
103
+ opts[:headers][:authorization] == 'Bearer local'
104
+ end.returns(test_response(test_charge))
105
+ Rainforest.expects(:execute_request).with do |opts|
106
+ opts[:url] == "#{Rainforest.api_base}/v1/charges/ch_test_charge/refund" &&
107
+ opts[:headers][:authorization] == 'Bearer local'
108
+ end.returns(test_response(test_charge))
109
+
110
+ ch = Rainforest::Charge.retrieve('ch_test_charge', 'local')
111
+ ch.refund
112
+ end
113
+ end
114
+ end
115
+
116
+ context "with valid credentials" do
117
+ should "urlencode values in GET params" do
118
+ response = test_response(test_charge_array)
119
+ @mock.expects(:get).with("#{Rainforest.api_base}/v1/charges?customer=test%20customer", nil, nil).returns(response)
120
+ charges = Rainforest::Charge.all(:customer => 'test customer').data
121
+ assert charges.kind_of? Array
122
+ end
123
+
124
+ should "construct URL properly with base query parameters" do
125
+ response = test_response(test_invoice_customer_array)
126
+ @mock.expects(:get).with("#{Rainforest.api_base}/v1/invoices?customer=test_customer", nil, nil).returns(response)
127
+ invoices = Rainforest::Invoice.all(:customer => 'test_customer')
128
+
129
+ @mock.expects(:get).with("#{Rainforest.api_base}/v1/invoices?customer=test_customer&paid=true", nil, nil).returns(response)
130
+ invoices.all(:paid => true)
131
+ end
132
+
133
+ should "a 400 should give an InvalidRequestError with http status, body, and JSON body" do
134
+ response = test_response(test_missing_id_error, 400)
135
+ @mock.expects(:get).once.raises(RestClient::ExceptionWithResponse.new(response, 404))
136
+ begin
137
+ Rainforest::Customer.retrieve("foo")
138
+ rescue Rainforest::InvalidRequestError => e
139
+ assert_equal(400, e.http_status)
140
+ assert_equal(true, !!e.http_body)
141
+ assert_equal(true, e.json_body.kind_of?(Hash))
142
+ end
143
+ end
144
+
145
+ should "a 401 should give an AuthenticationError with http status, body, and JSON body" do
146
+ response = test_response(test_missing_id_error, 401)
147
+ @mock.expects(:get).once.raises(RestClient::ExceptionWithResponse.new(response, 404))
148
+ begin
149
+ Rainforest::Customer.retrieve("foo")
150
+ rescue Rainforest::AuthenticationError => e
151
+ assert_equal(401, e.http_status)
152
+ assert_equal(true, !!e.http_body)
153
+ assert_equal(true, e.json_body.kind_of?(Hash))
154
+ end
155
+ end
156
+
157
+ should "a 402 should give a CardError with http status, body, and JSON body" do
158
+ response = test_response(test_missing_id_error, 402)
159
+ @mock.expects(:get).once.raises(RestClient::ExceptionWithResponse.new(response, 404))
160
+ begin
161
+ Rainforest::Customer.retrieve("foo")
162
+ rescue Rainforest::CardError => e
163
+ assert_equal(402, e.http_status)
164
+ assert_equal(true, !!e.http_body)
165
+ assert_equal(true, e.json_body.kind_of?(Hash))
166
+ end
167
+ end
168
+
169
+ should "a 404 should give an InvalidRequestError with http status, body, and JSON body" do
170
+ response = test_response(test_missing_id_error, 404)
171
+ @mock.expects(:get).once.raises(RestClient::ExceptionWithResponse.new(response, 404))
172
+ begin
173
+ Rainforest::Customer.retrieve("foo")
174
+ rescue Rainforest::InvalidRequestError => e
175
+ assert_equal(404, e.http_status)
176
+ assert_equal(true, !!e.http_body)
177
+ assert_equal(true, e.json_body.kind_of?(Hash))
178
+ end
179
+ end
180
+
181
+ should "setting a nil value for a param should exclude that param from the request" do
182
+ @mock.expects(:get).with do |url, api_key, params|
183
+ uri = URI(url)
184
+ query = CGI.parse(uri.query)
185
+ (url =~ %r{^#{Rainforest.api_base}/v1/charges?} &&
186
+ query.keys.sort == ['offset', 'sad'])
187
+ end.returns(test_response({ :count => 1, :data => [test_charge] }))
188
+ c = Rainforest::Charge.all(:count => nil, :offset => 5, :sad => false)
189
+
190
+ @mock.expects(:post).with do |url, api_key, params|
191
+ url == "#{Rainforest.api_base}/v1/charges" &&
192
+ api_key.nil? &&
193
+ CGI.parse(params) == { 'amount' => ['50'], 'currency' => ['usd'] }
194
+ end.returns(test_response({ :count => 1, :data => [test_charge] }))
195
+ c = Rainforest::Charge.create(:amount => 50, :currency => 'usd', :card => { :number => nil })
196
+ end
197
+
198
+ should "requesting with a unicode ID should result in a request" do
199
+ response = test_response(test_missing_id_error, 404)
200
+ @mock.expects(:get).once.with("#{Rainforest.api_base}/v1/customers/%E2%98%83", nil, nil).raises(RestClient::ExceptionWithResponse.new(response, 404))
201
+ c = Rainforest::Customer.new("☃")
202
+ assert_raises(Rainforest::InvalidRequestError) { c.refresh }
203
+ end
204
+
205
+ should "requesting with no ID should result in an InvalidRequestError with no request" do
206
+ c = Rainforest::Customer.new
207
+ assert_raises(Rainforest::InvalidRequestError) { c.refresh }
208
+ end
209
+
210
+ should "making a GET request with parameters should have a query string and no body" do
211
+ params = { :limit => 1 }
212
+ @mock.expects(:get).once.with("#{Rainforest.api_base}/v1/charges?limit=1", nil, nil).returns(test_response([test_charge]))
213
+ c = Rainforest::Charge.all(params)
214
+ end
215
+
216
+ should "making a POST request with parameters should have a body and no query string" do
217
+ params = { :amount => 100, :currency => 'usd', :card => 'sc_token' }
218
+ @mock.expects(:post).once.with do |url, get, post|
219
+ get.nil? && CGI.parse(post) == {'amount' => ['100'], 'currency' => ['usd'], 'card' => ['sc_token']}
220
+ end.returns(test_response(test_charge))
221
+ c = Rainforest::Charge.create(params)
222
+ end
223
+
224
+ should "loading an object should issue a GET request" do
225
+ @mock.expects(:get).once.returns(test_response(test_customer))
226
+ c = Rainforest::Customer.new("test_customer")
227
+ c.refresh
228
+ end
229
+
230
+ should "using array accessors should be the same as the method interface" do
231
+ @mock.expects(:get).once.returns(test_response(test_customer))
232
+ c = Rainforest::Customer.new("test_customer")
233
+ c.refresh
234
+ assert_equal c.created, c[:created]
235
+ assert_equal c.created, c['created']
236
+ c['created'] = 12345
237
+ assert_equal c.created, 12345
238
+ end
239
+
240
+ should "accessing a property other than id or parent on an unfetched object should fetch it" do
241
+ @mock.expects(:get).once.returns(test_response(test_customer))
242
+ c = Rainforest::Customer.new("test_customer")
243
+ c.charges
244
+ end
245
+
246
+ should "updating an object should issue a POST request with only the changed properties" do
247
+ @mock.expects(:post).with do |url, api_key, params|
248
+ url == "#{Rainforest.api_base}/v1/customers/c_test_customer" && api_key.nil? && CGI.parse(params) == {'description' => ['another_mn']}
249
+ end.once.returns(test_response(test_customer))
250
+ c = Rainforest::Customer.construct_from(test_customer)
251
+ c.description = "another_mn"
252
+ c.save
253
+ end
254
+
255
+ should "updating should merge in returned properties" do
256
+ @mock.expects(:post).once.returns(test_response(test_customer))
257
+ c = Rainforest::Customer.new("c_test_customer")
258
+ c.description = "another_mn"
259
+ c.save
260
+ assert_equal false, c.livemode
261
+ end
262
+
263
+ should "deleting should send no props and result in an object that has no props other deleted" do
264
+ @mock.expects(:get).never
265
+ @mock.expects(:post).never
266
+ @mock.expects(:delete).with("#{Rainforest.api_base}/v1/customers/c_test_customer", nil, nil).once.returns(test_response({ "id" => "test_customer", "deleted" => true }))
267
+
268
+ c = Rainforest::Customer.construct_from(test_customer)
269
+ c.delete
270
+ assert_equal true, c.deleted
271
+
272
+ assert_raises NoMethodError do
273
+ c.livemode
274
+ end
275
+ end
276
+
277
+ should "loading an object with properties that have specific types should instantiate those classes" do
278
+ @mock.expects(:get).once.returns(test_response(test_charge))
279
+ c = Rainforest::Charge.retrieve("test_charge")
280
+ assert c.card.kind_of?(Rainforest::RainforestObject) && c.card.object == 'card'
281
+ end
282
+
283
+ should "loading all of an APIResource should return an array of recursively instantiated objects" do
284
+ @mock.expects(:get).once.returns(test_response(test_charge_array))
285
+ c = Rainforest::Charge.all.data
286
+ assert c.kind_of? Array
287
+ assert c[0].kind_of? Rainforest::Charge
288
+ assert c[0].card.kind_of?(Rainforest::RainforestObject) && c[0].card.object == 'card'
289
+ end
290
+
291
+ context "error checking" do
292
+
293
+ should "404s should raise an InvalidRequestError" do
294
+ response = test_response(test_missing_id_error, 404)
295
+ @mock.expects(:get).once.raises(RestClient::ExceptionWithResponse.new(response, 404))
296
+
297
+ begin
298
+ Rainforest::Customer.new("test_customer").refresh
299
+ assert false #shouldn't get here either
300
+ rescue Rainforest::InvalidRequestError => e # we don't use assert_raises because we want to examine e
301
+ assert e.kind_of? Rainforest::InvalidRequestError
302
+ assert_equal "id", e.param
303
+ assert_equal "Missing id", e.message
304
+ return
305
+ end
306
+
307
+ assert false #shouldn't get here
308
+ end
309
+
310
+ should "5XXs should raise an APIError" do
311
+ response = test_response(test_api_error, 500)
312
+ @mock.expects(:get).once.raises(RestClient::ExceptionWithResponse.new(response, 500))
313
+
314
+ begin
315
+ Rainforest::Customer.new("test_customer").refresh
316
+ assert false #shouldn't get here either
317
+ rescue Rainforest::APIError => e # we don't use assert_raises because we want to examine e
318
+ assert e.kind_of? Rainforest::APIError
319
+ return
320
+ end
321
+
322
+ assert false #shouldn't get here
323
+ end
324
+
325
+ should "402s should raise a CardError" do
326
+ response = test_response(test_invalid_exp_year_error, 402)
327
+ @mock.expects(:get).once.raises(RestClient::ExceptionWithResponse.new(response, 402))
328
+
329
+ begin
330
+ Rainforest::Customer.new("test_customer").refresh
331
+ assert false #shouldn't get here either
332
+ rescue Rainforest::CardError => e # we don't use assert_raises because we want to examine e
333
+ assert e.kind_of? Rainforest::CardError
334
+ assert_equal "invalid_expiry_year", e.code
335
+ assert_equal "exp_year", e.param
336
+ assert_equal "Your card's expiration year is invalid", e.message
337
+ return
338
+ end
339
+
340
+ assert false #shouldn't get here
341
+ end
342
+ end
343
+ end
344
+ end
345
+ end
@@ -0,0 +1,67 @@
1
+ require File.expand_path('../../test_helper', __FILE__)
2
+
3
+ module Rainforest
4
+ class ChargeTest < Test::Unit::TestCase
5
+ should "charges should be listable" do
6
+ @mock.expects(:get).once.returns(test_response(test_charge_array))
7
+ c = Rainforest::Charge.all
8
+ assert c.data.kind_of? Array
9
+ c.each do |charge|
10
+ assert charge.kind_of?(Rainforest::Charge)
11
+ end
12
+ end
13
+
14
+ should "charges should be refundable" do
15
+ @mock.expects(:get).never
16
+ @mock.expects(:post).once.returns(test_response({:id => "ch_test_charge", :refunded => true}))
17
+ c = Rainforest::Charge.new("test_charge")
18
+ c.refund
19
+ assert c.refunded
20
+ end
21
+
22
+ should "charges should not be deletable" do
23
+ assert_raises NoMethodError do
24
+ @mock.expects(:get).once.returns(test_response(test_charge))
25
+ c = Rainforest::Charge.retrieve("test_charge")
26
+ c.delete
27
+ end
28
+ end
29
+
30
+ should "charges should be updateable" do
31
+ @mock.expects(:get).once.returns(test_response(test_charge))
32
+ @mock.expects(:post).once.returns(test_response(test_charge))
33
+ c = Rainforest::Charge.new("test_charge")
34
+ c.refresh
35
+ c.mnemonic = "New charge description"
36
+ c.save
37
+ end
38
+
39
+ should "charges should have Card objects associated with their Card property" do
40
+ @mock.expects(:get).once.returns(test_response(test_charge))
41
+ c = Rainforest::Charge.retrieve("test_charge")
42
+ assert c.card.kind_of?(Rainforest::RainforestObject) && c.card.object == 'card'
43
+ end
44
+
45
+ should "execute should return a new, fully executed charge when passed correct parameters" do
46
+ @mock.expects(:post).with do |url, api_key, params|
47
+ url == "#{Rainforest.api_base}/v1/charges" && api_key.nil? && CGI.parse(params) == {
48
+ 'currency' => ['usd'], 'amount' => ['100'],
49
+ 'card[exp_year]' => ['2012'],
50
+ 'card[number]' => ['4242424242424242'],
51
+ 'card[exp_month]' => ['11']
52
+ }
53
+ end.once.returns(test_response(test_charge))
54
+
55
+ c = Rainforest::Charge.create({
56
+ :amount => 100,
57
+ :card => {
58
+ :number => "4242424242424242",
59
+ :exp_month => 11,
60
+ :exp_year => 2012,
61
+ },
62
+ :currency => "usd"
63
+ })
64
+ assert c.paid
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,11 @@
1
+ require File.expand_path('../../test_helper', __FILE__)
2
+
3
+ module Rainforest
4
+ class CouponTest < Test::Unit::TestCase
5
+ should "create should return a new coupon" do
6
+ @mock.expects(:post).once.returns(test_response(test_coupon))
7
+ c = Rainforest::Coupon.create
8
+ assert_equal "co_test_coupon", c.id
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,70 @@
1
+ require File.expand_path('../../test_helper', __FILE__)
2
+
3
+ module Rainforest
4
+ class CustomerTest < Test::Unit::TestCase
5
+ should "customers should be listable" do
6
+ @mock.expects(:get).once.returns(test_response(test_customer_array))
7
+ c = Rainforest::Customer.all.data
8
+ assert c.kind_of? Array
9
+ assert c[0].kind_of? Rainforest::Customer
10
+ end
11
+
12
+ should "customers should be deletable" do
13
+ @mock.expects(:delete).once.returns(test_response(test_customer({:deleted => true})))
14
+ c = Rainforest::Customer.new("test_customer")
15
+ c.delete
16
+ assert c.deleted
17
+ end
18
+
19
+ should "customers should be updateable" do
20
+ @mock.expects(:get).once.returns(test_response(test_customer({:mnemonic => "foo"})))
21
+ @mock.expects(:post).once.returns(test_response(test_customer({:mnemonic => "bar"})))
22
+ c = Rainforest::Customer.new("test_customer").refresh
23
+ assert_equal c.mnemonic, "foo"
24
+ c.mnemonic = "bar"
25
+ c.save
26
+ assert_equal c.mnemonic, "bar"
27
+ end
28
+
29
+ should "create should return a new customer" do
30
+ @mock.expects(:post).once.returns(test_response(test_customer))
31
+ c = Rainforest::Customer.create
32
+ assert_equal "c_test_customer", c.id
33
+ end
34
+
35
+ should "be able to update a customer's subscription" do
36
+ @mock.expects(:get).once.returns(test_response(test_customer))
37
+ c = Rainforest::Customer.retrieve("test_customer")
38
+
39
+ @mock.expects(:post).once.with do |url, api_key, params|
40
+ url == "#{Rainforest.api_base}/v1/customers/c_test_customer/subscription" && api_key.nil? && CGI.parse(params) == {'plan' => ['silver']}
41
+ end.returns(test_response(test_subscription('silver')))
42
+ s = c.update_subscription({:plan => 'silver'})
43
+
44
+ assert_equal 'subscription', s.object
45
+ assert_equal 'silver', s.plan.identifier
46
+ end
47
+
48
+ should "be able to cancel a customer's subscription" do
49
+ @mock.expects(:get).once.returns(test_response(test_customer))
50
+ c = Rainforest::Customer.retrieve("test_customer")
51
+
52
+ # Not an accurate response, but whatever
53
+
54
+ @mock.expects(:delete).once.with("#{Rainforest.api_base}/v1/customers/c_test_customer/subscription?at_period_end=true", nil, nil).returns(test_response(test_subscription('silver')))
55
+ s = c.cancel_subscription({:at_period_end => 'true'})
56
+
57
+ @mock.expects(:delete).once.with("#{Rainforest.api_base}/v1/customers/c_test_customer/subscription", nil, nil).returns(test_response(test_subscription('silver')))
58
+ s = c.cancel_subscription
59
+ end
60
+
61
+ should "be able to delete a customer's discount" do
62
+ @mock.expects(:get).once.returns(test_response(test_customer))
63
+ c = Rainforest::Customer.retrieve("test_customer")
64
+
65
+ @mock.expects(:delete).once.with("#{Rainforest.api_base}/v1/customers/c_test_customer/discount", nil, nil).returns(test_response(test_delete_discount_response))
66
+ s = c.delete_discount
67
+ assert_equal nil, c.discount
68
+ end
69
+ end
70
+ end