activemerchant/active_merchant

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

Summary

Maintainability
D
2 days
Test Coverage
module ActiveMerchant #:nodoc:
  module Billing #:nodoc:
    class SecurionPayGateway < Gateway
      self.test_url = 'https://api.securionpay.com/'
      self.live_url = 'https://api.securionpay.com/'

      self.supported_countries = %w(AD BE BG CH CY CZ DE DK EE ES FI FO FR GI GL GR GS GT HR HU IE IS IT LI LR LT
                                    LU LV MC MT MU MV MW NL NO PL RO SE SI)

      self.default_currency = 'USD'
      self.money_format = :cents
      self.supported_cardtypes = %i[visa master american_express discover jcb diners_club]

      self.homepage_url = 'https://securionpay.com/'
      self.display_name = 'SecurionPay'

      STANDARD_ERROR_CODE_MAPPING = {
        'incorrect_number' => STANDARD_ERROR_CODE[:incorrect_number],
        'invalid_number' => STANDARD_ERROR_CODE[:invalid_number],
        'invalid_expiry_month' => STANDARD_ERROR_CODE[:invalid_expiry_date],
        'invalid_expiry_year' => STANDARD_ERROR_CODE[:invalid_expiry_date],
        'invalid_cvc' => STANDARD_ERROR_CODE[:invalid_cvc],
        'expired_card' => STANDARD_ERROR_CODE[:expired_card],
        'insufficient_funds' => STANDARD_ERROR_CODE[:card_declined],
        'incorrect_cvc' => STANDARD_ERROR_CODE[:incorrect_cvc],
        'incorrect_zip' => STANDARD_ERROR_CODE[:incorrect_zip],
        'card_declined' => STANDARD_ERROR_CODE[:card_declined],
        'processing_error' => STANDARD_ERROR_CODE[:processing_error],
        'lost_or_stolen' => STANDARD_ERROR_CODE[:card_declined],
        'suspected_fraud' => STANDARD_ERROR_CODE[:card_declined],
        'expired_token' => STANDARD_ERROR_CODE[:card_declined]
      }

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

      def purchase(money, payment, options = {})
        post = create_post_for_auth_or_purchase(money, payment, options)
        commit('charges', post, options)
      end

      def authorize(money, payment, options = {})
        post = create_post_for_auth_or_purchase(money, payment, options)
        post[:captured] = 'false'
        commit('charges', post, options)
      end

      def capture(money, authorization, options = {})
        post = {}
        add_amount(post, money, options)
        commit("charges/#{CGI.escape(authorization)}/capture", post, options)
      end

      def refund(money, authorization, options = {})
        post = {}
        add_amount(post, money, options)
        commit("charges/#{CGI.escape(authorization)}/refund", post, options)
      end

      def void(authorization, options = {})
        commit("charges/#{CGI.escape(authorization)}/refund", {}, options)
      end

      def verify(credit_card, options = {})
        MultiResponse.run(:use_first_response) do |r|
          r.process { authorize(100, credit_card, options) }
          r.process(:ignore_result) { void(r.authorization, options) }
        end
      end

      def store(credit_card, options = {})
        if options[:customer_id].blank?
          MultiResponse.run() do |r|
            # create charge object
            r.process { authorize(100, credit_card, options) }
            # create customer and save card
            r.process { create_customer_add_card(r.authorization, options) }
            # void the charge
            r.process(:ignore_result) { void(r.params['metadata']['chargeId'], options) }
          end
        else
          verify(credit_card, options)
        end
      end

      def customer(options = {})
        if options[:customer_id].blank?
          return nil
        else
          commit("customers/#{CGI.escape(options[:customer_id])}", nil, options, :get)
        end
      end

      def supports_scrubbing?
        true
      end

      def scrub(transcript)
        transcript.
          gsub(%r((Authorization: Basic )\w+), '\1[FILTERED]').
          gsub(%r((card\[number\]=)\d+), '\1[FILTERED]').
          gsub(%r((card\[cvc\]=)\d+), '\1[FILTERED]')
      end

      private

      def create_customer_add_card(authorization, options)
        post = {}
        post[:email] = options[:email]
        post[:description] = options[:description]
        post[:card] = authorization
        post[:metadata] = {}
        post[:metadata][:chargeId] = authorization
        commit('customers', post, options)
      end

      def add_customer(post, payment, options)
        post[:customerId] = options[:customer_id] if options[:customer_id]
      end

      def add_customer_data(post, options)
        post[:description] = options[:description]
        post[:ip] =          options[:ip]
        post[:user_agent] =  options[:user_agent]
        post[:referrer] =    options[:referrer]
      end

      def create_post_for_auth_or_purchase(money, payment, options)
        post = {}
        add_amount(post, money, options, true)
        add_creditcard(post, payment, options)
        add_customer(post, payment, options)
        add_customer_data(post, options)
        add_external_three_ds(post, options)
        if options[:email]
          post[:metadata] = {}
          post[:metadata][:email] = options[:email]
        end
        post
      end

      def add_external_three_ds(post, options)
        return if options[:three_d_secure].blank?

        post[:threeDSecure] = {
          external: {
            version: options[:three_d_secure][:version],
            authenticationValue: options[:three_d_secure][:cavv],
            acsTransactionId: options[:three_d_secure][:acs_transaction_id],
            status: options[:three_d_secure][:authentication_response_status],
            eci: options[:three_d_secure][:eci]
          }.merge(xid_or_ds_trans_id(options[:three_d_secure]))
        }
      end

      def xid_or_ds_trans_id(three_ds)
        if three_ds[:version].to_f >= 2.0
          { dsTransactionId: three_ds[:ds_transaction_id] }
        else
          { xid: three_ds[:xid] }
        end
      end

      def validate_three_ds_params(three_ds)
        errors = {}
        supported_version = %w{1.0.2 2.1.0 2.2.0}.include?(three_ds[:version])
        supported_auth_response = ['Y', 'N', 'U', 'R', 'E', 'A', nil].include?(three_ds[:status])

        errors[:three_ds_version] = 'ThreeDs version not supported' unless supported_version
        errors[:auth_response] = 'Authentication response value not supported' unless supported_auth_response
        errors.compact!

        errors.present? ? Response.new(false, 'ThreeDs data is invalid', errors) : nil
      end

      def add_amount(post, money, options, include_currency = false)
        currency = (options[:currency] || default_currency)
        post[:amount] = localized_amount(money, currency)
        post[:currency] = currency.downcase if include_currency
      end

      def add_creditcard(post, creditcard, options)
        card = {}
        if creditcard.respond_to?(:number)
          card[:number] = creditcard.number
          card[:expMonth] = creditcard.month
          card[:expYear] = creditcard.year
          card[:cvc] = creditcard.verification_value if creditcard.verification_value?
          card[:cardholderName] = creditcard.name if creditcard.name

          post[:card] = card
          add_address(post, options)
        elsif creditcard.kind_of?(String)
          post[:card] = creditcard
        else
          raise ArgumentError.new("Unhandled payment method #{creditcard.class}.")
        end
      end

      def add_address(post, options)
        return unless post[:card]&.kind_of?(Hash)

        if address = options[:billing_address]
          post[:card][:addressLine1] = address[:address1] if address[:address1]
          post[:card][:addressLine2] = address[:address2] if address[:address2]
          post[:card][:addressCountry] = address[:country] if address[:country]
          post[:card][:addressZip] = address[:zip] if address[:zip]
          post[:card][:addressState] = address[:state] if address[:state]
          post[:card][:addressCity] = address[:city] if address[:city]
        end
      end

      def parse(body)
        JSON.parse(body)
      end

      def commit(url, parameters = nil, options = {}, method = nil)
        if parameters.present? && parameters[:threeDSecure].present?
          three_ds_errors = validate_three_ds_params(parameters[:threeDSecure][:external])
          return three_ds_errors if three_ds_errors
        end

        response = api_request(url, parameters, options, method)
        success = success?(response)

        Response.new(
          success,
          (success ? 'Transaction approved' : response['error']['message']),
          response,
          test: test?,
          authorization: authorization_from(url, response),
          error_code: (success ? nil : STANDARD_ERROR_CODE_MAPPING[response['error']['code']])
        )
      end

      def authorization_from(action, response)
        if action == 'customers' && success?(response) && response['cards'].present?
          response['cards'].first['id']
        else
          success?(response) ? response['id'] : (response.dig('error', 'charge') || response.dig('error', 'chargeId'))
        end
      end

      def success?(response)
        !response.key?('error')
      end

      def headers(options = {})
        secret_key = options[:secret_key] || @options[:secret_key]

        {
          'Authorization' => 'Basic ' + Base64.encode64(secret_key.to_s + ':').strip,
          'User-Agent' => "SecurionPay/v1 ActiveMerchantBindings/#{ActiveMerchant::VERSION}"
        }
      end

      def response_error(raw_response)
        parse(raw_response)
      rescue JSON::ParserError
        json_error(raw_response)
      end

      def post_data(params)
        return nil unless params

        params.map do |key, value|
          next if value.blank?

          if value.is_a?(Hash)
            h = {}
            value.each do |k, v|
              h["#{key}[#{k}]"] = v unless v.blank?
            end
            post_data(h)
          elsif value.is_a?(Array)
            value.map { |v| "#{key}[]=#{CGI.escape(v.to_s)}" }.join('&')
          else
            "#{key}=#{CGI.escape(value.to_s)}"
          end
        end.compact.join('&')
      end

      def api_request(endpoint, parameters = nil, options = {}, method = nil)
        raw_response = response = nil
        begin
          if method.blank?
            raw_response = ssl_post(self.live_url + endpoint, post_data(parameters), headers(options))
          else
            raw_response = ssl_request(method, self.live_url + endpoint, post_data(parameters), headers(options))
          end
          response = parse(raw_response)
        rescue ResponseError => e
          raw_response = e.response.body
          response = response_error(raw_response)
        rescue JSON::ParserError
          response = json_error(raw_response)
        end
        response
      end

      def json_error(raw_response, gateway_name = 'SecurionPay')
        msg = "Invalid response received from the #{gateway_name} API."
        msg += "  (The raw response returned by the API was #{raw_response.inspect})"
        {
          'error' => {
            'message' => msg
          }
        }
      end

      def test?
        @options[:secret_key]&.include?('_test_')
      end
    end
  end
end