activemerchant/active_merchant

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

Summary

Maintainability
C
7 hrs
Test Coverage
module ActiveMerchant #:nodoc:
  module Billing #:nodoc:
    class OptimalPaymentGateway < Gateway
      self.test_url = 'https://webservices.test.optimalpayments.com/creditcardWS/CreditCardServlet/v1'
      self.live_url = 'https://webservices.optimalpayments.com/creditcardWS/CreditCardServlet/v1'

      # The countries the gateway supports merchants from as 2 digit ISO country codes
      self.supported_countries = %w[CA US GB AU AT BE BG HR CY CZ DK
                                    EE FI DE GR HU IE IT LV LT LU MT
                                    NL NO PL PT RO SK SI ES SE CH]

      # The card types supported by the payment gateway
      self.supported_cardtypes = %i[visa master american_express discover diners_club]

      # The homepage URL of the gateway
      self.homepage_url = 'http://www.optimalpayments.com/'

      # The name of the gateway
      self.display_name = 'Optimal Payments'

      def initialize(options = {})
        if options[:login]
          ActiveMerchant.deprecated("The 'login' option is deprecated in favor of 'store_id' and will be removed in a future version.")
          options[:store_id] = options[:login]
        end

        if options[:account]
          ActiveMerchant.deprecated("The 'account' option is deprecated in favor of 'account_number' and will be removed in a future version.")
          options[:account_number] = options[:account]
        end

        requires!(options, :account_number, :store_id, :password)
        super
      end

      def authorize(money, card_or_auth, options = {})
        parse_card_or_auth(card_or_auth, options)
        commit("cc#{@stored_data}Authorize", money, options)
      end
      alias stored_authorize authorize # back-compat

      def purchase(money, card_or_auth, options = {})
        parse_card_or_auth(card_or_auth, options)
        commit("cc#{@stored_data}Purchase", money, options)
      end
      alias stored_purchase purchase # back-compat

      def refund(money, authorization, options = {})
        options[:confirmationNumber] = authorization
        commit('ccCredit', money, options)
      end

      def void(authorization, options = {})
        options[:confirmationNumber] = authorization
        commit('ccAuthorizeReversal', nil, options)
      end

      def capture(money, authorization, options = {})
        options[:confirmationNumber] = authorization
        commit('ccSettlement', money, options)
      end

      def verify(credit_card, options = {})
        parse_card_or_auth(credit_card, options)
        commit('ccVerification', 0, options)
      end

      def store(credit_card, options = {})
        verify(credit_card, options)
      end

      def supports_scrubbing?
        true
      end

      def scrub(transcript)
        transcript.
          gsub(%r((%3CstorePwd%3E).*(%3C(%2F|/)storePwd%3E))i, '\1[FILTERED]\2').
          gsub(%r((%3CcardNum%3E)\d*(%3C(%2F|/)cardNum%3E))i, '\1[FILTERED]\2').
          gsub(%r((%3Ccvd%3E)\d*(%3C(%2F|/)cvd%3E))i, '\1[FILTERED]\2')
      end

      private

      def parse_card_or_auth(card_or_auth, options)
        if card_or_auth.respond_to?(:number)
          @credit_card = card_or_auth
          @stored_data = ''
        else
          options[:confirmationNumber] = card_or_auth
          @stored_data = 'StoredData'
        end
      end

      def parse(body)
        REXML::Document.new(body || '')
      end

      def commit(action, money, post)
        post[:order_id] ||= 'order_id'

        xml =
          case action
          when 'ccAuthorize', 'ccPurchase', 'ccVerification'
            cc_auth_request(money, post)
          when 'ccCredit', 'ccSettlement'
            cc_post_auth_request(money, post)
          when 'ccStoredDataAuthorize', 'ccStoredDataPurchase'
            cc_stored_data_request(money, post)
          when 'ccAuthorizeReversal'
            cc_auth_reversal_request(post)
          # when 'ccCancelSettle', 'ccCancelCredit', 'ccCancelPayment'
          #  cc_cancel_request(money, post)
          # when 'ccPayment'
          #  cc_payment_request(money, post)
          # when 'ccAuthenticate'
          #  cc_authenticate_request(money, post)
          else
            raise 'Unknown Action'
          end
        txnRequest = escape_uri(xml)
        response = parse(ssl_post(test? ? self.test_url : self.live_url, "txnMode=#{action}&txnRequest=#{txnRequest}"))

        Response.new(
          successful?(response),
          message_from(response),
          hash_from_xml(response),
          test: test?,
          authorization: authorization_from(response),
          avs_result: { code: avs_result_from(response) },
          cvv_result: cvv_result_from(response)
        )
      end

      # The upstream is picky and so we can't use CGI.escape like we want to
      def escape_uri(uri)
        URI::DEFAULT_PARSER.escape(uri)
      end

      def successful?(response)
        REXML::XPath.first(response, '//decision').text == 'ACCEPTED' rescue false
      end

      def message_from(response)
        REXML::XPath.each(response, '//detail') do |detail|
          return detail.elements['value'].text if detail.is_a?(REXML::Element) && detail.elements['tag'].text == 'InternalResponseDescription'
        end
        nil
      end

      def authorization_from(response)
        get_text_from_document(response, '//confirmationNumber')
      end

      def avs_result_from(response)
        get_text_from_document(response, '//avsResponse')
      end

      def cvv_result_from(response)
        get_text_from_document(response, '//cvdResponse')
      end

      def hash_from_xml(response)
        hsh = {}
        %w(confirmationNumber authCode
           decision code description
           actionCode avsResponse cvdResponse
           txnTime duplicateFound).each do |tag|
          node = REXML::XPath.first(response, "//#{tag}")
          hsh[tag] = node.text if node
        end
        REXML::XPath.each(response, '//detail') do |detail|
          next unless detail.is_a?(REXML::Element)

          tag = detail.elements['tag'].text
          value = detail.elements['value'].text
          hsh[tag] = value
        end
        hsh
      end

      def xml_document(root_tag)
        xml = Builder::XmlMarkup.new indent: 2
        xml.tag!(root_tag, schema) do
          yield xml
        end
        xml.target!
      end

      def get_text_from_document(document, node)
        node = REXML::XPath.first(document, node)
        node&.text
      end

      def cc_auth_request(money, opts)
        xml_document('ccAuthRequestV1') do |xml|
          build_merchant_account(xml)
          xml.merchantRefNum opts[:order_id]
          xml.amount(money / 100.0)
          build_card(xml, opts)
          build_billing_details(xml, opts)
          build_shipping_details(xml, opts)
          xml.customerIP opts[:ip] if opts[:ip]
        end
      end

      def cc_auth_reversal_request(opts)
        xml_document('ccAuthReversalRequestV1') do |xml|
          build_merchant_account(xml)
          xml.confirmationNumber opts[:confirmationNumber]
          xml.merchantRefNum opts[:order_id]
        end
      end

      def cc_post_auth_request(money, opts)
        xml_document('ccPostAuthRequestV1') do |xml|
          build_merchant_account(xml)
          xml.confirmationNumber opts[:confirmationNumber]
          xml.merchantRefNum opts[:order_id]
          xml.amount(money / 100.0)
        end
      end

      def cc_stored_data_request(money, opts)
        xml_document('ccStoredDataRequestV1') do |xml|
          build_merchant_account(xml)
          xml.merchantRefNum opts[:order_id]
          xml.confirmationNumber opts[:confirmationNumber]
          xml.amount(money / 100.0)
        end
      end

      # untested
      #
      # def cc_cancel_request(opts)
      #   xml_document('ccCancelRequestV1') do |xml|
      #     build_merchant_account(xml)
      #     xml.confirmationNumber opts[:confirmationNumber]
      #   end
      # end
      #
      # def cc_payment_request(money, opts)
      #   xml_document('ccPaymentRequestV1') do |xml|
      #     build_merchant_account(xml)
      #     xml.merchantRefNum opts[:order_id]
      #     xml.amount(money/100.0)
      #     build_card(xml, opts)
      #     build_billing_details(xml, opts)
      #   end
      # end
      #
      # def cc_authenticate_request(opts)
      #   xml_document('ccAuthenticateRequestV1') do |xml|
      #     build_merchant_account(xml)
      #     xml.confirmationNumber opts[:confirmationNumber]
      #     xml.paymentResponse 'myPaymentResponse'
      #   end
      # end

      def schema
        { 'xmlns' => 'http://www.optimalpayments.com/creditcard/xmlschema/v1',
          'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance',
          'xsi:schemaLocation' => 'http://www.optimalpayments.com/creditcard/xmlschema/v1' }
      end

      def build_merchant_account(xml)
        xml.tag! 'merchantAccount' do
          xml.tag! 'accountNum', @options[:account_number]
          xml.tag! 'storeID',    @options[:store_id]
          xml.tag! 'storePwd',   @options[:password]
        end
      end

      def build_card(xml, opts)
        xml.tag! 'card' do
          xml.tag! 'cardNum', @credit_card.number
          xml.tag! 'cardExpiry' do
            xml.tag! 'month', @credit_card.month
            xml.tag! 'year', @credit_card.year
          end
          if brand = card_type(@credit_card.brand)
            xml.tag! 'cardType', brand
          end
          if @credit_card.verification_value?
            xml.tag! 'cvdIndicator', '1' # Value Provided
            xml.tag! 'cvd', @credit_card.verification_value
          else
            xml.tag! 'cvdIndicator', '0'
          end
        end
      end

      def build_billing_details(xml, opts)
        xml.tag! 'billingDetails' do
          xml.tag! 'cardPayMethod', 'WEB'
          build_address(xml, opts[:billing_address]) if opts[:billing_address]
          xml.tag! 'email', opts[:email] if opts[:email]
        end
      end

      def build_shipping_details(xml, opts)
        xml.tag! 'shippingDetails' do
          build_address(xml, opts[:shipping_address])
          xml.tag! 'email', opts[:email] if opts[:email]
        end if opts[:shipping_address].present?
      end

      def build_address(xml, addr)
        if addr[:name]
          first_name, last_name = split_names(addr[:name])
          xml.tag! 'firstName', first_name
          xml.tag! 'lastName', last_name
        end
        xml.tag! 'street', addr[:address1] if addr[:address1].present?
        xml.tag! 'street2', addr[:address2] if addr[:address2].present?
        xml.tag! 'city', addr[:city] if addr[:city].present?
        if addr[:state].present?
          state_tag = %w(US CA).include?(addr[:country]) ? 'state' : 'region'
          xml.tag! state_tag, addr[:state]
        end
        xml.tag! 'country', addr[:country] if addr[:country].present?
        xml.tag! 'zip', addr[:zip] if addr[:zip].present?
        xml.tag! 'phone', addr[:phone] if addr[:phone].present?
      end

      def card_type(key)
        { 'visa'            => 'VI',
          'master'          => 'MC',
          'american_express' => 'AM',
          'discover'        => 'DI',
          'diners_club'     => 'DC' }[key]
      end
    end
  end
end