Shopify/active_merchant

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

Summary

Maintainability
D
2 days
Test Coverage
module ActiveMerchant #:nodoc:
  module Billing #:nodoc:
    class DeepstackGateway < Gateway
      self.test_url = 'https://api.sandbox.deepstack.io'
      self.live_url = 'https://api.deepstack.io'

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

      self.homepage_url = 'https://deepstack.io/'
      self.display_name = 'Deepstack Gateway'

      STANDARD_ERROR_CODE_MAPPING = {}

      def initialize(options = {})
        requires!(options, :publishable_api_key, :app_id, :shared_secret)
        @publishable_api_key, @app_id, @shared_secret = options.values_at(:publishable_api_key, :app_id, :shared_secret)
        super
      end

      def purchase(money, payment, options = {})
        post = {}
        add_payment(post, payment, options)
        add_order(post, money, options)
        add_purchase_capture(post)
        add_address(post, payment, options)
        add_customer_data(post, options)
        commit('sale', post)
      end

      def authorize(money, payment, options = {})
        post = {}
        add_payment(post, payment, options)
        add_order(post, money, options)
        add_address(post, payment, options)
        add_customer_data(post, options)

        commit('auth', post)
      end

      def capture(money, authorization, options = {})
        post = {}
        add_invoice(post, money, authorization, options)

        commit('capture', post)
      end

      def refund(money, authorization, options = {})
        post = {}
        add_invoice(post, money, authorization, options)
        commit('refund', post)
      end

      def void(money, authorization, options = {})
        post = {}
        add_invoice(post, money, authorization, options)
        commit('void', post)
      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(0, r.authorization, options) }
        end
      end

      def get_token(credit_card, options = {})
        post = {}
        add_payment_instrument(post, credit_card, options)
        add_address_payment_instrument(post, credit_card, options)
        commit('gettoken', post)
      end

      def supports_scrubbing?
        true
      end

      def scrub(transcript)
        transcript.
          gsub(%r((Authorization: Bearer )\w+), '\1[FILTERED]').
          gsub(%r((Authorization: Basic )\w+), '\1[FILTERED]').
          gsub(%r((Hmac: )[\w=]+), '\1[FILTERED]').
          gsub(%r((\\"account_number\\":\\")[\w*]+), '\1[FILTERED]').
          gsub(%r((\\"cvv\\":\\")\w+), '\1[FILTERED]').
          gsub(%r((\\"expiration\\":\\")\w+), '\1[FILTERED]')
      end

      private

      def add_customer_data(post, options)
        post[:meta] ||= {}

        add_shipping(post, options) if options.key?(:shipping_address)
        post[:meta][:client_customer_id] = options[:customer] if options[:customer]
        post[:meta][:client_transaction_id] = options[:order_id] if options[:order_id]
        post[:meta][:client_transaction_description] = options[:description] if options[:description]
        post[:meta][:client_invoice_id] = options[:invoice] if options[:invoice]
        post[:meta][:card_holder_ip_address] = options[:ip] if options[:ip]
      end

      def add_address(post, creditcard, options)
        return post unless options.key?(:address) || options.key?(:billing_address)

        billing_address = options[:address] || options[:billing_address]
        post[:source] ||= {}

        post[:source][:billing_contact] = {}
        post[:source][:billing_contact][:first_name] = billing_address[:first_name] if billing_address[:first_name]
        post[:source][:billing_contact][:last_name] = billing_address[:last_name] if billing_address[:last_name]
        post[:source][:billing_contact][:phone] = billing_address[:phone] if billing_address[:phone]
        post[:source][:billing_contact][:email] = options[:email] if options[:email]
        post[:source][:billing_contact][:address] = {}
        post[:source][:billing_contact][:address][:line_1] = billing_address[:address1] if billing_address[:address1]
        post[:source][:billing_contact][:address][:line_2] = billing_address[:address2] if billing_address[:address2]
        post[:source][:billing_contact][:address][:city] = billing_address[:city] if billing_address[:city]
        post[:source][:billing_contact][:address][:state] = billing_address[:state] if billing_address[:state]
        post[:source][:billing_contact][:address][:postal_code] = billing_address[:zip] if billing_address[:zip]
        post[:source][:billing_contact][:address][:country_code] = billing_address[:country] if billing_address[:country]
      end

      def add_address_payment_instrument(post, creditcard, options)
        return post unless options.key?(:address) || options.key?(:billing_address)

        billing_address = options[:address] || options[:billing_address]
        post[:source] = {} unless post.key?(:payment_instrument)

        post[:payment_instrument][:billing_contact] = {}
        post[:payment_instrument][:billing_contact][:first_name] = billing_address[:first_name] if billing_address[:first_name]
        post[:payment_instrument][:billing_contact][:last_name] = billing_address[:last_name] if billing_address[:last_name]
        post[:payment_instrument][:billing_contact][:phone] = billing_address[:phone] if billing_address[:phone]
        post[:payment_instrument][:billing_contact][:email] = billing_address[:email] if billing_address[:email]
        post[:payment_instrument][:billing_contact][:address] = {}
        post[:payment_instrument][:billing_contact][:address][:line_1] = billing_address[:address1] if billing_address[:address1]
        post[:payment_instrument][:billing_contact][:address][:line_2] = billing_address[:address2] if billing_address[:address2]
        post[:payment_instrument][:billing_contact][:address][:city] = billing_address[:city] if billing_address[:city]
        post[:payment_instrument][:billing_contact][:address][:state] = billing_address[:state] if billing_address[:state]
        post[:payment_instrument][:billing_contact][:address][:postal_code] = billing_address[:zip] if billing_address[:zip]
        post[:payment_instrument][:billing_contact][:address][:country_code] = billing_address[:country] if billing_address[:country]
      end

      def add_shipping(post, options = {})
        return post unless options.key?(:shipping_address)

        shipping = options[:shipping_address]
        post[:meta][:shipping_info] = {}
        post[:meta][:shipping_info][:first_name] = shipping[:first_name] if shipping[:first_name]
        post[:meta][:shipping_info][:last_name] = shipping[:last_name] if shipping[:last_name]
        post[:meta][:shipping_info][:phone] = shipping[:phone] if shipping[:phone]
        post[:meta][:shipping_info][:email] = shipping[:email] if shipping[:email]
        post[:meta][:shipping_info][:address] = {}
        post[:meta][:shipping_info][:address][:line_1] = shipping[:address1] if shipping[:address1]
        post[:meta][:shipping_info][:address][:line_2] = shipping[:address2] if shipping[:address2]
        post[:meta][:shipping_info][:address][:city] = shipping[:city] if shipping[:city]
        post[:meta][:shipping_info][:address][:state] = shipping[:state] if shipping[:state]
        post[:meta][:shipping_info][:address][:postal_code] = shipping[:zip] if shipping[:zip]
        post[:meta][:shipping_info][:address][:country_code] = shipping[:country] if shipping[:country]
      end

      def add_invoice(post, money, authorization, options)
        post[:amount] = amount(money)
        post[:charge] = authorization
      end

      def add_payment(post, payment, options)
        if payment.kind_of?(String)
          post[:source] = {}
          post[:source][:type] = 'card_on_file'
          post[:source][:card_on_file] = {}
          post[:source][:card_on_file][:id] = payment
          post[:source][:card_on_file][:cvv] = options[:verification_value] || ''
          post[:source][:card_on_file][:customer_id] = options[:customer_id] || ''
        # credit card object
        elsif payment.respond_to?(:number)
          post[:source] = {}
          post[:source][:type] = 'credit_card'
          post[:source][:credit_card] = {}
          post[:source][:credit_card][:account_number] = payment.number
          post[:source][:credit_card][:cvv] = payment.verification_value || ''
          post[:source][:credit_card][:expiration] = '%02d%02d' % [payment.month, payment.year % 100]
          post[:source][:credit_card][:customer_id] = options[:customer_id] || ''
        end
      end

      def add_payment_instrument(post, creditcard, options)
        if creditcard.kind_of?(String)
          post[:source] = creditcard
          return post
        end
        return post unless creditcard.respond_to?(:number)

        post[:payment_instrument] = {}
        post[:payment_instrument][:type] = 'credit_card'
        post[:payment_instrument][:credit_card] = {}
        post[:payment_instrument][:credit_card][:account_number] = creditcard.number
        post[:payment_instrument][:credit_card][:expiration] = '%02d%02d' % [creditcard.month, creditcard.year % 100]
        post[:payment_instrument][:credit_card][:cvv] = creditcard.verification_value
      end

      def add_order(post, amount, options)
        post[:transaction] ||= {}

        post[:transaction][:amount] = amount
        post[:transaction][:cof_type] = options.key?(:cof_type) ? options[:cof_type].upcase : 'UNSCHEDULED_CARDHOLDER'
        post[:transaction][:capture] = false # Change this in the request (auth/charge)
        post[:transaction][:currency_code] = (options[:currency] || currency(amount).upcase)
        post[:transaction][:avs] = options[:avs] || true # default avs to true unless told otherwise
        post[:transaction][:save_payment_instrument] = options[:save_payment_instrument] || false
      end

      def add_purchase_capture(post)
        post[:transaction] ||= {}
        post[:transaction][:capture] = true
      end

      def parse(body)
        return {} if !body || body.empty?

        JSON.parse(body)
      end

      def commit(action, parameters, method = 'POST')
        url = (test? ? test_url : live_url)
        if no_hmac(action)
          request_headers = headers.merge(create_basic(parameters, action))
        else
          request_headers = headers.merge(create_hmac(parameters, method))
        end
        request_url = url + get_url(action)
        begin
          response = parse(ssl_post(request_url, post_data(action, parameters), request_headers))
          Response.new(
            success_from(response),
            message_from(response),
            response,
            authorization: authorization_from(response),
            avs_result: AVSResult.new(code: response['avs_result']),
            cvv_result: CVVResult.new(response['cvv_result']),
            test: test?,
            error_code: error_code_from(response)
          )
        rescue ResponseError => e
          Response.new(
            false,
            message_from_error(e.response.body),
            response_error(e.response.body)
          )
        rescue JSON::ParserError
          Response.new(
            false,
            message_from(response),
            json_error(response)
          )
        end
      end

      def headers
        {
          'Accept' => 'text/plain',
          'Content-Type' => 'application/json'
        }
      end

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

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

      def success_from(response)
        success = false
        if response.key?('response_code')
          success = response['response_code'] == '00'
        # Hack because token/payment instrument methods do not return a response_code
        elsif response.key?('id')
          success = true if response['id'].start_with?('tok', 'card')
        end

        return success
      end

      def message_from(response)
        response = JSON.parse(response) if response.is_a?(String)
        if response.key?('message')
          return response['message']
        elsif response.key?('detail')
          return response['detail']
        end
      end

      def message_from_error(response)
        if response.is_a?(String)
          response.gsub!('\\"', '"')
          response = JSON.parse(response)
        end

        if response.key?('detail')
          return response['detail']
        elsif response.key?('message')
          return response['message']
        end
      end

      def authorization_from(response)
        response['id']
      end

      def post_data(action, parameters = {})
        return JSON.generate(parameters)
      end

      def error_code_from(response)
        error_code = nil
        error_code = response['response_code'] unless success_from(response)
        if error = response.dig('detail')
          error_code = error
        elsif error = response.dig('error')
          error_code = error.dig('reason', 'id')
        end
        error_code
      end

      def get_url(action)
        base = '/api/v1/'
        case action
        when 'sale'
          return base + 'payments/charge'
        when 'auth'
          return base + 'payments/charge'
        when 'capture'
          return base + 'payments/capture'
        when 'void'
          return base + 'payments/refund'
        when 'refund'
          return base + 'payments/refund'
        when 'gettoken'
          return base + 'vault/token'
        when 'vault'
          return base + 'vault/payment-instrument/token'
        else
          return base + 'noaction'
        end
      end

      def no_hmac(action)
        case action
        when 'gettoken'
          return true
        else
          return false
        end
      end

      def create_basic(post, method)
        return { 'Authorization' => "Bearer #{@publishable_api_key}" }
      end

      def create_hmac(post, method)
        # Need requestDate, requestMethod, Nonce, AppIDKey
        app_id_key = @app_id
        request_method = method.upcase
        uuid = SecureRandom.uuid
        request_time = Time.now.utc.strftime('%Y-%m-%dT%H:%M:%S.%LZ')

        string_to_hash = "#{app_id_key}|#{request_method}|#{request_time}|#{uuid}|#{JSON.generate(post)}"
        signature = OpenSSL::HMAC.digest(OpenSSL::Digest.new('SHA256'), Base64.strict_decode64(@shared_secret), string_to_hash)
        base64_signature = Base64.strict_encode64(signature)
        hmac_header = Base64.strict_encode64("#{app_id_key}|#{request_method}|#{request_time}|#{uuid}|#{base64_signature}")
        return { 'hmac' => hmac_header }
      end
    end
  end
end