activemerchant/active_merchant

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

Summary

Maintainability
B
5 hrs
Test Coverage
require 'json'

module ActiveMerchant #:nodoc:
  module Billing #:nodoc:
    class FatZebraGateway < Gateway
      self.live_url = 'https://gateway.fatzebra.com.au/v1.0'
      self.test_url = 'https://gateway.sandbox.fatzebra.com.au/v1.0'

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

      self.homepage_url = 'https://www.fatzebra.com.au/'
      self.display_name = 'Fat Zebra'

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

      def purchase(money, creditcard, options = {})
        post = {}

        add_amount(post, money, options)
        add_creditcard(post, creditcard, options)
        add_extra_options(post, options)
        add_order_id(post, options)
        add_ip(post, options)
        add_metadata(post, options)
        add_three_ds(post, options)

        commit(:post, 'purchases', post)
      end

      def authorize(money, creditcard, options = {})
        post = {}

        add_amount(post, money, options)
        add_creditcard(post, creditcard, options)
        add_extra_options(post, options)
        add_order_id(post, options)
        add_ip(post, options)
        add_metadata(post, options)
        add_three_ds(post, options)

        post[:capture] = false

        commit(:post, 'purchases', post)
      end

      def capture(money, authorization, options = {})
        txn_id, = authorization.to_s.split('|')
        post = {}

        add_amount(post, money, options)
        add_extra_options(post, options)

        commit(:post, "purchases/#{CGI.escape(txn_id)}/capture", post)
      end

      def refund(money, authorization, options = {})
        txn_id, = authorization.to_s.split('|')
        post = {}

        add_extra_options(post, options)
        add_amount(post, money, options)
        post[:transaction_id] = txn_id
        add_order_id(post, options)

        commit(:post, 'refunds', post)
      end

      def void(authorization, options = {})
        txn_id, endpoint = authorization.to_s.split('|')

        commit(:post, "#{endpoint}/void?id=#{txn_id}", {})
      end

      def store(creditcard, options = {})
        post = {}

        add_creditcard(post, creditcard)
        post[:is_billing] = true if options[:recurring]

        commit(:post, 'credit_cards', post)
      end

      def supports_scrubbing?
        true
      end

      def scrub(transcript)
        transcript.
          gsub(%r((Authorization: Basic )\w+), '\1[FILTERED]').
          gsub(%r(("card_number\\?":\\?")[^"\\]*)i, '\1[FILTERED]').
          gsub(%r(("cvv\\?":\\?")\d+), '\1[FILTERED]')
      end

      private

      def add_amount(post, money, options)
        post[:currency] = (options[:currency] || currency(money))
        post[:currency] = post[:currency].upcase if post[:currency]
        post[:amount] = money
      end

      def add_creditcard(post, creditcard, options = {})
        if creditcard.respond_to?(:number)
          post[:card_number] = creditcard.number
          post[:card_expiry] = "#{creditcard.month}/#{creditcard.year}"
          post[:cvv] = creditcard.verification_value if creditcard.verification_value?
          post[:card_holder] = creditcard.name if creditcard.name
        elsif creditcard.is_a?(String)
          id, = creditcard.to_s.split('|')
          post[:card_token] = id
          post[:cvv] = options[:cvv]
        elsif creditcard.is_a?(Hash)
          ActiveMerchant.deprecated 'Passing the credit card as a Hash is deprecated. Use a String and put the (optional) CVV in the options hash instead.'
          post[:card_token] = creditcard[:token]
          post[:cvv] = creditcard[:cvv]
        else
          raise ArgumentError.new("Unknown credit card format #{creditcard.inspect}")
        end
      end

      def add_extra_options(post, options)
        extra = {}
        extra[:ecm] = '32' if options[:recurring]
        extra[:name] = options[:merchant] if options[:merchant]
        extra[:location] = options[:merchant_location] if options[:merchant_location]
        extra[:card_on_file] = options.dig(:extra, :card_on_file) if options.dig(:extra, :card_on_file)
        extra[:auth_reason]  = options.dig(:extra, :auth_reason) if options.dig(:extra, :auth_reason)

        unless options[:three_d_secure].present?
          extra[:sli] = options[:sli] if options[:sli]
          extra[:xid] = options[:xid] if options[:xid]
          extra[:cavv] = options[:cavv] if options[:cavv]
        end

        post[:extra] = extra if extra.any?
      end

      def add_three_ds(post, options)
        return unless three_d_secure = options[:three_d_secure]

        post[:extra] = {
          sli: three_d_secure[:eci],
          xid: three_d_secure[:xid],
          cavv: three_d_secure[:cavv],
          par: three_d_secure[:authentication_response_status],
          ver: formatted_enrollment(three_d_secure[:enrolled]),
          threeds_version: three_d_secure[:version],
          ds_transaction_id: three_d_secure[:ds_transaction_id]
        }.compact
      end

      def formatted_enrollment(val)
        case val
        when 'Y', 'N', 'U' then val
        when true, 'true' then 'Y'
        when false, 'false' then 'N'
        end
      end

      def add_order_id(post, options)
        post[:reference] = options[:order_id] || SecureRandom.hex(15)
      end

      def add_ip(post, options)
        post[:customer_ip] = options[:ip] || '127.0.0.1'
      end

      def add_metadata(post, options)
        post[:metadata] = options.fetch(:metadata, {})
      end

      def commit(method, uri, parameters = nil)
        response =
          begin
            parse(ssl_request(method, get_url(uri), parameters.to_json, headers))
          rescue ResponseError => e
            return Response.new(false, 'Invalid Login') if e.response.code == '401'

            parse(e.response.body)
          end

        success = success_from(response)
        Response.new(
          success,
          message_from(response),
          response,
          test: response['test'],
          authorization: authorization_from(response, success, uri)
        )
      end

      def success_from(response)
        (
          response['successful'] &&
          response['response'] &&
          (response['response']['successful'] || response['response']['token'] || response['response']['response_code'] == '00')
        )
      end

      def authorization_from(response, success, uri)
        endpoint = uri.split('/')[0]
        if success
          id = response['response']['id'] || response['response']['token']
          "#{id}|#{endpoint}"
        else
          nil
        end
      end

      def message_from(response)
        if !response['errors'].empty?
          response['errors'].join(', ')
        elsif response['response']['message']
          response['response']['message']
        else
          'Unknown Error'
        end
      end

      def parse(response)
        JSON.parse(response)
      rescue JSON::ParserError
        msg = 'Invalid JSON response received from Fat Zebra. Please contact support@fatzebra.com.au if you continue to receive this message.'
        msg += "  (The raw response returned by the API was #{response.inspect})"
        {
          'successful' => false,
          'response' => {},
          'errors' => [msg]
        }
      end

      def get_url(uri)
        base = test? ? self.test_url : self.live_url
        base + '/' + uri
      end

      def headers
        {
          'Authorization' => 'Basic ' + Base64.strict_encode64(@options[:username].to_s + ':' + @options[:token].to_s).strip,
          'User-Agent' => "Fat Zebra v1.0/ActiveMerchant #{ActiveMerchant::VERSION}"
        }
      end
    end
  end
end