braintree 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. data/LICENSE +22 -0
  2. data/README.rdoc +62 -0
  3. data/lib/braintree.rb +66 -0
  4. data/lib/braintree/address.rb +122 -0
  5. data/lib/braintree/base_module.rb +29 -0
  6. data/lib/braintree/configuration.rb +99 -0
  7. data/lib/braintree/credit_card.rb +231 -0
  8. data/lib/braintree/credit_card_verification.rb +31 -0
  9. data/lib/braintree/customer.rb +231 -0
  10. data/lib/braintree/digest.rb +20 -0
  11. data/lib/braintree/error_codes.rb +95 -0
  12. data/lib/braintree/error_result.rb +39 -0
  13. data/lib/braintree/errors.rb +29 -0
  14. data/lib/braintree/http.rb +105 -0
  15. data/lib/braintree/paged_collection.rb +55 -0
  16. data/lib/braintree/ssl_expiration_check.rb +28 -0
  17. data/lib/braintree/successful_result.rb +38 -0
  18. data/lib/braintree/test/credit_card_numbers.rb +50 -0
  19. data/lib/braintree/test/transaction_amounts.rb +10 -0
  20. data/lib/braintree/transaction.rb +360 -0
  21. data/lib/braintree/transaction/address_details.rb +15 -0
  22. data/lib/braintree/transaction/credit_card_details.rb +22 -0
  23. data/lib/braintree/transaction/customer_details.rb +13 -0
  24. data/lib/braintree/transparent_redirect.rb +110 -0
  25. data/lib/braintree/util.rb +94 -0
  26. data/lib/braintree/validation_error.rb +15 -0
  27. data/lib/braintree/validation_error_collection.rb +80 -0
  28. data/lib/braintree/version.rb +9 -0
  29. data/lib/braintree/xml.rb +12 -0
  30. data/lib/braintree/xml/generator.rb +80 -0
  31. data/lib/braintree/xml/libxml.rb +69 -0
  32. data/lib/braintree/xml/parser.rb +93 -0
  33. data/lib/ssl/securetrust_ca.crt +44 -0
  34. data/lib/ssl/valicert_ca.crt +18 -0
  35. data/spec/integration/braintree/address_spec.rb +352 -0
  36. data/spec/integration/braintree/credit_card_spec.rb +676 -0
  37. data/spec/integration/braintree/customer_spec.rb +664 -0
  38. data/spec/integration/braintree/http_spec.rb +201 -0
  39. data/spec/integration/braintree/test/transaction_amounts_spec.rb +29 -0
  40. data/spec/integration/braintree/transaction_spec.rb +900 -0
  41. data/spec/integration/spec_helper.rb +38 -0
  42. data/spec/script/httpsd.rb +27 -0
  43. data/spec/spec_helper.rb +41 -0
  44. data/spec/unit/braintree/address_spec.rb +86 -0
  45. data/spec/unit/braintree/configuration_spec.rb +190 -0
  46. data/spec/unit/braintree/credit_card_spec.rb +137 -0
  47. data/spec/unit/braintree/credit_card_verification_spec.rb +17 -0
  48. data/spec/unit/braintree/customer_spec.rb +103 -0
  49. data/spec/unit/braintree/digest_spec.rb +28 -0
  50. data/spec/unit/braintree/error_result_spec.rb +42 -0
  51. data/spec/unit/braintree/errors_spec.rb +81 -0
  52. data/spec/unit/braintree/http_spec.rb +42 -0
  53. data/spec/unit/braintree/paged_collection_spec.rb +128 -0
  54. data/spec/unit/braintree/ssl_expiration_check_spec.rb +92 -0
  55. data/spec/unit/braintree/successful_result_spec.rb +27 -0
  56. data/spec/unit/braintree/transaction/credit_card_details_spec.rb +22 -0
  57. data/spec/unit/braintree/transaction_spec.rb +136 -0
  58. data/spec/unit/braintree/transparent_redirect_spec.rb +154 -0
  59. data/spec/unit/braintree/util_spec.rb +142 -0
  60. data/spec/unit/braintree/validation_error_collection_spec.rb +128 -0
  61. data/spec/unit/braintree/validation_error_spec.rb +19 -0
  62. data/spec/unit/braintree/xml/libxml_spec.rb +51 -0
  63. data/spec/unit/braintree/xml_spec.rb +122 -0
  64. data/spec/unit/spec_helper.rb +1 -0
  65. metadata +118 -0
@@ -0,0 +1,105 @@
1
+ module Braintree
2
+ module Http # :nodoc:
3
+
4
+ def self.delete(path)
5
+ response = _http_do Net::HTTP::Delete, path
6
+ if response.code.to_i == 200
7
+ true
8
+ else
9
+ Util.raise_exception_for_status_code(response.code)
10
+ end
11
+ end
12
+
13
+ def self.get(path)
14
+ response = _http_do Net::HTTP::Get, path
15
+ if response.code.to_i == 200
16
+ Xml.hash_from_xml(_body(response))
17
+ else
18
+ Util.raise_exception_for_status_code(response.code)
19
+ end
20
+ end
21
+
22
+ def self.post(path, params = nil)
23
+ response = _http_do Net::HTTP::Post, path, _build_xml(params)
24
+ if response.code.to_i == 200 || response.code.to_i == 201 || response.code.to_i == 422
25
+ Xml.hash_from_xml(_body(response))
26
+ else
27
+ Util.raise_exception_for_status_code(response.code)
28
+ end
29
+ end
30
+
31
+ def self.put(path, params = nil)
32
+ response = _http_do Net::HTTP::Put, path, _build_xml(params)
33
+ if response.code.to_i == 200 || response.code.to_i == 201 || response.code.to_i == 422
34
+ Xml.hash_from_xml(_body(response))
35
+ else
36
+ Util.raise_exception_for_status_code(response.code)
37
+ end
38
+ end
39
+
40
+ def self._build_xml(params)
41
+ return nil if params.nil?
42
+ Braintree::Xml.hash_to_xml params
43
+ end
44
+
45
+ def self._http_do(http_verb, path, body = nil)
46
+ connection = Net::HTTP.new(Configuration.server, Configuration.port)
47
+ if Configuration.ssl?
48
+ connection.use_ssl = true
49
+ connection.verify_mode = OpenSSL::SSL::VERIFY_PEER
50
+ connection.ca_file = Configuration.ca_file
51
+ connection.verify_callback = proc { |preverify_ok, ssl_context| _verify_ssl_certificate(preverify_ok, ssl_context) }
52
+ end
53
+ connection.start do |http|
54
+ request = http_verb.new("#{Configuration.base_merchant_path}#{path}")
55
+ request["Accept"] = "application/xml"
56
+ request["User-Agent"] = "Braintree Ruby Gem #{Braintree::Version::String}"
57
+ request["Accept-Encoding"] = "gzip"
58
+ request["X-ApiVersion"] = Configuration::API_VERSION
59
+ request.basic_auth Configuration.public_key, Configuration.private_key
60
+ Configuration.logger.debug "[Braintree] [#{_current_time}] #{request.method} #{path}"
61
+ if body
62
+ request["Content-Type"] = "application/xml"
63
+ request.body = body
64
+ Configuration.logger.debug _format_and_sanitize_body_for_log(body)
65
+ end
66
+ response = http.request(request)
67
+ Configuration.logger.info "[Braintree] [#{_current_time}] #{request.method} #{path} #{response.code}"
68
+ Configuration.logger.debug "[Braintree] [#{_current_time}] #{response.code} #{response.message}"
69
+ if Configuration.logger.level == Logger::DEBUG
70
+ Configuration.logger.debug _format_and_sanitize_body_for_log(_body(response))
71
+ end
72
+ response
73
+ end
74
+ end
75
+
76
+ def self._body(response)
77
+ if response.header["Content-Encoding"] == "gzip"
78
+ Zlib::GzipReader.new(StringIO.new(response.body)).read
79
+ else
80
+ raise UnexpectedError, "expected a gzip'd response"
81
+ end
82
+ end
83
+
84
+ def self._current_time
85
+ Time.now.utc.strftime("%d/%b/%Y %H:%M:%S %Z")
86
+ end
87
+
88
+ def self._format_and_sanitize_body_for_log(input_xml)
89
+ formatted_xml = input_xml.gsub(/^/, "[Braintree] ")
90
+ formatted_xml = formatted_xml.gsub(/<number>(.{6}).+?(.{4})<\/number>/, '<number>\1******\2</number>')
91
+ formatted_xml = formatted_xml.gsub(/<cvv>.+?<\/cvv>/, '<cvv>***</cvv>')
92
+ formatted_xml
93
+ end
94
+
95
+ def self._verify_ssl_certificate(preverify_ok, ssl_context)
96
+ if preverify_ok != true || ssl_context.error != 0
97
+ err_msg = "SSL Verification failed -- Preverify: #{preverify_ok}, Error: #{ssl_context.error_string} (#{ssl_context.error})"
98
+ Configuration.logger.error err_msg
99
+ raise SSLCertificateError.new(err_msg)
100
+ end
101
+ true
102
+ end
103
+ end
104
+ end
105
+
@@ -0,0 +1,55 @@
1
+ module Braintree
2
+ class PagedCollection
3
+ include BaseModule
4
+ include Enumerable
5
+
6
+ attr_reader :current_page_number, :items, :next_page_number, :page_size, :previous_page_number, :total_items
7
+
8
+ def initialize(attributes, &block) # :nodoc:
9
+ set_instance_variables_from_hash attributes
10
+ @paging_block = block
11
+ end
12
+
13
+ # Returns the item from the current page at the given +index+.
14
+ def [](index)
15
+ @items[index]
16
+ end
17
+
18
+ # Yields each item on the current page.
19
+ def each(&block)
20
+ @items.each(&block)
21
+ end
22
+
23
+ # Returns the first item from the current page.
24
+ def first
25
+ @items.first
26
+ end
27
+
28
+ # Returns true if the page is the last page. False otherwise.
29
+ def last_page?
30
+ current_page_number == total_pages
31
+ end
32
+
33
+ # Retrieves the next page of records.
34
+ def next_page
35
+ if last_page?
36
+ return nil
37
+ end
38
+ @paging_block.call(next_page_number)
39
+ end
40
+
41
+ # The next page number. Returns +nil+ if on the last page.
42
+ def next_page_number
43
+ last_page? ? nil : current_page_number + 1
44
+ end
45
+
46
+ # Returns the total number of pages.
47
+ def total_pages
48
+ total = total_items / page_size
49
+ if total_items % page_size != 0
50
+ total += 1
51
+ end
52
+ total
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,28 @@
1
+ module Braintree
2
+ module SSLExpirationCheck # :nodoc:
3
+ class << self
4
+ attr_reader :ssl_expiration_dates_checked
5
+ end
6
+
7
+ def self.check_dates # :nodoc:
8
+ {
9
+ "QA" => qa_expiration_date,
10
+ "Sandbox" => sandbox_expiration_date
11
+ }.each do |host, expiration_date|
12
+ if Date.today + (3 * 30) > expiration_date
13
+ Configuration.logger.warn "[Braintree] The SSL Certificate for the #{host} environment will expire on #{expiration_date}. Please check for an updated client library."
14
+ end
15
+ end
16
+ @ssl_expiration_dates_checked = true
17
+ end
18
+
19
+
20
+ def self.sandbox_expiration_date # :nodoc:
21
+ Date.new(2010, 12, 1)
22
+ end
23
+
24
+ def self.qa_expiration_date # :nodoc:
25
+ Date.new(2010, 12, 1)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,38 @@
1
+ module Braintree
2
+ # A SuccessfulResult will be returned from non-bang methods when
3
+ # validations pass. It will provide access to the created resource.
4
+ # For example, when creating a customer, SuccessfulResult will
5
+ # respond to +customer+ like so:
6
+ #
7
+ # result = Customer.create(:first_name => "John")
8
+ # if result.success?
9
+ # # have a SuccessfulResult
10
+ # puts "Created customer #{result.customer.id}
11
+ # else
12
+ # # have an ErrorResult
13
+ # end
14
+ class SuccessfulResult
15
+ include BaseModule
16
+
17
+ def initialize(attributes = {}) # :nodoc:
18
+ @attrs = attributes.keys
19
+ singleton_class.class_eval do
20
+ attributes.each do |key, value|
21
+ define_method key do
22
+ value
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ def inspect # :nodoc:
29
+ inspected_attributes = @attrs.map { |attr| "#{attr}:#{send(attr).inspect}" }
30
+ "#<#{self.class} #{inspected_attributes}>"
31
+ end
32
+
33
+ # Always returns true.
34
+ def success?
35
+ true
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,50 @@
1
+ module Braintree
2
+ module Test # :nodoc:
3
+ # The constants contained in the Braintree::Test::CreditCardNumbers module provide
4
+ # credit card numbers that should be used when working in the sandbox environment. The sandbox
5
+ # will not accept any credit card numbers other than the ones listed below.
6
+ module CreditCardNumbers
7
+ AmExes = %w[
8
+ 378282246310005
9
+ 371449635398431
10
+ 378734493671000
11
+ ]
12
+ CarteBlanches = %w[30569309025904] # :nodoc:
13
+ DinersClubs = %w[38520000023237] # :nodoc:
14
+
15
+ Discovers = %w[
16
+ 6011111111111117
17
+ 6011000990139424
18
+ ]
19
+ JCBs = %w[3530111333300000 3566002020360505] # :nodoc:
20
+
21
+ MasterCard = "5555555555554444"
22
+ MasterCardInternational = "5105105105105100" # :nodoc:
23
+
24
+ MasterCards = %w[5105105105105100 5555555555554444]
25
+
26
+ Visa = "4012888888881881"
27
+ VisaInternational = "4009348888881881" # :nodoc:
28
+
29
+ Visas = %w[
30
+ 4009348888881881
31
+ 4012888888881881
32
+ 4111111111111111
33
+ 4222222222222
34
+ ]
35
+ Unknowns = %w[
36
+ 1000000000000008
37
+ ]
38
+
39
+ module FailsSandboxVerification
40
+ AmEx = "378734493671000"
41
+ Discover = "6011000990139424"
42
+ MasterCard = "5105105105105100"
43
+ Visa = "4222222222222"
44
+ Numbers = [AmEx, Discover, MasterCard, Visa]
45
+ end
46
+
47
+ All = AmExes + Discovers + MasterCards + Visas
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,10 @@
1
+ module Braintree
2
+ module Test # :nodoc:
3
+ # The constants in this module can be used to create transactions with
4
+ # the desired status in the sandbox environment.
5
+ module TransactionAmounts
6
+ Authorize = "1000.00"
7
+ Decline = "2000.00"
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,360 @@
1
+ module Braintree
2
+ # == Creating a Transaction
3
+ #
4
+ # At minimum, an amount, credit card number, and credit card expiration date are required. Minimalistic
5
+ # example:
6
+ # Braintree::Transaction.sale!(
7
+ # :amount => "100.00",
8
+ # :credit_card => {
9
+ # :number => "5105105105105100",
10
+ # :expiration_date => "05/2012"
11
+ # }
12
+ # )
13
+ #
14
+ # Full example:
15
+ #
16
+ # Braintree::Transaction.sale!(
17
+ # :amount => "100.00",
18
+ # :order_id => "123",
19
+ # :credit_card => {
20
+ # # if :token is omitted, the gateway will generate a token
21
+ # :token => "credit_card_123",
22
+ # :number => "5105105105105100",
23
+ # :expiration_date => "05/2011",
24
+ # :cvv => "123"
25
+ # },
26
+ # :customer => {
27
+ # # if :id is omitted, the gateway will generate an id
28
+ # :id => "customer_123",
29
+ # :first_name => "Dan",
30
+ # :last_name => "Smith",
31
+ # :company => "Braintree Payment Solutions",
32
+ # :email => "dan@example.com",
33
+ # :phone => "419-555-1234",
34
+ # :fax => "419-555-1235",
35
+ # :website => "http://braintreepaymentsolutions.com"
36
+ # },
37
+ # :billing => {
38
+ # :first_name => "Carl",
39
+ # :last_name => "Jones",
40
+ # :company => "Braintree",
41
+ # :street_address => "123 E Main St",
42
+ # :extended_address => "Suite 403",
43
+ # :locality => "Chicago",
44
+ # :region => "IL",
45
+ # :postal_code => "60622",
46
+ # :country_name => "United States of America"
47
+ # },
48
+ # :shipping => {
49
+ # :first_name => "Andrew",
50
+ # :last_name => "Mason",
51
+ # :company => "Braintree",
52
+ # :street_address => "456 W Main St",
53
+ # :extended_address => "Apt 2F",
54
+ # :locality => "Bartlett",
55
+ # :region => "IL",
56
+ # :postal_code => "60103",
57
+ # :country_name => "United States of America"
58
+ # }
59
+ # )
60
+ #
61
+ # == Storing in the Vault
62
+ #
63
+ # The customer and credit card information used for
64
+ # a transaction can be stored in the vault by setting
65
+ # <tt>transaction[options][store_in_vault]</tt> to true.
66
+ #
67
+ # transaction = Braintree::Transaction.create!(
68
+ # :customer => {
69
+ # :first_name => "Adam",
70
+ # :last_name => "Williams"
71
+ # },
72
+ # :credit_card => {
73
+ # :number => "5105105105105100",
74
+ # :expiration_date => "05/2012"
75
+ # },
76
+ # :options => {
77
+ # :store_in_vault => true
78
+ # }
79
+ # )
80
+ # transaction.customer_details.id
81
+ # # => "865534"
82
+ # transaction.credit_card_details.token
83
+ # # => "6b6m"
84
+ #
85
+ # == Submitting for Settlement
86
+ #
87
+ # This can only be done when the transction's
88
+ # status is +authorized+. If +amount+ is not specified, the full authorized amount will be
89
+ # settled. If you would like to settle less than the full authorized amount, pass the
90
+ # desired amount. You cannot settle more than the authorized amount.
91
+ #
92
+ # A transaction can be submitted for settlement when created by setting
93
+ # transaction[options][submit_for_settlement] to true.
94
+ #
95
+ # transaction = Braintree::Transaction.sale!(
96
+ # :amount => "100.00",
97
+ # :credit_card => {
98
+ # :number => "5105105105105100",
99
+ # :expiration_date => "05/2012"
100
+ # },
101
+ # :options => {
102
+ # :submit_for_settlement => true
103
+ # }
104
+ # )
105
+ class Transaction
106
+ include BaseModule
107
+
108
+ module Type # :nodoc:
109
+ Credit = "credit" # :nodoc:
110
+ Sale = "sale" # :nodoc:
111
+ end
112
+
113
+ attr_reader :avs_error_response_code, :avs_postal_code_response_code, :avs_street_address_response_code
114
+ attr_reader :amount, :created_at, :credit_card_details, :customer_details, :id, :status
115
+ attr_reader :order_id
116
+ attr_reader :billing_details, :shipping_details
117
+ # The response code from the processor.
118
+ attr_reader :processor_response_code
119
+ # Will either be "sale" or "credit"
120
+ attr_reader :type
121
+ attr_reader :updated_at
122
+
123
+ def self.create(attributes)
124
+ Util.verify_keys(_create_signature, attributes)
125
+ _do_create "/transactions", :transaction => attributes
126
+ end
127
+
128
+ def self.create!(attributes)
129
+ return_object_or_raise(:transaction) { create(attributes) }
130
+ end
131
+
132
+ def self.create_from_transparent_redirect(query_string)
133
+ params = TransparentRedirect.parse_and_validate_query_string query_string
134
+ _do_create("/transactions/all/confirm_transparent_redirect_request", :id => params[:id])
135
+ end
136
+
137
+ # The URL to use to create transactions via transparent redirect.
138
+ def self.create_transaction_url
139
+ "#{Braintree::Configuration.base_merchant_url}/transactions/all/create_via_transparent_redirect_request"
140
+ end
141
+
142
+ # Creates a credit transaction.
143
+ def self.credit(attributes)
144
+ create(attributes.merge(:type => 'credit'))
145
+ end
146
+
147
+ def self.credit!(attributes)
148
+ return_object_or_raise(:transaction) { credit(attributes) }
149
+ end
150
+
151
+ # Finds the transaction with the given id. Raises a Braintree::NotFoundError
152
+ # if the transaction cannot be found.
153
+ def self.find(id)
154
+ response = Http.get "/transactions/#{id}"
155
+ new(response[:transaction])
156
+ rescue NotFoundError
157
+ raise NotFoundError, "transaction with id #{id.inspect} not found"
158
+ end
159
+
160
+ # Creates a sale transaction.
161
+ def self.sale(attributes)
162
+ create(attributes.merge(:type => 'sale'))
163
+ end
164
+
165
+ def self.sale!(attributes)
166
+ return_object_or_raise(:transaction) { sale(attributes) }
167
+ end
168
+
169
+ # Returns a PagedCollection of transactions matching the search query.
170
+ # If <tt>query</tt> is a string, the search will be a basic search.
171
+ # If <tt>query</tt> is a hash, the search will be an advanced search.
172
+ def self.search(query, options = {})
173
+ if query.is_a?(String)
174
+ _basic_search query, options
175
+ elsif query.is_a?(Hash)
176
+ _advanced_search query, options
177
+ else
178
+ raise ArgumentError, "expected query to be a string or a hash"
179
+ end
180
+ end
181
+
182
+ # Submits transaction with +transaction_id+ for settlement.
183
+ def self.submit_for_settlement(transaction_id, amount = nil)
184
+ raise ArgumentError, "transaction_id is invalid" unless transaction_id =~ /\A[0-9a-z]+\z/
185
+ response = Http.put "/transactions/#{transaction_id}/submit_for_settlement", :transaction => {:amount => amount}
186
+ if response[:transaction]
187
+ SuccessfulResult.new(:transaction => new(response[:transaction]))
188
+ elsif response[:api_error_response]
189
+ ErrorResult.new(response[:api_error_response])
190
+ else
191
+ raise UnexpectedError, "expected :transaction or :response"
192
+ end
193
+ end
194
+
195
+ def self.submit_for_settlement!(transaction_id, amount = nil)
196
+ return_object_or_raise(:transaction) { submit_for_settlement(transaction_id, amount) }
197
+ end
198
+
199
+ # Voids the transaction with the given <tt>transaction_id</tt>
200
+ def self.void(transaction_id)
201
+ response = Http.put "/transactions/#{transaction_id}/void"
202
+ if response[:transaction]
203
+ SuccessfulResult.new(:transaction => new(response[:transaction]))
204
+ elsif response[:api_error_response]
205
+ ErrorResult.new(response[:api_error_response])
206
+ else
207
+ raise UnexpectedError, "expected :transaction or :api_error_response"
208
+ end
209
+ end
210
+
211
+ def self.void!(transaction_id)
212
+ return_object_or_raise(:transaction) { void(transaction_id) }
213
+ end
214
+
215
+ def initialize(attributes) # :nodoc:
216
+ _init attributes
217
+ end
218
+
219
+ # True if <tt>other</tt> has the same id.
220
+ def ==(other)
221
+ id == other.id
222
+ end
223
+
224
+ def inspect # :nodoc:
225
+ first = [:id, :type, :amount, :status]
226
+ order = first + (self.class._attributes - first)
227
+ nice_attributes = order.map do |attr|
228
+ "#{attr}: #{send(attr).inspect}"
229
+ end
230
+ "#<#{self.class} #{nice_attributes.join(', ')}>"
231
+ end
232
+
233
+ # Creates a credit transaction that refunds this transaction.
234
+ def refund
235
+ response = Http.post "/transactions/#{id}/refund"
236
+ if response[:transaction]
237
+ # TODO: need response to return original_transaction so that we can update status, updated_at, etc.
238
+ SuccessfulResult.new(:new_transaction => Transaction._new(response[:transaction]))
239
+ elsif response[:api_error_response]
240
+ ErrorResult.new(response[:api_error_response])
241
+ else
242
+ raise UnexpectedError, "expected :transaction or :api_error_response"
243
+ end
244
+ end
245
+
246
+ # Returns true if the transaction has been refunded. False otherwise.
247
+ def refunded?
248
+ !@refund_id.nil?
249
+ end
250
+
251
+ # Submits the transaction for settlement.
252
+ def submit_for_settlement(amount = nil)
253
+ response = Http.put "/transactions/#{id}/submit_for_settlement", :transaction => {:amount => amount}
254
+ if response[:transaction]
255
+ _init(response[:transaction])
256
+ SuccessfulResult.new :transaction => self
257
+ elsif response[:api_error_response]
258
+ ErrorResult.new(response[:api_error_response])
259
+ else
260
+ raise UnexpectedError, "expected transaction or api_error_response"
261
+ end
262
+ end
263
+
264
+ def submit_for_settlement!(amount = nil)
265
+ return_object_or_raise(:transaction) { submit_for_settlement(amount) }
266
+ end
267
+
268
+ # If this transaction was stored in the vault, or created from vault records,
269
+ # vault_credit_card will return the associated Braintree::CreditCard. Because the
270
+ # vault credit card can be updated after the transaction was created, the attributes
271
+ # on vault_credit_card may not match the attributes on credit_card_details.
272
+ def vault_credit_card
273
+ return nil if credit_card_details.token.nil?
274
+ CreditCard.find(credit_card_details.token)
275
+ end
276
+
277
+ # If this transaction was stored in the vault, or created from vault records,
278
+ # vault_customer will return the associated Braintree::Customer. Because the
279
+ # vault customer can be updated after the transaction was created, the attributes
280
+ # on vault_customer may not match the attributes on customer_details.
281
+ def vault_customer
282
+ return nil if customer_details.id.nil?
283
+ Customer.find(customer_details.id)
284
+ end
285
+
286
+ # Voids the transaction.
287
+ def void
288
+ response = Http.put "/transactions/#{id}/void"
289
+ if response[:transaction]
290
+ _init response[:transaction]
291
+ SuccessfulResult.new(:transaction => self)
292
+ elsif response[:api_error_response]
293
+ ErrorResult.new(response[:api_error_response])
294
+ else
295
+ raise UnexpectedError, "expected :transaction or :api_error_response"
296
+ end
297
+ end
298
+
299
+ def void!
300
+ return_object_or_raise(:transaction) { void }
301
+ end
302
+
303
+ class << self
304
+ protected :new
305
+ def _new(*args) # :nodoc:
306
+ self.new *args
307
+ end
308
+ end
309
+
310
+ def self._do_create(url, params) # :nodoc:
311
+ response = Http.post url, params
312
+ if response[:transaction]
313
+ SuccessfulResult.new(:transaction => new(response[:transaction]))
314
+ elsif response[:api_error_response]
315
+ ErrorResult.new(response[:api_error_response])
316
+ else
317
+ raise UnexpectedError, "expected :transaction or :api_error_response"
318
+ end
319
+ end
320
+
321
+ def self._advanced_search(query, options) # :nodoc:
322
+ page = options[:page] || 1
323
+ response = Http.post "/transactions/advanced_search?page=#{Util.url_encode(page)}", :search => query
324
+ attributes = response[:credit_card_transactions]
325
+ attributes[:items] = Util.extract_attribute_as_array(attributes, :transaction).map { |attrs| _new(attrs) }
326
+ PagedCollection.new(attributes) { |page_number| Transaction.search(query, :page => page_number) }
327
+ end
328
+
329
+ def self._attributes # :nodoc:
330
+ [:amount, :created_at, :credit_card_details, :customer_details, :id, :status, :type, :updated_at]
331
+ end
332
+
333
+ def self._basic_search(query, options) # :nodoc:
334
+ page = options[:page] || 1
335
+ response = Http.get "/transactions/all/search?q=#{Util.url_encode(query)}&page=#{Util.url_encode(page)}"
336
+ attributes = response[:credit_card_transactions]
337
+ attributes[:items] = Util.extract_attribute_as_array(attributes, :transaction).map { |attrs| _new(attrs) }
338
+ PagedCollection.new(attributes) { |page_number| Transaction.search(query, :page => page_number) }
339
+ end
340
+
341
+ def self._create_signature # :nodoc:
342
+ [
343
+ :amount, :customer_id, :order_id, :payment_method_token, :type,
344
+ {:credit_card => [:token, :cvv, :expiration_date, :number]},
345
+ {:customer => [:id, :company, :email, :fax, :first_name, :last_name, :phone, :website]},
346
+ {:billing => [:first_name, :last_name, :company, :country_name, :extended_address, :locality, :postal_code, :region, :street_address]},
347
+ {:shipping => [:first_name, :last_name, :company, :country_name, :extended_address, :locality, :postal_code, :region, :street_address]},
348
+ {:options => [:store_in_vault, :submit_for_settlement]}
349
+ ]
350
+ end
351
+
352
+ def _init(attributes) # :nodoc:
353
+ set_instance_variables_from_hash(attributes)
354
+ @credit_card_details = CreditCardDetails.new(@credit_card)
355
+ @customer_details = CustomerDetails.new(@customer)
356
+ @billing_details = AddressDetails.new(@billing)
357
+ @shipping_details = AddressDetails.new(@shipping)
358
+ end
359
+ end
360
+ end