activemerchant/active_merchant

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

Summary

Maintainability
B
6 hrs
Test Coverage
module ActiveMerchant #:nodoc:
  module Billing #:nodoc:
    class MokaGateway < Gateway
      self.test_url = 'https://service.refmoka.com'
      self.live_url = 'https://service.moka.com'

      self.supported_countries = %w[GB TR US]
      self.default_currency = 'TRY'
      self.money_format = :dollars
      self.supported_cardtypes = %i[visa master american_express discover]

      self.homepage_url = 'http://developer.moka.com/'
      self.display_name = 'Moka'

      ERROR_CODE_MAPPING = {
        '000' =>    'General error',
        '001' =>    '3D Not authenticated',
        '002' =>    'Limit is insufficient',
        '003' =>    'Credit card number format is wrong',
        '004' =>    'General decline',
        '005' =>    'This process is invalid for the card owner',
        '006' =>    'Expiration date is wrong',
        '007' =>    'Invalid transaction',
        '008' =>    'Connection with the bank not established',
        '009' =>    'Undefined error code',
        '010' =>    'Bank SSL error',
        '011' =>    'Call the bank for the manual authentication',
        '012' =>    'Card info is wrong - Kart Number or CVV2',
        '013' =>    '3D secure is not supported other than Visa MC cards',
        '014' =>    'Invalid account number',
        '015' =>    'CVV is wrong',
        '016' =>    'Authentication process is not present',
        '017' =>    'System error',
        '018' =>    'Stolen card',
        '019' =>    'Lost card',
        '020' =>    'Card with limited properties',
        '021' =>    'Timeout',
        '022' =>    'Invalid merchant',
        '023' =>    'False authentication',
        '024' =>    '3D authorization is successful but the process cannot be completed',
        '025' =>    '3D authorization failure',
        '026' =>    'Either the issuer bank or the card is not enrolled to the 3D process',
        '027' =>    'The bank did not allow the process',
        '028' =>    'Fraud suspect',
        '029' =>    'The card is closed to the e-commerce operations'
      }

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

      def purchase(money, payment, options = {})
        post = {}
        post[:PaymentDealerRequest] = {}
        options[:pre_auth] = 0
        add_auth_purchase(post, money, payment, options)
        add_3ds_data(post, options) if options[:execute_threed]

        action = options[:execute_threed] ? 'three_ds_purchase' : 'purchase'
        commit(action, post)
      end

      def authorize(money, payment, options = {})
        post = {}
        post[:PaymentDealerRequest] = {}
        options[:pre_auth] = 1
        add_auth_purchase(post, money, payment, options)
        add_3ds_data(post, options) if options[:execute_threed]

        action = options[:execute_threed] ? 'three_ds_authorize' : 'authorize'
        commit(action, post)
      end

      def capture(money, authorization, options = {})
        post = {}
        post[:PaymentDealerRequest] = {}
        add_payment_dealer_authentication(post)
        add_transaction_reference(post, authorization)
        add_invoice(post, money, options)

        commit('capture', post)
      end

      def refund(money, authorization, options = {})
        post = {}
        post[:PaymentDealerRequest] = {}
        add_payment_dealer_authentication(post)
        add_transaction_reference(post, authorization)
        add_void_refund_reason(post)
        add_amount(post, money)

        commit('refund', post)
      end

      def void(authorization, options = {})
        post = {}
        post[:PaymentDealerRequest] = {}
        add_payment_dealer_authentication(post)
        add_transaction_reference(post, authorization)
        add_void_refund_reason(post)

        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(r.authorization, options) }
        end
      end

      def supports_scrubbing?
        true
      end

      def scrub(transcript)
        transcript.
          gsub(%r(("CardNumber\\?":\\?")[^"]*)i, '\1[FILTERED]').
          gsub(%r(("CvcNumber\\?":\\?")[^"]*)i, '\1[FILTERED]').
          gsub(%r(("DealerCode\\?":\\?"?)[^"?]*)i, '\1[FILTERED]').
          gsub(%r(("Username\\?":\\?")[^"]*)i, '\1[FILTERED]').
          gsub(%r(("Password\\?":\\?")[^"]*)i, '\1[FILTERED]').
          gsub(%r(("CheckKey\\?":\\?")[^"]*)i, '\1[FILTERED]')
      end

      private

      def add_auth_purchase(post, money, payment, options)
        add_payment_dealer_authentication(post)
        add_invoice(post, money, options)
        add_payment(post, payment)
        add_additional_auth_purchase_data(post, options)
        add_additional_transaction_data(post, options)
        add_buyer_information(post, payment, options)
        add_basket_product(post, options[:basket_product]) if options[:basket_product]
      end

      def add_3ds_data(post, options)
        post[:PaymentDealerRequest][:ReturnHash] = 1
        post[:PaymentDealerRequest][:RedirectUrl] = options[:redirect_url] || ''
        post[:PaymentDealerRequest][:RedirectType] = options[:redirect_type] || 0
      end

      def add_payment_dealer_authentication(post)
        post[:PaymentDealerAuthentication] = {
          DealerCode: @options[:dealer_code],
          Username: @options[:username],
          Password: @options[:password],
          CheckKey: check_key
        }
      end

      def check_key
        str = "#{@options[:dealer_code]}MK#{@options[:username]}PD#{@options[:password]}"
        Digest::SHA256.hexdigest(str)
      end

      def add_invoice(post, money, options)
        post[:PaymentDealerRequest][:Amount] = amount(money) || 0
        post[:PaymentDealerRequest][:Currency] = options[:currency] || 'TL'
      end

      def add_payment(post, card)
        post[:PaymentDealerRequest][:CardHolderFullName] = card.name
        post[:PaymentDealerRequest][:CardNumber] = card.number
        post[:PaymentDealerRequest][:ExpMonth] = card.month.to_s.rjust(2, '0')
        post[:PaymentDealerRequest][:ExpYear] = card.year
        post[:PaymentDealerRequest][:CvcNumber] = card.verification_value || ''
      end

      def add_amount(post, money)
        post[:PaymentDealerRequest][:Amount] = amount(money) || 0
      end

      def add_additional_auth_purchase_data(post, options)
        post[:PaymentDealerRequest][:IsPreAuth] = options[:pre_auth]
        post[:PaymentDealerRequest][:Description] = options[:description] if options[:description]
        post[:PaymentDealerRequest][:InstallmentNumber] = options[:installment_number].to_i if options[:installment_number]
        post[:SubMerchantName] = options[:sub_merchant_name] if options[:sub_merchant_name]
        post[:IsPoolPayment] = options[:is_pool_payment] || 0
      end

      def add_buyer_information(post, card, options)
        obj = {}

        obj[:BuyerFullName] = card.name || ''
        obj[:BuyerEmail] = options[:email] if options[:email]
        obj[:BuyerAddress] = options[:billing_address][:address1] if options[:billing_address]
        obj[:BuyerGsmNumber] = options[:billing_address][:phone] if options[:billing_address]

        post[:PaymentDealerRequest][:BuyerInformation] = obj
      end

      def add_basket_product(post, basket_options)
        basket = []

        basket_options.each do |product|
          obj = {}
          obj[:ProductId] = product[:product_id] if product[:product_id]
          obj[:ProductCode] = product[:product_code] if product[:product_code]
          obj[:UnitPrice] = amount(product[:unit_price]) if product[:unit_price]
          obj[:Quantity] = product[:quantity] if product[:quantity]
          basket << obj
        end

        post[:PaymentDealerRequest][:BasketProduct] = basket
      end

      def add_additional_transaction_data(post, options)
        post[:PaymentDealerRequest][:ClientIP] = options[:ip] if options[:ip]
        post[:PaymentDealerRequest][:OtherTrxCode] = options[:order_id] if options[:order_id]
      end

      def add_transaction_reference(post, authorization)
        post[:PaymentDealerRequest][:VirtualPosOrderId] = authorization
      end

      def add_void_refund_reason(post)
        post[:PaymentDealerRequest][:VoidRefundReason] = 2
      end

      def commit(action, parameters)
        response = parse(ssl_post(url(action), post_data(parameters), request_headers))
        Response.new(
          success_from(response),
          message_from(response),
          response,
          authorization: authorization_from(response),
          test: test?,
          error_code: error_code_from(response)
        )
      end

      def url(action)
        host = (test? ? test_url : live_url)
        endpoint = endpoint(action)

        "#{host}/PaymentDealer/#{endpoint}"
      end

      def endpoint(action)
        case action
        when 'three_ds_authorize', 'three_ds_purchase'
          'DoDirectPaymentThreeD'
        when 'purchase', 'authorize'
          'DoDirectPayment'
        when 'capture'
          'DoCapture'
        when 'void'
          'DoVoid'
        when 'refund'
          'DoCreateRefundRequest'
        end
      end

      def request_headers
        { 'Content-Type' => 'application/json' }
      end

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

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

      def success_from(response)
        return response.dig('Data', 'IsSuccessful') if response.dig('Data', 'IsSuccessful').to_s.present?

        response['ResultCode']&.casecmp('success') == 0
      end

      def message_from(response)
        response.dig('Data', 'ResultMessage').presence || response['ResultCode']
      end

      def authorization_from(response)
        response.dig('Data', 'VirtualPosOrderId')
      end

      def error_code_from(response)
        codes = [response['ResultCode'], response.dig('Data', 'ResultCode')].flatten
        codes.reject! { |code| code.blank? || code.casecmp('success').zero? }
        codes.map { |code| ERROR_CODE_MAPPING[code] || code }.join(', ')
      end
    end
  end
end