braintree 2.0.0 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +3 -3
- data/lib/braintree.rb +1 -0
- data/lib/braintree/advanced_search.rb +70 -8
- data/lib/braintree/credit_card.rb +43 -4
- data/lib/braintree/error_codes.rb +7 -0
- data/lib/braintree/subscription.rb +22 -4
- data/lib/braintree/transaction.rb +34 -17
- data/lib/braintree/transaction_search.rb +63 -0
- data/lib/braintree/transparent_redirect.rb +1 -1
- data/lib/braintree/util.rb +3 -2
- data/lib/braintree/version.rb +1 -1
- data/lib/braintree/xml/generator.rb +17 -3
- data/spec/integration/braintree/credit_card_spec.rb +47 -0
- data/spec/integration/braintree/subscription_spec.rb +39 -8
- data/spec/integration/braintree/transaction_search_spec.rb +734 -0
- data/spec/integration/braintree/transaction_spec.rb +138 -97
- data/spec/spec_helper.rb +4 -0
- data/spec/unit/braintree/credit_card_spec.rb +46 -4
- data/spec/unit/braintree/subscription_spec.rb +22 -3
- data/spec/unit/braintree/transaction_search_spec.rb +24 -0
- data/spec/unit/braintree/transaction_spec.rb +13 -2
- data/spec/unit/braintree/util_spec.rb +7 -1
- data/spec/unit/braintree/xml_spec.rb +22 -0
- metadata +16 -24
data/README.rdoc
CHANGED
@@ -13,9 +13,9 @@ The Braintree gem provides integration access to the Braintree Gateway.
|
|
13
13
|
require "braintree"
|
14
14
|
|
15
15
|
Braintree::Configuration.environment = :sandbox
|
16
|
-
Braintree::Configuration.merchant_id = "
|
17
|
-
Braintree::Configuration.public_key = "
|
18
|
-
Braintree::Configuration.private_key = "
|
16
|
+
Braintree::Configuration.merchant_id = "your_merchant_id"
|
17
|
+
Braintree::Configuration.public_key = "your_public_key"
|
18
|
+
Braintree::Configuration.private_key = "your_private_key"
|
19
19
|
|
20
20
|
result = Braintree::Transaction.sale(
|
21
21
|
:amount => "1000.00",
|
data/lib/braintree.rb
CHANGED
@@ -41,6 +41,7 @@ require "braintree/successful_result.rb"
|
|
41
41
|
require "braintree/test/credit_card_numbers.rb"
|
42
42
|
require "braintree/test/transaction_amounts.rb"
|
43
43
|
require "braintree/transaction.rb"
|
44
|
+
require "braintree/transaction_search.rb"
|
44
45
|
require "braintree/transaction/address_details.rb"
|
45
46
|
require "braintree/transaction/credit_card_details.rb"
|
46
47
|
require "braintree/transaction/customer_details.rb"
|
@@ -14,8 +14,22 @@ module Braintree
|
|
14
14
|
end
|
15
15
|
end
|
16
16
|
|
17
|
-
class
|
18
|
-
operators :is, :is_not
|
17
|
+
class EqualityNode < SearchNode
|
18
|
+
operators :is, :is_not
|
19
|
+
end
|
20
|
+
|
21
|
+
class PartialMatchNode < EqualityNode
|
22
|
+
operators :ends_with, :starts_with
|
23
|
+
end
|
24
|
+
|
25
|
+
class TextNode < PartialMatchNode
|
26
|
+
operators :contains
|
27
|
+
end
|
28
|
+
|
29
|
+
class KeyValueNode < SearchNode
|
30
|
+
def is(value)
|
31
|
+
@parent.add_criteria(@node_name, value)
|
32
|
+
end
|
19
33
|
end
|
20
34
|
|
21
35
|
class MultipleValueNode < SearchNode
|
@@ -30,6 +44,10 @@ module Braintree
|
|
30
44
|
@parent.add_criteria(@node_name, values)
|
31
45
|
end
|
32
46
|
|
47
|
+
def is(value)
|
48
|
+
self.in(value)
|
49
|
+
end
|
50
|
+
|
33
51
|
def initialize(name, parent, options)
|
34
52
|
super(name, parent)
|
35
53
|
@options = options
|
@@ -40,26 +58,70 @@ module Braintree
|
|
40
58
|
end
|
41
59
|
end
|
42
60
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
61
|
+
class RangeNode < SearchNode
|
62
|
+
def between(min, max)
|
63
|
+
self >= min
|
64
|
+
self <= max
|
65
|
+
end
|
66
|
+
|
67
|
+
def >=(min)
|
68
|
+
@parent.add_criteria(@node_name, :min => min)
|
69
|
+
end
|
70
|
+
|
71
|
+
def <=(max)
|
72
|
+
@parent.add_criteria(@node_name, :max => max)
|
48
73
|
end
|
49
74
|
end
|
50
75
|
|
76
|
+
def self.search_fields(*fields)
|
77
|
+
_create_field_accessors(fields, TextNode)
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.equality_fields(*fields)
|
81
|
+
_create_field_accessors(fields, EqualityNode)
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.partial_match_fields(*fields)
|
85
|
+
_create_field_accessors(fields, PartialMatchNode)
|
86
|
+
end
|
87
|
+
|
51
88
|
def self.multiple_value_field(field, options={})
|
52
89
|
define_method(field) do
|
53
90
|
MultipleValueNode.new(field, self, options)
|
54
91
|
end
|
55
92
|
end
|
56
93
|
|
94
|
+
def self.key_value_fields(*fields)
|
95
|
+
_create_field_accessors(fields, KeyValueNode)
|
96
|
+
end
|
97
|
+
|
98
|
+
def self.range_fields(*fields)
|
99
|
+
_create_field_accessors(fields, RangeNode)
|
100
|
+
end
|
101
|
+
|
102
|
+
def self.date_range_fields(*fields)
|
103
|
+
_create_field_accessors(fields, DateRangeNode)
|
104
|
+
end
|
105
|
+
|
106
|
+
def self._create_field_accessors(fields, node_class)
|
107
|
+
fields.each do |field|
|
108
|
+
define_method(field) do |*args|
|
109
|
+
raise "An operator is required" unless args.empty?
|
110
|
+
node_class.new(field, self)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
57
115
|
def initialize
|
58
116
|
@criteria = {}
|
59
117
|
end
|
60
118
|
|
61
119
|
def add_criteria(key, value)
|
62
|
-
@criteria[key]
|
120
|
+
if @criteria[key].is_a?(Hash)
|
121
|
+
@criteria[key].merge!(value)
|
122
|
+
else
|
123
|
+
@criteria[key] = value
|
124
|
+
end
|
63
125
|
end
|
64
126
|
|
65
127
|
def to_hash
|
@@ -6,6 +6,29 @@ module Braintree
|
|
6
6
|
class CreditCard
|
7
7
|
include BaseModule # :nodoc:
|
8
8
|
|
9
|
+
module CardType
|
10
|
+
AmEx = "American Express"
|
11
|
+
CarteBlanche = "Carte Blanche"
|
12
|
+
ChinaUnionPay = "China UnionPay"
|
13
|
+
DinersClubInternational = "Diners Club"
|
14
|
+
Discover = "Discover"
|
15
|
+
JCB = "JCB"
|
16
|
+
Laser = "Laser"
|
17
|
+
Maestro = "Maestro"
|
18
|
+
MasterCard = "MasterCard"
|
19
|
+
Solo = "Solo"
|
20
|
+
Switch = "Switch"
|
21
|
+
Visa = "Visa"
|
22
|
+
Unknown = "Unknown"
|
23
|
+
|
24
|
+
All = constants.map { |c| const_get(c) }
|
25
|
+
end
|
26
|
+
|
27
|
+
module CustomerLocation
|
28
|
+
International = "international"
|
29
|
+
US = "us"
|
30
|
+
end
|
31
|
+
|
9
32
|
attr_reader :billing_address, :bin, :card_type, :cardholder_name, :created_at, :customer_id, :expiration_month,
|
10
33
|
:expiration_year, :last_4, :subscriptions, :token, :updated_at
|
11
34
|
|
@@ -103,7 +126,7 @@ module Braintree
|
|
103
126
|
|
104
127
|
def initialize(attributes) # :nodoc:
|
105
128
|
_init attributes
|
106
|
-
@subscriptions = (@subscriptions || []).map { |subscription_hash| Subscription.
|
129
|
+
@subscriptions = (@subscriptions || []).map { |subscription_hash| Subscription._new(subscription_hash) }
|
107
130
|
end
|
108
131
|
|
109
132
|
# Creates a credit transaction for this credit card.
|
@@ -196,7 +219,7 @@ module Braintree
|
|
196
219
|
end
|
197
220
|
|
198
221
|
def self._create_signature # :nodoc:
|
199
|
-
|
222
|
+
_signature(:create)
|
200
223
|
end
|
201
224
|
|
202
225
|
def self._new(*args) # :nodoc:
|
@@ -226,11 +249,27 @@ module Braintree
|
|
226
249
|
end
|
227
250
|
|
228
251
|
def self._update_signature # :nodoc:
|
229
|
-
|
252
|
+
_signature(:update)
|
253
|
+
end
|
254
|
+
|
255
|
+
def self._signature(type) # :nodoc:
|
256
|
+
billing_address_params = [:company, :country_name, :extended_address, :first_name, :last_name, :locality, :postal_code, :region, :street_address]
|
257
|
+
signature = [
|
230
258
|
:cardholder_name, :cvv, :expiration_date, :expiration_month, :expiration_year, :number, :token,
|
231
259
|
{:options => [:make_default, :verify_card]},
|
232
|
-
{:billing_address =>
|
260
|
+
{:billing_address => billing_address_params}
|
233
261
|
]
|
262
|
+
|
263
|
+
case type
|
264
|
+
when :create
|
265
|
+
signature << :customer_id
|
266
|
+
when :update
|
267
|
+
billing_address_params << {:options => [:update_existing]}
|
268
|
+
else
|
269
|
+
raise ArgumentError
|
270
|
+
end
|
271
|
+
|
272
|
+
return signature
|
234
273
|
end
|
235
274
|
|
236
275
|
def _init(attributes) # :nodoc:
|
@@ -11,6 +11,7 @@ module Braintree
|
|
11
11
|
FirstNameIsTooLong = "81805"
|
12
12
|
LastNameIsTooLong = "81806"
|
13
13
|
LocalityIsTooLong = "81807"
|
14
|
+
PostalCodeInvalidCharacters = "81813"
|
14
15
|
PostalCodeIsRequired = "81808"
|
15
16
|
PostalCodeIsTooLong = "81809"
|
16
17
|
RegionIsTooLong = "81810"
|
@@ -87,6 +88,7 @@ module Braintree
|
|
87
88
|
CreditCardIsRequired = "91508"
|
88
89
|
CustomerDefaultPaymentMethodCardTypeIsNotAccepted = "81509"
|
89
90
|
CustomFieldIsInvalid = "91526"
|
91
|
+
CustomFieldIsTooLong = "81527"
|
90
92
|
CustomerIdIsInvalid = "91510"
|
91
93
|
CustomerDoesNotHaveCreditCard = "91511"
|
92
94
|
HasAlreadyBeenRefunded = "91512"
|
@@ -95,10 +97,15 @@ module Braintree
|
|
95
97
|
OrderIdIsTooLong = "91501"
|
96
98
|
PaymentMethodConflict = "91515"
|
97
99
|
PaymentMethodDoesNotBelongToCustomer = "91516"
|
100
|
+
PaymentMethodDoesNotBelongToSubscription = "91527"
|
98
101
|
PaymentMethodTokenCardTypeIsNotAccepted = "91517"
|
99
102
|
PaymentMethodTokenIsInvalid = "91518"
|
103
|
+
ProcessorAuthorizationCodeCannotBeSet = "91519"
|
104
|
+
ProcessorAuthorizationCodeIsInvalid = "81520"
|
100
105
|
RefundAmountIsTooLarge = "91521"
|
101
106
|
SettlementAmountIsTooLarge = "91522"
|
107
|
+
SubscriptionDoesNotBelongToCustomer = "91529"
|
108
|
+
SubscriptionIdIsInvalid = "91528"
|
102
109
|
TypeIsInvalid = "91523"
|
103
110
|
TypeIsRequired = "91524"
|
104
111
|
module Options
|
@@ -47,7 +47,7 @@ module Braintree
|
|
47
47
|
def self.cancel(subscription_id)
|
48
48
|
response = Http.put "/subscriptions/#{subscription_id}/cancel"
|
49
49
|
if response[:subscription]
|
50
|
-
SuccessfulResult.new(:subscription =>
|
50
|
+
SuccessfulResult.new(:subscription => _new(response[:subscription]))
|
51
51
|
elsif response[:api_error_response]
|
52
52
|
ErrorResult.new(response[:api_error_response])
|
53
53
|
else
|
@@ -66,11 +66,21 @@ module Braintree
|
|
66
66
|
# if the subscription cannot be found.
|
67
67
|
def self.find(id)
|
68
68
|
response = Http.get "/subscriptions/#{id}"
|
69
|
-
|
69
|
+
_new(response[:subscription])
|
70
70
|
rescue NotFoundError
|
71
71
|
raise NotFoundError, "subscription with id #{id.inspect} not found"
|
72
72
|
end
|
73
73
|
|
74
|
+
def self.retry_charge(subscription_id, amount=nil)
|
75
|
+
attributes = {
|
76
|
+
:amount => amount,
|
77
|
+
:subscription_id => subscription_id,
|
78
|
+
:type => Transaction::Type::Sale
|
79
|
+
}
|
80
|
+
|
81
|
+
Transaction.send(:_do_create, "/transactions", :transaction => attributes)
|
82
|
+
end
|
83
|
+
|
74
84
|
# Allows searching on subscriptions. There are two types of fields that are searchable: text and
|
75
85
|
# multiple value fields. Searchable text fields are:
|
76
86
|
# - plan_id
|
@@ -93,7 +103,7 @@ module Braintree
|
|
93
103
|
|
94
104
|
response = Http.post "/subscriptions/advanced_search?page=#{page}", {:search => search.to_hash}
|
95
105
|
attributes = response[:subscriptions]
|
96
|
-
attributes[:items] = Util.extract_attribute_as_array(attributes, :subscription).map { |attrs|
|
106
|
+
attributes[:items] = Util.extract_attribute_as_array(attributes, :subscription).map { |attrs| _new(attrs) }
|
97
107
|
ResourceCollection.new(attributes) { |page_number| Subscription.search(page_number, &block) }
|
98
108
|
end
|
99
109
|
|
@@ -101,7 +111,7 @@ module Braintree
|
|
101
111
|
Util.verify_keys(_update_signature, attributes)
|
102
112
|
response = Http.put "/subscriptions/#{subscription_id}", :subscription => attributes
|
103
113
|
if response[:subscription]
|
104
|
-
SuccessfulResult.new(:subscription =>
|
114
|
+
SuccessfulResult.new(:subscription => _new(response[:subscription]))
|
105
115
|
elsif response[:api_error_response]
|
106
116
|
ErrorResult.new(response[:api_error_response])
|
107
117
|
else
|
@@ -130,9 +140,17 @@ module Braintree
|
|
130
140
|
|
131
141
|
# True if <tt>other</tt> has the same id.
|
132
142
|
def ==(other)
|
143
|
+
return false unless other.is_a?(Subscription)
|
133
144
|
id == other.id
|
134
145
|
end
|
135
146
|
|
147
|
+
class << self
|
148
|
+
protected :new
|
149
|
+
def _new(*args) # :nodoc:
|
150
|
+
self.new *args
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
136
154
|
def self._do_create(url, params) # :nodoc:
|
137
155
|
response = Http.post url, params
|
138
156
|
if response[:subscription]
|
@@ -123,6 +123,11 @@ module Braintree
|
|
123
123
|
class Transaction
|
124
124
|
include BaseModule
|
125
125
|
|
126
|
+
module CreatedUsing
|
127
|
+
FullInformation = 'full_information'
|
128
|
+
Token = 'token'
|
129
|
+
end
|
130
|
+
|
126
131
|
module Status
|
127
132
|
Authorizing = 'authorizing'
|
128
133
|
Authorized = 'authorized'
|
@@ -134,6 +139,14 @@ module Braintree
|
|
134
139
|
SubmittedForSettlement = 'submitted_for_settlement'
|
135
140
|
Unknown = 'unknown'
|
136
141
|
Voided = 'voided'
|
142
|
+
|
143
|
+
All = constants.map { |c| const_get(c) }
|
144
|
+
end
|
145
|
+
|
146
|
+
module Source
|
147
|
+
Api = "api"
|
148
|
+
ControlPanel = "control_panel"
|
149
|
+
Recurring = "recurring"
|
137
150
|
end
|
138
151
|
|
139
152
|
module Type # :nodoc:
|
@@ -145,6 +158,7 @@ module Braintree
|
|
145
158
|
attr_reader :amount, :created_at, :credit_card_details, :customer_details, :id
|
146
159
|
attr_reader :custom_fields
|
147
160
|
attr_reader :cvv_response_code
|
161
|
+
attr_reader :merchant_account_id
|
148
162
|
attr_reader :order_id
|
149
163
|
attr_reader :billing_details, :shipping_details
|
150
164
|
# The authorization code from the processor.
|
@@ -209,13 +223,13 @@ module Braintree
|
|
209
223
|
# Returns a ResourceCollection of transactions matching the search query.
|
210
224
|
# If <tt>query</tt> is a string, the search will be a basic search.
|
211
225
|
# If <tt>query</tt> is a hash, the search will be an advanced search.
|
212
|
-
def self.search(query,
|
213
|
-
if
|
214
|
-
|
215
|
-
elsif query.is_a?(
|
216
|
-
|
226
|
+
def self.search(query = nil, page=1, &block)
|
227
|
+
if block_given?
|
228
|
+
_advanced_search page, &block
|
229
|
+
elsif query.is_a?(String)
|
230
|
+
_basic_search query, page
|
217
231
|
else
|
218
|
-
raise ArgumentError, "expected
|
232
|
+
raise ArgumentError, "expected search to be a string or a block"
|
219
233
|
end
|
220
234
|
end
|
221
235
|
|
@@ -256,8 +270,9 @@ module Braintree
|
|
256
270
|
_init attributes
|
257
271
|
end
|
258
272
|
|
259
|
-
# True if <tt>other</tt>
|
273
|
+
# True if <tt>other</tt> is a Braintree::Transaction with the same id.
|
260
274
|
def ==(other)
|
275
|
+
return false unless other.is_a?(Transaction)
|
261
276
|
id == other.id
|
262
277
|
end
|
263
278
|
|
@@ -275,8 +290,8 @@ module Braintree
|
|
275
290
|
end
|
276
291
|
|
277
292
|
# Creates a credit transaction that refunds this transaction.
|
278
|
-
def refund
|
279
|
-
response = Http.post "/transactions/#{id}/refund"
|
293
|
+
def refund(amount = nil)
|
294
|
+
response = Http.post "/transactions/#{id}/refund", :transaction => {:amount => amount}
|
280
295
|
if response[:transaction]
|
281
296
|
# TODO: need response to return original_transaction so that we can update status, updated_at, etc.
|
282
297
|
SuccessfulResult.new(:new_transaction => Transaction._new(response[:transaction]))
|
@@ -380,29 +395,31 @@ module Braintree
|
|
380
395
|
end
|
381
396
|
end
|
382
397
|
|
383
|
-
def self._advanced_search(
|
384
|
-
|
385
|
-
|
398
|
+
def self._advanced_search(page, &block) # :nodoc:
|
399
|
+
search = TransactionSearch.new
|
400
|
+
block.call(search)
|
401
|
+
|
402
|
+
response = Http.post "/transactions/advanced_search?page=#{page}", {:search => search.to_hash}
|
386
403
|
attributes = response[:credit_card_transactions]
|
387
404
|
attributes[:items] = Util.extract_attribute_as_array(attributes, :transaction).map { |attrs| _new(attrs) }
|
388
|
-
|
405
|
+
|
406
|
+
ResourceCollection.new(attributes) { |page_number| Transaction.search(nil, page_number, &block) }
|
389
407
|
end
|
390
408
|
|
391
409
|
def self._attributes # :nodoc:
|
392
410
|
[:amount, :created_at, :credit_card_details, :customer_details, :id, :status, :type, :updated_at]
|
393
411
|
end
|
394
412
|
|
395
|
-
def self._basic_search(query,
|
396
|
-
page = options[:page] || 1
|
413
|
+
def self._basic_search(query, page) # :nodoc:
|
397
414
|
response = Http.get "/transactions/all/search?q=#{Util.url_encode(query)}&page=#{Util.url_encode(page)}"
|
398
415
|
attributes = response[:credit_card_transactions]
|
399
416
|
attributes[:items] = Util.extract_attribute_as_array(attributes, :transaction).map { |attrs| _new(attrs) }
|
400
|
-
ResourceCollection.new(attributes) { |page_number| Transaction.search(query,
|
417
|
+
ResourceCollection.new(attributes) { |page_number| Transaction.search(query, page_number) }
|
401
418
|
end
|
402
419
|
|
403
420
|
def self._create_signature # :nodoc:
|
404
421
|
[
|
405
|
-
:amount, :customer_id, :order_id, :payment_method_token, :type,
|
422
|
+
:amount, :customer_id, :merchant_account_id, :order_id, :payment_method_token, :type,
|
406
423
|
{:credit_card => [:token, :cardholder_name, :cvv, :expiration_date, :expiration_month, :expiration_year, :number]},
|
407
424
|
{:customer => [:id, :company, :email, :fax, :first_name, :last_name, :phone, :website]},
|
408
425
|
{:billing => [:first_name, :last_name, :company, :country_name, :extended_address, :locality, :postal_code, :region, :street_address]},
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Braintree
|
2
|
+
class TransactionSearch < AdvancedSearch
|
3
|
+
search_fields(
|
4
|
+
:billing_company,
|
5
|
+
:billing_country_name,
|
6
|
+
:billing_extended_address,
|
7
|
+
:billing_first_name,
|
8
|
+
:billing_last_name,
|
9
|
+
:billing_locality,
|
10
|
+
:billing_postal_code,
|
11
|
+
:billing_region,
|
12
|
+
:billing_street_address,
|
13
|
+
:credit_card_cardholder_name,
|
14
|
+
:currency,
|
15
|
+
:customer_company,
|
16
|
+
:customer_email,
|
17
|
+
:customer_fax,
|
18
|
+
:customer_first_name,
|
19
|
+
:customer_id,
|
20
|
+
:customer_last_name,
|
21
|
+
:customer_phone,
|
22
|
+
:customer_website,
|
23
|
+
:id,
|
24
|
+
:order_id,
|
25
|
+
:payment_method_token,
|
26
|
+
:processor_authorization_code,
|
27
|
+
:shipping_company,
|
28
|
+
:shipping_country_name,
|
29
|
+
:shipping_extended_address,
|
30
|
+
:shipping_first_name,
|
31
|
+
:shipping_last_name,
|
32
|
+
:shipping_locality,
|
33
|
+
:shipping_postal_code,
|
34
|
+
:shipping_region,
|
35
|
+
:shipping_street_address
|
36
|
+
)
|
37
|
+
|
38
|
+
equality_fields :credit_card_expiration_date
|
39
|
+
partial_match_fields :credit_card_number
|
40
|
+
|
41
|
+
multiple_value_field :created_using, :allows => [
|
42
|
+
Transaction::CreatedUsing::FullInformation,
|
43
|
+
Transaction::CreatedUsing::Token
|
44
|
+
]
|
45
|
+
multiple_value_field :credit_card_card_type, :allows => CreditCard::CardType::All
|
46
|
+
multiple_value_field :credit_card_customer_location, :allows => [
|
47
|
+
CreditCard::CustomerLocation::International,
|
48
|
+
CreditCard::CustomerLocation::US
|
49
|
+
]
|
50
|
+
multiple_value_field :merchant_account_id
|
51
|
+
multiple_value_field :status, :allows => Transaction::Status::All
|
52
|
+
multiple_value_field :source, :allows => [
|
53
|
+
Transaction::Source::Api,
|
54
|
+
Transaction::Source::ControlPanel,
|
55
|
+
Transaction::Source::Recurring
|
56
|
+
]
|
57
|
+
multiple_value_field :type
|
58
|
+
|
59
|
+
key_value_fields :refund
|
60
|
+
|
61
|
+
range_fields :amount, :created_at
|
62
|
+
end
|
63
|
+
end
|