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 # transaction[options][store_in_vault] 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 :order_id attr_reader :billing_details, :shipping_details attr_reader :status_history # The response code from the processor. attr_reader :processor_response_code # 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 query is a string, the search will be a basic search. # If query 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 transaction_id 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 other 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| "#{attr}: #{send(attr).inspect}" 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_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 # 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, :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]}, {:custom_fields => :_any_key_} ] end def _init(attributes) # :nodoc: set_instance_variables_from_hash(attributes) @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