Shopify/active_merchant

View on GitHub
lib/active_merchant/billing/gateways/secure_pay.rb

Summary

Maintainability
C
1 day
Test Coverage
require 'active_merchant/billing/gateways/authorize_net'

module ActiveMerchant #:nodoc:
  module Billing #:nodoc:
    class SecurePayGateway < Gateway
      API_VERSION = '3.1'

      self.live_url = self.test_url = 'https://www.securepay.com/AuthSpayAdapter/process.aspx'

      class_attribute :duplicate_window

      APPROVED, DECLINED, ERROR, FRAUD_REVIEW = 1, 2, 3, 4

      RESPONSE_CODE, RESPONSE_REASON_CODE, RESPONSE_REASON_TEXT, AUTHORIZATION_CODE = 0, 2, 3, 4
      AVS_RESULT_CODE, TRANSACTION_ID, CARD_CODE_RESPONSE_CODE, CARDHOLDER_AUTH_CODE = 5, 6, 38, 39

      self.default_currency = 'USD'

      self.supported_countries = %w(US CA GB AU)
      self.supported_cardtypes = %i[visa master american_express discover diners_club jcb]
      self.homepage_url = 'http://www.securepay.com/'
      self.display_name = 'SecurePay'

      CARD_CODE_ERRORS = %w(N S)
      AVS_ERRORS = %w(A E N R W Z)
      AVS_REASON_CODES = %w(27 45)
      TRANSACTION_ALREADY_ACTIONED = %w(310 311)

      def initialize(options = {})
        requires!(options, :login, :password)
        super
      end

      def purchase(money, paysource, options = {})
        post = {}
        add_currency_code(post, money, options)
        add_invoice(post, options)
        add_payment_source(post, paysource, options)
        add_address(post, options)
        add_customer_data(post, options)
        add_duplicate_window(post)

        commit('AUTH_CAPTURE', money, post)
      end

      private

      def commit(action, money, parameters)
        parameters[:amount] = amount(money) unless action == 'VOID'

        url = (test? ? self.test_url : self.live_url)
        data = ssl_post(url, post_data(action, parameters))

        response          = parse(data)
        response[:action] = action

        message = message_from(response)

        Response.new(
          success?(response),
          message,
          response,
          test: test?,
          authorization: response[:transaction_id],
          fraud_review: fraud_review?(response),
          avs_result: { code: response[:avs_result_code] },
          cvv_result: response[:card_code]
        )
      end

      def success?(response)
        response[:response_code] == APPROVED && TRANSACTION_ALREADY_ACTIONED.exclude?(response[:response_reason_code])
      end

      def fraud_review?(response)
        response[:response_code] == FRAUD_REVIEW
      end

      def parse(body)
        fields = split(body)

        {
          response_code: fields[RESPONSE_CODE].to_i,
          response_reason_code: fields[RESPONSE_REASON_CODE],
          response_reason_text: fields[RESPONSE_REASON_TEXT],
          avs_result_code: fields[AVS_RESULT_CODE],
          transaction_id: fields[TRANSACTION_ID],
          card_code: fields[CARD_CODE_RESPONSE_CODE],
          authorization_code: fields[AUTHORIZATION_CODE],
          cardholder_authentication_code: fields[CARDHOLDER_AUTH_CODE]
        }
      end

      def post_data(action, parameters = {})
        post = {}

        post[:version]        = API_VERSION
        post[:login]          = @options[:login]
        post[:tran_key]       = @options[:password]
        post[:relay_response] = 'FALSE'
        post[:type]           = action
        post[:delim_data]     = 'TRUE'
        post[:delim_char]     = ','
        post[:encap_char]     = '$'
        post[:solution_ID]    = application_id if application_id

        post.merge(parameters).collect { |key, value| "x_#{key}=#{CGI.escape(value.to_s)}" }.join('&')
      end

      def add_currency_code(post, money, options)
        post[:currency_code] = options[:currency] || currency(money)
      end

      def add_invoice(post, options)
        post[:invoice_num] = options[:order_id]
        post[:description] = options[:description]
      end

      def add_creditcard(post, creditcard, options = {})
        post[:card_num]   = creditcard.number
        post[:card_code]  = creditcard.verification_value if creditcard.verification_value?
        post[:exp_date]   = expdate(creditcard)
        post[:first_name] = creditcard.first_name
        post[:last_name]  = creditcard.last_name
      end

      def add_payment_source(params, source, options = {})
        add_creditcard(params, source, options)
      end

      def add_customer_data(post, options)
        if options.has_key? :email
          post[:email] = options[:email]
          post[:email_customer] = false
        end

        if options.has_key? :customer
          post[:cust_id] = options[:customer] if Float(options[:customer]) rescue nil
        end

        post[:customer_ip] = options[:ip] if options.has_key? :ip

        post[:cardholder_authentication_value] = options[:cardholder_authentication_value] if options.has_key? :cardholder_authentication_value

        post[:authentication_indicator] = options[:authentication_indicator] if options.has_key? :authentication_indicator
      end

      # x_duplicate_window won't be sent by default, because sending it changes the response.
      # "If this field is present in the request with or without a value, an enhanced duplicate transaction response will be sent."
      # (as of 2008-12-30) http://www.authorize.net/support/AIM_guide_SCC.pdf
      def add_duplicate_window(post)
        post[:duplicate_window] = duplicate_window if duplicate_window
      end

      def add_address(post, options)
        if address = options[:billing_address] || options[:address]
          post[:address] = address[:address1].to_s
          post[:company] = address[:company].to_s
          post[:phone]   = address[:phone].to_s
          post[:zip]     = address[:zip].to_s
          post[:city]    = address[:city].to_s
          post[:country] = address[:country].to_s
          post[:state]   = address[:state].blank? ? 'n/a' : address[:state]
        end

        if address = options[:shipping_address]
          post[:ship_to_first_name] = address[:first_name].to_s
          post[:ship_to_last_name] = address[:last_name].to_s
          post[:ship_to_address] = address[:address1].to_s
          post[:ship_to_company] = address[:company].to_s
          post[:ship_to_phone]   = address[:phone].to_s
          post[:ship_to_zip]     = address[:zip].to_s
          post[:ship_to_city]    = address[:city].to_s
          post[:ship_to_country] = address[:country].to_s
          post[:ship_to_state]   = address[:state].blank? ? 'n/a' : address[:state]
        end
      end

      def message_from(results)
        if results[:response_code] == DECLINED
          return CVVResult.messages[results[:card_code]] if CARD_CODE_ERRORS.include?(results[:card_code])
          return AVSResult.messages[results[:avs_result_code]] if AVS_REASON_CODES.include?(results[:response_reason_code]) && AVS_ERRORS.include?(results[:avs_result_code])
        end

        (results[:response_reason_text] ? results[:response_reason_text].chomp('.') : '')
      end

      def split(response)
        response.split(',')
      end
    end
  end
end