braintree 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|