braintree 1.0.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.
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