braintree 2.0.0 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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 = "the_merchant_id"
17
- Braintree::Configuration.public_key = "a_public_key"
18
- Braintree::Configuration.private_key = "a_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",
@@ -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 TextNode < SearchNode
18
- operators :is, :is_not, :ends_with, :starts_with, :contains
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
- def self.search_fields(*fields)
44
- fields.each do |field|
45
- define_method(field) do
46
- TextNode.new(field, self)
47
- end
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] = value
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.new(subscription_hash) }
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
- _update_signature + [:customer_id]
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 => [:company, :country_name, :extended_address, :first_name, :last_name, :locality, :postal_code, :region, :street_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 => new(response[: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
- new(response[:subscription])
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| new(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 => new(response[: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, options = {})
213
- if query.is_a?(String)
214
- _basic_search query, options
215
- elsif query.is_a?(Hash)
216
- _advanced_search query, options
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 query to be a string or a hash"
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> has the same id.
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(query, options) # :nodoc:
384
- page = options[:page] || 1
385
- response = Http.post "/transactions/advanced_search?page=#{Util.url_encode(page)}", :search => query
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
- ResourceCollection.new(attributes) { |page_number| Transaction.search(query, :page => page_number) }
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, options) # :nodoc:
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, :page => page_number) }
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