activemerchant/active_merchant

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

Summary

Maintainability
C
1 day
Test Coverage
module ActiveMerchant #:nodoc:
  module Billing #:nodoc:
    class QvalentGateway < Gateway
      self.display_name = 'Qvalent'
      self.homepage_url = 'https://www.qvalent.com/'

      self.test_url = 'https://ccapi.client.support.qvalent.com/post/CreditCardAPIReceiver'
      self.live_url = 'https://ccapi.client.qvalent.com/post/CreditCardAPIReceiver'

      self.supported_countries = ['AU']
      self.default_currency = 'AUD'
      self.money_format = :cents
      self.supported_cardtypes = %i[visa master american_express discover jcb diners]

      CVV_CODE_MAPPING = {
        'S' => 'D'
      }

      def initialize(options = {})
        requires!(options, :username, :password, :merchant, :pem)
        super
      end

      def purchase(amount, payment_method, options = {})
        post = {}
        add_invoice(post, amount, options)
        add_order_number(post, options)
        add_payment_method(post, payment_method)
        add_verification_value(post, payment_method)
        add_stored_credential_data(post, payment_method, options)
        add_customer_data(post, options)
        add_soft_descriptors(post, options)
        add_customer_reference(post, options)

        commit('capture', post)
      end

      def authorize(amount, payment_method, options = {})
        post = {}
        add_invoice(post, amount, options)
        add_order_number(post, options)
        add_payment_method(post, payment_method)
        add_verification_value(post, payment_method)
        add_stored_credential_data(post, payment_method, options)
        add_customer_data(post, options)
        add_soft_descriptors(post, options)
        add_customer_reference(post, options)

        commit('preauth', post)
      end

      def capture(amount, authorization, options = {})
        post = {}
        add_invoice(post, amount, options)
        add_reference(post, authorization, options)
        add_customer_data(post, options)
        add_soft_descriptors(post, options)
        add_customer_reference(post, options)

        commit('captureWithoutAuth', post)
      end

      def refund(amount, authorization, options = {})
        post = {}
        add_invoice(post, amount, options)
        add_reference(post, authorization, options)
        add_customer_data(post, options)
        add_soft_descriptors(post, options)
        post['order.ECI'] = options[:eci] || 'SSL'
        add_customer_reference(post, options)

        commit('refund', post)
      end

      # Credit requires the merchant account to be enabled for "Adhoc Refunds"
      def credit(amount, payment_method, options = {})
        post = {}
        add_invoice(post, amount, options)
        add_order_number(post, options)
        add_payment_method(post, payment_method)
        add_customer_data(post, options)
        add_soft_descriptors(post, options)
        add_customer_reference(post, options)

        commit('refund', post)
      end

      def void(authorization, options = {})
        post = {}
        add_reference(post, authorization, options)
        add_customer_data(post, options)
        add_soft_descriptors(post, options)
        add_customer_reference(post, options)

        commit('reversal', post)
      end

      def store(payment_method, options = {})
        post = {}
        add_payment_method(post, payment_method)
        add_card_reference(post, options)

        commit('registerAccount', post)
      end

      def supports_scrubbing?
        true
      end

      def scrub(transcript)
        transcript.
          gsub(%r((&?customer.password=)[^&]*), '\1[FILTERED]').
          gsub(%r((&?card.PAN=)[^&]*), '\1[FILTERED]').
          gsub(%r((&?card.CVN=)[^&]*), '\1[FILTERED]')
      end

      private

      CURRENCY_CODES = Hash.new { |_h, k| raise ArgumentError.new("Unsupported currency: #{k}") }
      CURRENCY_CODES['AUD'] = 'AUD'
      CURRENCY_CODES['INR'] = 'INR'

      def add_soft_descriptors(post, options)
        post['customer.merchantName'] = options[:customer_merchant_name] if options[:customer_merchant_name]
        post['customer.merchantStreetAddress'] = options[:customer_merchant_street_address] if options[:customer_merchant_street_address]
        post['customer.merchantLocation'] = options[:customer_merchant_location] if options[:customer_merchant_location]
        post['customer.merchantState'] = options[:customer_merchant_state] if options[:customer_merchant_state]
        post['customer.merchantCountry'] = options[:customer_merchant_country] if options[:customer_merchant_country]
        post['customer.merchantPostCode'] = options[:customer_merchant_post_code] if options[:customer_merchant_post_code]
        post['customer.subMerchantId'] = options[:customer_sub_merchant_id] if options[:customer_sub_merchant_id]
      end

      def add_invoice(post, money, options)
        post['order.amount'] = amount(money)
        post['card.currency'] = CURRENCY_CODES[options[:currency] || currency(money)]
      end

      def add_payment_method(post, payment_method)
        post['card.cardHolderName'] = payment_method.name
        post['card.PAN'] = payment_method.number
        post['card.expiryYear'] = format(payment_method.year, :two_digits)
        post['card.expiryMonth'] = format(payment_method.month, :two_digits)
      end

      def add_stored_credential_data(post, payment_method, options)
        post['order.ECI'] = options[:eci] || eci(options)
        if (stored_credential = options[:stored_credential]) && %w(visa master).include?(payment_method.brand)
          post['card.posEntryMode'] = stored_credential[:initial_transaction] ? 'MANUAL' : 'STORED_CREDENTIAL'
          stored_credential_usage(post, payment_method, options) unless stored_credential[:initiator] && stored_credential[:initiator] == 'cardholder'
          post['order.authTraceId'] = stored_credential[:network_transaction_id] if stored_credential[:network_transaction_id]
        end
      end

      def stored_credential_usage(post, payment_method, options)
        return unless payment_method.brand == 'visa'

        stored_credential = options[:stored_credential]
        if stored_credential[:reason_type] == 'unscheduled'
          if stored_credential[:initiator] == 'merchant'
            post['card.storedCredentialUsage'] = 'UNSCHEDULED_MIT'
          elsif stored_credential[:initiator] == 'customer'
            post['card.storedCredentialUsage'] = 'UNSCHEDULED_CIT'
          end
        elsif stored_credential[:reason_type] == 'recurring'
          post['card.storedCredentialUsage'] = 'RECURRING'
        elsif stored_credential[:reason_type] == 'installment'
          post['card.storedCredentialUsage'] = 'INSTALLMENT'
        end
      end

      def eci(options)
        if options.dig(:stored_credential, :initial_transaction)
          'SSL'
        elsif options.dig(:stored_credential, :initiator) && options[:stored_credential][:initiator] == 'cardholder'
          'MTO'
        elsif options.dig(:stored_credential, :reason_type)
          case options[:stored_credential][:reason_type]
          when 'recurring'
            'REC'
          when 'installment'
            'INS'
          when 'unscheduled'
            'MTO'
          end
        else
          'SSL'
        end
      end

      def add_verification_value(post, payment_method)
        post['card.CVN'] = payment_method.verification_value
      end

      def add_card_reference(post, options)
        post['customer.customerReferenceNumber'] = options[:customer_reference_number] || options[:order_id]
      end

      def add_customer_reference(post, options)
        post['customer.customerReferenceNumber'] = options[:customer_reference_number] if options[:customer_reference_number]
      end

      def add_reference(post, authorization, options)
        post['customer.originalOrderNumber'] = authorization
        add_order_number(post, options)
      end

      def add_order_number(post, options)
        post['customer.orderNumber'] = options[:order_id] || SecureRandom.uuid
      end

      def add_customer_data(post, options)
        post['order.ipAddress'] = options[:ip] || '127.0.0.1'
        post['order.xid'] = options[:xid] if options[:xid]
        post['order.cavv'] = options[:cavv] if options[:cavv]
      end

      def commit(action, post)
        post['customer.username'] = @options[:username]
        post['customer.password'] = @options[:password]
        post['customer.merchant'] = @options[:merchant]
        post['order.type'] = action

        data = build_request(post)
        raw = parse(ssl_post(url(action), data, headers))

        succeeded = success_from(raw['response.responseCode'])
        Response.new(
          succeeded,
          message_from(succeeded, raw),
          raw,
          authorization: raw['response.orderNumber'] || raw['response.customerReferenceNumber'],
          cvv_result: cvv_result(succeeded, raw),
          error_code: error_code_from(succeeded, raw),
          test: test?
        )
      end

      def cvv_result(succeeded, raw)
        return unless succeeded

        code = CVV_CODE_MAPPING[raw['response.cvnResponse']] || raw['response.cvnResponse']
        CVVResult.new(code)
      end

      def headers
        {
          'Content-Type' => 'application/x-www-form-urlencoded'
        }
      end

      def build_request(post)
        post.to_query + '&message.end'
      end

      def url(action)
        (test? ? test_url : live_url)
      end

      def parse(body)
        result = {}
        body.to_s.each_line do |pair|
          result[$1] = $2 if pair.strip =~ /\A([^=]+)=(.+)\Z/im
        end
        result
      end

      def parse_element(response, node)
        if node.has_elements?
          node.elements.each { |element| parse_element(response, element) }
        else
          response[node.name.underscore.to_sym] = node.text
        end
      end

      SUCCESS_CODES = %w(00 08 10 11 16 QS QZ)

      def success_from(response)
        SUCCESS_CODES.include?(response)
      end

      def message_from(succeeded, response)
        if succeeded
          'Succeeded'
        else
          response['response.text'] || 'Unable to read error message'
        end
      end

      STANDARD_ERROR_CODE_MAPPING = {
        '14' => STANDARD_ERROR_CODE[:invalid_number],
        'QQ' => STANDARD_ERROR_CODE[:invalid_cvc],
        '33' => STANDARD_ERROR_CODE[:expired_card],
        'NT' => STANDARD_ERROR_CODE[:incorrect_address],
        '12' => STANDARD_ERROR_CODE[:card_declined],
        '06' => STANDARD_ERROR_CODE[:processing_error],
        '01' => STANDARD_ERROR_CODE[:call_issuer],
        '04' => STANDARD_ERROR_CODE[:pickup_card]
      }

      def error_code_from(succeeded, response)
        succeeded ? nil : STANDARD_ERROR_CODE_MAPPING[response['response.responseCode']]
      end
    end
  end
end