yurijmi/better_offsite_payments

View on GitHub
lib/offsite_payments/integrations/authorize_net_sim.rb

Summary

Maintainability
C
1 day
Test Coverage
module OffsitePayments #:nodoc:
  module Integrations #:nodoc:
    module AuthorizeNetSim
      # Overwrite this if you want to change the ANS test url
      mattr_accessor :test_url
      self.test_url = 'https://test.authorize.net/gateway/transact.dll'

      # Overwrite this if you want to change the ANS production url
      mattr_accessor :production_url
      self.production_url = 'https://secure.authorize.net/gateway/transact.dll'

      def self.service_url
        mode = OffsitePayments.mode
        case mode
        when :production
          self.production_url
        when :test
          self.test_url
        else
          raise StandardError, "Integration mode set to an invalid value: #{mode}"
        end
      end

      def self.notification(post)
        Notification.new(post)
      end

      def self.return(query_string)
        Return.new(query_string)
      end

      # An example. Note the username as a parameter and transaction key you
      # will want to use later. The amount that you pass in will be *rounded*,
      # so preferably pass in X.2 decimal so that no rounding occurs. It is
      # rounded because if it looks like 00.000 Authorize.Net fails the
      # transaction as incorrectly formatted.
      #
      #  payment_service_for('order_id', 'authorize_net_account', :service => :authorize_net_sim,  :amount => 157.0) do |service|
      #
      #    # You must call setup_hash and invoice
      #
      #    service.setup_hash :transaction_key => '8CP6zJ7uD875J6tY',
      #        :order_timestamp => 1206836763
      #    service.customer_id 8
      #    service.customer :first_name => 'g',
      #                       :last_name => 'g',
      #                       :email => 'g@g.com',
      #                       :phone => '3'
      #   service.billing_address :zip => 'g',
      #                   :country => 'United States of America',
      #                   :address => 'g'
      #
      #   service.ship_to_address :first_name => 'g',
      #                            :last_name => 'g',
      #                            :city => '',
      #                            :address => 'g',
      #                            :address2 => '',
      #                            :state => address.state,
      #                            :country => 'United States of America',
      #                            :zip => 'g'
      #
      #   service.invoice "516428355" # your invoice number
      #   # The end-user is presented with the HTML produced by the notify_url.
      #   service.notify_url "http://t/authorize_net_sim/payment_received_notification_sub_step"
      #   service.payment_header 'My store name'
      #   service.add_line_item :name => 'item name', :quantity => 1, :unit_price => 0
      #   service.test_request 'true' # only if it's just a test
      #   service.shipping '25.0'
      #   # Tell it to display a "0" line item for shipping, with the price in
      #   # the name, otherwise it isn't shown at all, leaving the end user to
      #   # wonder why the total is different than the sum of the line items.
      #   service.add_shipping_as_line_item
      #   server.add_tax_as_line_item # same with tax
      #   # See the helper.rb file for various custom fields
      # end

      class Helper < OffsitePayments::Helper
        mapping :order, 'x_fp_sequence'
        mapping :account, 'x_login'

        mapping :customer, :first_name => 'x_first_name',
                           :last_name  => 'x_last_name',
                           :email      => 'x_email',
                           :phone      => 'x_phone'

        mapping :notify_url, 'x_relay_url'
        mapping :return_url, '' # unused
        mapping :cancel_return_url, '' # unused

        # Custom fields for Authorize.net SIM.
        # See http://www.Authorize.Net/support/SIM_guide.pdf for more descriptions.
        mapping :fax, 'x_fax'
        mapping :customer_id, 'x_cust_id'
        mapping :description, 'x_description'
        mapping :tax, 'x_tax'
        mapping :shipping, 'x_freight'

        # True or false, or 0 or 1 same effect [not required to send one,
        # defaults to false].
        mapping :test_request, 'x_test_request'

        # This one is necessary for the notify url to be able to parse its
        # information later! They also pass back customer id, if that's
        # useful.
        def invoice(number)
          add_field 'x_invoice_num', number
        end

        # Set the billing address. Call like service.billing_address {:city =>
        # 'provo, :state => 'UT'}...
        def billing_address(options)
          for setting in [:city, :state, :zip, :country, :po_num] do
            add_field 'x_' + setting.to_s, options[setting]
          end
          raise 'must use address1 and address2' if options[:address]
          add_field 'x_address', (options[:address1].to_s + ' ' + options[:address2].to_s).strip
        end

        # Adds a custom field which you submit to Authorize.Net. These fields
        # are all passed back to you verbatim when it does its relay
        # (callback) to you note that if you call it twice with the same name,
        # this function only uses keeps the second value you called it with.
        def add_custom_field(name, value)
          add_field name, value
        end

        # Displays tax as a line item, so they can see it. Otherwise it isn't
        # displayed.
        def add_tax_as_line_item
          raise unless @fields['x_tax']
          add_line_item :name => 'Total Tax', :quantity => 1, :unit_price => @fields['x_tax'], :tax => 0, :line_title => 'Tax'
        end

        # Displays shipping as a line item, so they can see it. Otherwise it
        # isn't displayed.
        def add_shipping_as_line_item(extra_options = {})
          raise 'must set shipping/freight before calling this' unless @fields['x_freight']
          add_line_item extra_options.merge({:name => 'Shipping and Handling Cost', :quantity => 1, :unit_price => @fields['x_freight'], :line_title => 'Shipping'})
        end

        # Add ship_to_address in the same format as the normal address is
        # added.
        def ship_to_address(options)
          for setting in [:first_name, :last_name, :company, :city, :state, :zip, :country] do
            if options[setting] then
              add_field 'x_ship_to_' + setting.to_s, options[setting]
            end
          end
          raise 'must use :address1 and/or :address2' if options[:address]
          add_field 'x_ship_to_address', (options[:address1].to_s + ' ' + options[:address2].to_s).strip
        end

        # These control the look of the SIM payment page. Note that you can
        # include a CSS header in descriptors, etc.
        mapping :color_link, 'x_color_link'
        mapping :color_text, 'x_color_text'
        mapping :logo_url, 'x_logo_url'
        mapping :background_url, 'x_background_url' # background image url for the page
        mapping :payment_header, 'x_header_html_payment_form'
        mapping :payment_footer, 'x_footer_html_payment_form'

        # For this to work you must have also passed in an email for the
        # purchaser.
        def yes_email_customer_from_authorizes_side
          add_field 'x_email_customer', 'TRUE'
        end

        # Add a line item to Authorize.Net.
        # Call line add_line_item {:name => 'orange', :unit_price => 30, :tax_value => 'Y', :quantity => 3, }
        # Note you can't pass in a negative unit price, and you can add an
        # optional :line_title => 'special name' if you don't want it to say
        # 'Item 1' or what not, the default coded here.
        # Cannot have a negative price, nor a name with "'s or $
        # You can use the :line_title for the product name and then :name for description, if desired
        def add_line_item(options)
          raise 'needs name' unless options[:name]

          if @line_item_count == 30
            # Add a note that we are not showing at least one -- AN doesn't
            # display more than 30 or so.
            description_of_last = @raw_html_fields[-1][1]
            # Pull off the second to last section, which is the description.
            description_of_last =~ />([^>]*)<\|>[YN]$/
            # Create a new description, which can't be too big, so truncate here.
            @raw_html_fields[-1][1] = description_of_last.gsub($1, $1[0..200] + ' + more unshown items after this one.')
          end

          name = options[:name]
          quantity = options[:quantity] || 1
          line_title = options[:line_title] || ('Item ' + (@line_item_count + 1).to_s) # left most field
          unit_price = options[:unit_price] || 0
          unit_price = unit_price.to_f.round(2)
          tax_value = options[:tax_value] || 'N'

          # Sanitization, in case they include a reserved word here, following
          # their guidelines; unfortunately, they require 'raw' fields here,
          # not CGI escaped, using their own delimiters.
          #
          # Authorize.net ignores the second field (sanitized_short_name)
          raise 'illegal char for line item <|>' if name.include? '<|>'
          raise 'illegal char for line item "' if name.include? '"'
          raise 'cannot pass in dollar sign' if unit_price.to_s.include? '$'
          raise 'must have positive or 0 unit price' if unit_price.to_f < 0
          # Using CGI::escape causes the output to be formated incorrectly in
          # the HTML presented to the end-user's browser (e.g., spaces turn
          # into +'s).
          sanitized_short_name = name[0..30]
          name = name[0..255]

          add_raw_html_field "x_line_item", "#{line_title}<|>#{sanitized_short_name}<|>#{name}<|>#{quantity}<|>#{unit_price}<|>#{tax_value}"

          @line_item_count += 1
        end

        # If you call this it will e-mail to this address a copy of a receipt
        # after successful, from Authorize.Net.
        def email_merchant_from_authorizes_side(to_this_email)
          add_field 'x_email_merchant', to_this_email
        end

        # You MUST call this at some point for it to actually work. Options
        # must include :transaction_key and :order_timestamp
        def setup_hash(options)
          raise unless options[:transaction_key]
          raise unless options[:order_timestamp]
          amount = @fields['x_amount']
          data = "#{@fields['x_login']}^#{@fields['x_fp_sequence']}^#{options[:order_timestamp].to_i}^#{amount}^#{@fields['x_currency_code']}"
          hmac = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('md5'), options[:transaction_key], data)
          add_field 'x_fp_hash', hmac
          add_field 'x_fp_timestamp', options[:order_timestamp].to_i
        end

        # Note that you should call #invoice and #setup_hash as well, for the
        # response_url to actually work.
        def initialize(order, account, options = {})
          super
          raise 'missing parameter' unless order and account and options[:amount]
          raise 'error -- amount with no digits!' unless options[:amount].to_s =~ /\d/
          add_field('x_type', 'AUTH_CAPTURE') # the only one we deal with, for now.  Not refunds or anything else, currently.
          add_field 'x_show_form', 'PAYMENT_FORM'
          add_field 'x_relay_response', 'TRUE'
          add_field 'x_duplicate_window', '28800' # large default duplicate window.
          add_field 'x_currency_code', currency_code
          add_field 'x_version' , '3.1' # version from doc
          add_field 'x_amount', options[:amount].to_f.round(2)
          @line_item_count = 0
        end
      end

      # # Example:
      # parser = AuthorizeNetSim::Notification.new(request.raw_post)
      # passed = parser.complete?
      #
      # order = Order.find_by_order_number(parser.invoice_num)
      #
      # unless order
      #   @message = 'Error--unable to find your transaction! Please contact us directly.'
      #   return render :partial => 'authorize_net_sim_payment_response'
      # end
      #
      # if order.total != parser.gross.to_f
      #   logger.error "Authorize.Net sim said they paid for #{parser.gross} and it should have been #{order.total}!"
      #   passed = false
      # end
      #
      # # Theoretically, Authorize.net will *never* pass us the same transaction
      # # ID twice, but we can double check that... by using
      # # parser.transaction_id, and checking against previous orders' transaction
      # # id's (which you can save when the order is completed)....
      # unless parser.acknowledge MD5_HASH_SET_IN_AUTHORIZE_NET, AUTHORIZE_LOGIN
      #  passed = false
      #  logger.error "ALERT POSSIBLE FRAUD ATTEMPT either that or you haven't setup your md5 hash setting right in #{__FILE__}
      #    because a transaction came back from Authorize.Net with the wrong hash value--rejecting!"
      # end
      #
      # unless parser.cavv_matches? and parser.avs_code_matches?
      #   logger.error 'Warning--non matching CC!' + params.inspect
      #   # Could fail them here, as well (recommended)...
      # end
      #
      # if passed
      #  # Set up your session, and render something that will redirect them to
      #  # your site, most likely.
      # else
      #  # Render failure or redirect them to your site where you will render failure
      # end
      class Notification < OffsitePayments::Notification

        def unescape(val) #:nodoc:
          if val
            CGI::unescape val
          else
            val
          end
        end

        # Passes a hash of the address the user entered in at Authorize.Net
        def billing_address
          all = {}
          [:fax, :city, :company, :last_name, :country, :zip, :first_name, :address, :email, :state].each do |key_out|
            all[key_out] = unescape params['x_' + key_out.to_s]
          end
          all
        end

        def customer_id
          unescape params['x_cust_id']
        end

        def auth_code
          unescape params['x_auth_code']
        end

        def po_num
         unescape params['x_po_num']
        end

        def ship_to_address
         all = {}
          [:city, :last_name, :first_name, :country, :zip, :address].each do |key_out|
            all[key_out] = unescape params['x_ship_to_' + key_out.to_s]
          end
          all
        end

        # Tax amount we sent them.
        def tax
          unescape params['x_tax']
        end

        # Transaction type (probably going to be auth_capture, since that's
        # all we set it as).
        def transaction_type
          unescape params['x_type']
        end

        # Payment method used--almost always CC (for credit card).
        def method
          unescape params['x_method']
        end

        # Ff our payment method is available. Almost always "true".
        def method_available
          params['x_method_available']
        end

        # Invoice num we passed in as invoice_num to them.
        def invoice_num
          item_id
        end

        # If you pass any values to authorize that aren't its expected, it
        # will pass them back to you verbatim, returned by this method.
        # custom values:
        def all_custom_values_passed_in_and_now_passed_back_to_us
          all = {}
          params.each do |key, value|
            if key[0..1] != 'x_'
              all[key] = unescape value
            end
          end
          all
        end

        def duty
          unescape params['x_duty']
        end

        # Shipping we sent them.
        def freight
          unescape params['x_freight']
        end
        alias_method :shipping, :freight

        def description
          unescape params['x_description']
        end

        # Returns the response code as a symbol.
        # {'1' => :approved, '2' => :declined, '3' => :error, '4' => :held_for_review}
        def response_code_as_ruby_symbol
          map = {'1' => :approved, '2' => :declined, '3' => :error, '4' => :held_for_review}
          map[params['x_response_code']]
        end

        def response_reason_text
          unescape params['x_response_reason_text']
        end

        # The response reason text's numeric id [equivalent--just a number]
        def response_reason_code
          unescape params['x_response_reason_code']
        end

        # 'used internally by their gateway'
        def response_subcode
          params['x_response_subcode']
        end

        # They pass back a tax_exempt value.
        def tax_exempt
          params['x_tax_exempt']
        end

        # avs [address verification] code
        # A = Address (Street)
        # matches, ZIP does not
        # B = Address information
        # not provided for AVS
        # check
        # E = AVS error
        # G = Non-U.S. Card Issuing
        # Bank
        # N = No Match on Address
        # (Street) or ZIP
        # P = AVS not applicable for
        # this transaction
        # R = Retry – System
        # unavailable or timed out
        # S = Service not supported
        # by issuer
        # U = Address information is
        # unavailable
        # W = Nine digit ZIP
        # matches, Address (Street)
        # does not
        # X = Address (Street) and
        # nine digit ZIP match
        # Y = Address (Street) and
        # five digit ZIP match
        # Z = Five digit ZIP matches
        # Address (Street) does not
        def avs_code
          params['x_avs_code']
        end

        # Returns true if their address completely matched [Y or X, P from
        # #avs_code, which mean 'add+zip match', 'address + 9-zip match', and
        # not applicable, respectively].
        def avs_code_matches?
          return ['Y', 'X', 'P'].include? params['x_avs_code']
        end

        # cvv2 response
        # M = Match
        # N = No Match
        # P = Not Processed
        # S = Should have been
        # present
        # U = Issuer unable to
        # process request
        def cvv2_resp_code
          params['x_cvv2_resp_code']
        end

        # check if #cvv2_resp_code == 'm' for Match.  otherwise false
        def cvv2_resp_code_matches?
          return ['M'].include? cvv2_resp_code
        end

        # cavv_response--'cardholder authentication verification response code'--most likely not use for SIM
        # Blank or not present  =
        # CAVV not validated
        # 0 = CAVV not validated
        # because erroneous data
        # was submitted
        # 1 = CAVV failed validation
        # 2 = CAVV passed
        # validation
        # 3 = CAVV validation could
        # not be performed; issuer
        # attempt incomplete
        # 4 = CAVV validation could
        # not be performed; issuer
        # system error
        # 5 = Reserved for future
        # use
        # 6 = Reserved for future
        # use
        # 7 = CAVV attempt – failed
        # validation – issuer
        # available (U.S.-issued
        # card/non-U.S acquirer)
        # 8 = CAVV attempt –
        # passed validation – issuer
        # available (U.S.-issued
        # card/non-U.S. acquirer)
        # 9 = CAVV attempt – failed
        # validation – issuer
        def cavv_response
          params['x_cavv_response']
        end

        # Check if #cavv_response == '', '2', '8' one of those [non failing]
        # [blank means no validated, 2 is passed, 8 is passed issuer
        # available]
        def cavv_matches?
          ['','2','8'].include? cavv_response
        end

        # Payment is complete -- returns true if x_response_code == '1'
        def complete?
          params["x_response_code"] == '1'
        end

        # Alias for invoice number--this is the only id they pass back to us
        # that we passed to them, except customer id is also passed back.
        def item_id
          unescape params['x_invoice_num']
        end

        # They return this number to us [it's unique to Authorize.net].
        def transaction_id
          params['x_trans_id']
        end

        # When was this payment was received by the client. --unimplemented --
        # always returns nil
        def received_at
          nil
        end

        # End-user's email
        def payer_email
          unescape params['x_email']
        end

        # They don't pass merchant email back to us -- unimplemented -- always
        # returns nil
        def receiver_email
          nil
        end

        # md5 hash used internally
        def security_key
          params['x_MD5_Hash']
        end

        # The money amount we received in X.2 decimal. Returns a string
        def gross
          unescape params['x_amount']
        end

        # Was this a test transaction?
        def test?
          params['x_test_request'] == 'true'
        end

        # #method_available alias
        def status
          complete?
        end

        # Called to request back and check if it was a valid request.
        # Authorize.net passes us back a hash that includes a hash of our
        # 'unique' MD5 value that we set within their system.
        #
        # Example:
        # acknowledge('my secret md5 hash that I set within Authorize.Net', 'authorize_login')
        #
        # Note this is somewhat unsafe unless you actually set that md5 hash
        # to something (defaults to '' in their system).
        def acknowledge(md5_hash_set_in_authorize_net, authorize_net_login_name)
          Digest::MD5.hexdigest(md5_hash_set_in_authorize_net + authorize_net_login_name + params['x_trans_id'] + gross) == params['x_MD5_Hash'].downcase
        end

       private

        # Take the posted data and move the relevant data into a hash.
        def parse(post)
          @raw = post
          post.split('&').each do |line|
            key, value = *line.scan( %r{^(\w+)\=(.*)$} ).flatten
            params[key] = value
          end
        end
      end
    end
  end
end