/
transaction.rb
406 lines (370 loc) · 14.8 KB
/
transaction.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
module Braintree
# == Creating a Transaction
#
# At minimum, an amount, credit card number, and credit card expiration date are required. Minimalistic
# example:
# Braintree::Transaction.sale!(
# :amount => "100.00",
# :credit_card => {
# :number => "5105105105105100",
# :expiration_date => "05/2012"
# }
# )
#
# Full example:
#
# Braintree::Transaction.sale!(
# :amount => "100.00",
# :order_id => "123",
# :credit_card => {
# # if :token is omitted, the gateway will generate a token
# :token => "credit_card_123",
# :number => "5105105105105100",
# :expiration_date => "05/2011",
# :cvv => "123"
# },
# :customer => {
# # if :id is omitted, the gateway will generate an id
# :id => "customer_123",
# :first_name => "Dan",
# :last_name => "Smith",
# :company => "Braintree Payment Solutions",
# :email => "dan@example.com",
# :phone => "419-555-1234",
# :fax => "419-555-1235",
# :website => "http://braintreepaymentsolutions.com"
# },
# :billing => {
# :first_name => "Carl",
# :last_name => "Jones",
# :company => "Braintree",
# :street_address => "123 E Main St",
# :extended_address => "Suite 403",
# :locality => "Chicago",
# :region => "IL",
# :postal_code => "60622",
# :country_name => "United States of America"
# },
# :shipping => {
# :first_name => "Andrew",
# :last_name => "Mason",
# :company => "Braintree",
# :street_address => "456 W Main St",
# :extended_address => "Apt 2F",
# :locality => "Bartlett",
# :region => "IL",
# :postal_code => "60103",
# :country_name => "United States of America"
# },
# :custom_fields => {
# :birthdate => "11/13/1954"
# }
# )
#
# == Storing in the Vault
#
# The customer and credit card information used for
# a transaction can be stored in the vault by setting
# <tt>transaction[options][store_in_vault]</tt> to true.
#
# transaction = Braintree::Transaction.sale!(
# :customer => {
# :first_name => "Adam",
# :last_name => "Williams"
# },
# :credit_card => {
# :number => "5105105105105100",
# :expiration_date => "05/2012"
# },
# :options => {
# :store_in_vault => true
# }
# )
# transaction.customer_details.id
# # => "865534"
# transaction.credit_card_details.token
# # => "6b6m"
#
# To also store the billing address in the vault, pass the
# +add_billing_address_to_payment_method+ option.
#
# Braintree::Transaction.sale!(
# # ...
# :options => {
# :store_in_vault => true
# :add_billing_address_to_payment_method => true
# }
# )
#
# == Submitting for Settlement
#
# This can only be done when the transction's
# status is +authorized+. If +amount+ is not specified, the full authorized amount will be
# settled. If you would like to settle less than the full authorized amount, pass the
# desired amount. You cannot settle more than the authorized amount.
#
# A transaction can be submitted for settlement when created by setting
# transaction[options][submit_for_settlement] to true.
#
# transaction = Braintree::Transaction.sale!(
# :amount => "100.00",
# :credit_card => {
# :number => "5105105105105100",
# :expiration_date => "05/2012"
# },
# :options => {
# :submit_for_settlement => true
# }
# )
class Transaction
include BaseModule
module Type # :nodoc:
Credit = "credit" # :nodoc:
Sale = "sale" # :nodoc:
end
attr_reader :avs_error_response_code, :avs_postal_code_response_code, :avs_street_address_response_code
attr_reader :amount, :created_at, :credit_card_details, :customer_details, :id, :status
attr_reader :custom_fields
attr_reader :cvv_response_code
attr_reader :order_id
attr_reader :billing_details, :shipping_details
attr_reader :status_history
# The authorization code from the processor.
attr_reader :processor_authorization_code
# The response code from the processor.
attr_reader :processor_response_code
# The response text from the processor.
attr_reader :processor_response_text
# Will either be "sale" or "credit"
attr_reader :type
attr_reader :updated_at
def self.create(attributes)
Util.verify_keys(_create_signature, attributes)
_do_create "/transactions", :transaction => attributes
end
def self.create!(attributes)
return_object_or_raise(:transaction) { create(attributes) }
end
def self.create_from_transparent_redirect(query_string)
params = TransparentRedirect.parse_and_validate_query_string query_string
_do_create("/transactions/all/confirm_transparent_redirect_request", :id => params[:id])
end
# The URL to use to create transactions via transparent redirect.
def self.create_transaction_url
"#{Braintree::Configuration.base_merchant_url}/transactions/all/create_via_transparent_redirect_request"
end
# Creates a credit transaction.
def self.credit(attributes)
create(attributes.merge(:type => 'credit'))
end
def self.credit!(attributes)
return_object_or_raise(:transaction) { credit(attributes) }
end
# Finds the transaction with the given id. Raises a Braintree::NotFoundError
# if the transaction cannot be found.
def self.find(id)
response = Http.get "/transactions/#{id}"
new(response[:transaction])
rescue NotFoundError
raise NotFoundError, "transaction with id #{id.inspect} not found"
end
# Creates a sale transaction.
def self.sale(attributes)
create(attributes.merge(:type => 'sale'))
end
def self.sale!(attributes)
return_object_or_raise(:transaction) { sale(attributes) }
end
# Returns a PagedCollection of transactions matching the search query.
# If <tt>query</tt> is a string, the search will be a basic search.
# If <tt>query</tt> is a hash, the search will be an advanced search.
def self.search(query, options = {})
if query.is_a?(String)
_basic_search query, options
elsif query.is_a?(Hash)
_advanced_search query, options
else
raise ArgumentError, "expected query to be a string or a hash"
end
end
# Submits transaction with +transaction_id+ for settlement.
def self.submit_for_settlement(transaction_id, amount = nil)
raise ArgumentError, "transaction_id is invalid" unless transaction_id =~ /\A[0-9a-z]+\z/
response = Http.put "/transactions/#{transaction_id}/submit_for_settlement", :transaction => {:amount => amount}
if response[:transaction]
SuccessfulResult.new(:transaction => new(response[:transaction]))
elsif response[:api_error_response]
ErrorResult.new(response[:api_error_response])
else
raise UnexpectedError, "expected :transaction or :response"
end
end
def self.submit_for_settlement!(transaction_id, amount = nil)
return_object_or_raise(:transaction) { submit_for_settlement(transaction_id, amount) }
end
# Voids the transaction with the given <tt>transaction_id</tt>
def self.void(transaction_id)
response = Http.put "/transactions/#{transaction_id}/void"
if response[:transaction]
SuccessfulResult.new(:transaction => new(response[:transaction]))
elsif response[:api_error_response]
ErrorResult.new(response[:api_error_response])
else
raise UnexpectedError, "expected :transaction or :api_error_response"
end
end
def self.void!(transaction_id)
return_object_or_raise(:transaction) { void(transaction_id) }
end
def initialize(attributes) # :nodoc:
_init attributes
end
# True if <tt>other</tt> has the same id.
def ==(other)
id == other.id
end
def inspect # :nodoc:
first = [:id, :type, :amount, :status]
order = first + (self.class._attributes - first)
nice_attributes = order.map do |attr|
if attr == :amount
self.amount ? "amount: #{self.amount.to_s("F").inspect}" : "amount: nil"
else
"#{attr}: #{send(attr).inspect}"
end
end
"#<#{self.class} #{nice_attributes.join(', ')}>"
end
# Creates a credit transaction that refunds this transaction.
def refund
response = Http.post "/transactions/#{id}/refund"
if response[:transaction]
# TODO: need response to return original_transaction so that we can update status, updated_at, etc.
SuccessfulResult.new(:new_transaction => Transaction._new(response[:transaction]))
elsif response[:api_error_response]
ErrorResult.new(response[:api_error_response])
else
raise UnexpectedError, "expected :transaction or :api_error_response"
end
end
# Returns true if the transaction has been refunded. False otherwise.
def refunded?
!@refund_id.nil?
end
# Submits the transaction for settlement.
def submit_for_settlement(amount = nil)
response = Http.put "/transactions/#{id}/submit_for_settlement", :transaction => {:amount => amount}
if response[:transaction]
_init(response[:transaction])
SuccessfulResult.new :transaction => self
elsif response[:api_error_response]
ErrorResult.new(response[:api_error_response])
else
raise UnexpectedError, "expected transaction or api_error_response"
end
end
def submit_for_settlement!(amount = nil)
return_object_or_raise(:transaction) { submit_for_settlement(amount) }
end
# If this transaction was stored in the vault, or created from vault records,
# vault_billing_address will return the associated Braintree::Address. Because the
# vault billing address can be updated after the transaction was created, the attributes
# on vault_billing_address may not match the attributes on billing_details.
def vault_billing_address
return nil if billing_details.id.nil?
Address.find(customer_details.id, billing_details.id)
end
# If this transaction was stored in the vault, or created from vault records,
# vault_credit_card will return the associated Braintree::CreditCard. Because the
# vault credit card can be updated after the transaction was created, the attributes
# on vault_credit_card may not match the attributes on credit_card_details.
def vault_credit_card
return nil if credit_card_details.token.nil?
CreditCard.find(credit_card_details.token)
end
# If this transaction was stored in the vault, or created from vault records,
# vault_customer will return the associated Braintree::Customer. Because the
# vault customer can be updated after the transaction was created, the attributes
# on vault_customer may not match the attributes on customer_details.
def vault_customer
return nil if customer_details.id.nil?
Customer.find(customer_details.id)
end
# If this transaction was stored in the vault, or created from vault records,
# vault_shipping_address will return the associated Braintree::Address. Because the
# vault shipping address can be updated after the transaction was created, the attributes
# on vault_shipping_address may not match the attributes on shipping_details.
def vault_shipping_address
return nil if shipping_details.id.nil?
Address.find(customer_details.id, shipping_details.id)
end
# Voids the transaction.
def void
response = Http.put "/transactions/#{id}/void"
if response[:transaction]
_init response[:transaction]
SuccessfulResult.new(:transaction => self)
elsif response[:api_error_response]
ErrorResult.new(response[:api_error_response])
else
raise UnexpectedError, "expected :transaction or :api_error_response"
end
end
def void!
return_object_or_raise(:transaction) { void }
end
class << self
protected :new
def _new(*args) # :nodoc:
self.new *args
end
end
def self._do_create(url, params) # :nodoc:
response = Http.post url, params
if response[:transaction]
SuccessfulResult.new(:transaction => new(response[:transaction]))
elsif response[:api_error_response]
ErrorResult.new(response[:api_error_response])
else
raise UnexpectedError, "expected :transaction or :api_error_response"
end
end
def self._advanced_search(query, options) # :nodoc:
page = options[:page] || 1
response = Http.post "/transactions/advanced_search?page=#{Util.url_encode(page)}", :search => query
attributes = response[:credit_card_transactions]
attributes[:items] = Util.extract_attribute_as_array(attributes, :transaction).map { |attrs| _new(attrs) }
PagedCollection.new(attributes) { |page_number| Transaction.search(query, :page => page_number) }
end
def self._attributes # :nodoc:
[:amount, :created_at, :credit_card_details, :customer_details, :id, :status, :type, :updated_at]
end
def self._basic_search(query, options) # :nodoc:
page = options[:page] || 1
response = Http.get "/transactions/all/search?q=#{Util.url_encode(query)}&page=#{Util.url_encode(page)}"
attributes = response[:credit_card_transactions]
attributes[:items] = Util.extract_attribute_as_array(attributes, :transaction).map { |attrs| _new(attrs) }
PagedCollection.new(attributes) { |page_number| Transaction.search(query, :page => page_number) }
end
def self._create_signature # :nodoc:
[
:amount, :customer_id, :order_id, :payment_method_token, :type,
{:credit_card => [:token, :cvv, :expiration_date, :expiration_month, :expiration_year, :number]},
{:customer => [:id, :company, :email, :fax, :first_name, :last_name, :phone, :website]},
{:billing => [:first_name, :last_name, :company, :country_name, :extended_address, :locality, :postal_code, :region, :street_address]},
{:shipping => [:first_name, :last_name, :company, :country_name, :extended_address, :locality, :postal_code, :region, :street_address]},
{:options => [:store_in_vault, :submit_for_settlement, :add_billing_address_to_payment_method, :store_shipping_address_in_vault]},
{:custom_fields => :_any_key_}
]
end
def _init(attributes) # :nodoc:
set_instance_variables_from_hash(attributes)
@amount = Util.to_big_decimal(amount)
@credit_card_details = CreditCardDetails.new(@credit_card)
@customer_details = CustomerDetails.new(@customer)
@billing_details = AddressDetails.new(@billing)
@shipping_details = AddressDetails.new(@shipping)
@status_history = attributes[:status_history] ? attributes[:status_history].map { |s| StatusDetails.new(s) } : []
end
end
end