activemerchant/active_merchant

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

Summary

Maintainability
C
1 day
Test Coverage
require 'jose'

module ActiveMerchant #:nodoc:
  module Billing #:nodoc:
    class AleloGateway < Gateway
      class_attribute :prelive_url

      self.test_url = 'https://sandbox-api.alelo.com.br/alelo/sandbox/'
      self.live_url = 'https://api.alelo.com.br/alelo/prd/'
      self.prelive_url = 'https://api.homologacaoalelo.com.br/alelo/uat/'

      self.supported_countries = ['BR']
      self.default_currency = 'BRL'
      self.supported_cardtypes = %i[visa master american_express discover]

      self.homepage_url = 'https://www.alelo.com.br'
      self.display_name = 'Alelo'

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

      def purchase(money, payment, options = {})
        post = {}
        add_order(post, options)
        add_amount(post, money)
        add_payment(post, payment)
        add_geolocation(post, options)
        add_extra_data(post, options)

        commit('capture/transaction', post, options)
      end

      def refund(money, authorization, options = {})
        request_id = authorization.split('#').first
        options[:http] = { method: :put, prevent_encrypt: true }
        commit('capture/transaction/refund', { requestId: request_id }, options, :put)
      end

      def supports_scrubbing?
        true
      end

      def scrub(transcript)
        force_utf8(transcript.encode).
          gsub(%r((Authorization: Bearer )[\w -]+), '\1[FILTERED]').
          gsub(%r((client_id=|Client-Id:)[\w -]+), '\1[FILTERED]\2').
          gsub(%r((client_secret=|Client-Secret:)[\w -]+), '\1[FILTERED]\2').
          gsub(%r((access_token\":\")[^\"]*), '\1[FILTERED]').
          gsub(%r((publicKey\":\")[^\"]*), '\1[FILTERED]')
      end

      private

      def force_utf8(string)
        return nil unless string

        # binary = string.encode('BINARY', invalid: :replace, undef: :replace, replace: '?')
        string.encode('UTF-8', invalid: :replace, undef: :replace, replace: '?')
      end

      def add_amount(post, money)
        post[:amount] = amount(money).to_f
      end

      def add_order(post, options)
        post[:requestId] = options[:order_id]
      end

      def add_extra_data(post, options)
        post.merge!({
          establishmentCode: options[:establishment_code],
          playerIdentification: options[:player_identification],
          captureType: '3', # send fixed value 3 to ecommerce
          subMerchantCode: options[:sub_merchant_mcc],
          externalTraceNumber: options[:external_trace_number]
        }.compact)
      end

      def add_geolocation(post, options)
        return if options[:geo_latitude].blank? || options[:geo_longitude].blank?

        post.merge!(geolocation: {
          latitude: options[:geo_latitude],
          longitude: options[:geo_longitude]
        })
      end

      def add_payment(post, payment)
        post.merge!({
          cardNumber: payment.number,
          cardholderName: payment.name,
          expirationMonth: payment.month,
          expirationYear: format(payment.year, :two_digits).to_i,
          securityCode: payment.verification_value
        })
      end

      def fetch_access_token
        params = {
          grant_type: 'client_credentials',
          client_id: @options[:client_id],
          client_secret: @options[:client_secret],
          scope: '/capture'
        }

        headers = {
          'Accept' => 'application/json',
          'Content-Type' => 'application/x-www-form-urlencoded'
        }

        begin
          raw_response = ssl_post(url('captura-oauth-provider/oauth/token'), post_data(params), headers)
        rescue ResponseError => e
          raise OAuthResponseError.new(e)
        else
          response = parse(raw_response)
          if (access_token = response[:access_token])
            Response.new(true, access_token, response)
          else
            raise OAuthResponseError.new(response)
          end
        end
      end

      def remote_encryption_key(access_token)
        response = parse(ssl_get(url('capture/key'), request_headers(access_token)))
        Response.new(true, response[:publicKey], response)
      end

      def ensure_credentials(try_again = true)
        multiresp = MultiResponse.new
        access_token = @options[:access_token]
        key = @options[:encryption_key]
        uuid = @options[:encryption_uuid]

        if access_token.blank?
          multiresp.process { fetch_access_token }
          access_token = multiresp.message
          key = nil
          uuid = nil
        end

        if key.blank?
          multiresp.process { remote_encryption_key(access_token) }
          key = multiresp.message
          uuid = multiresp.params['uuid']
        end

        {
          key: key,
          uuid: uuid,
          access_token: access_token,
          multiresp: multiresp.responses.present? ? multiresp : nil
        }
      rescue ActiveMerchant::OAuthResponseError => e
        raise e
      rescue ResponseError => e
        # retry to generate a new access_token when the provided one is expired
        raise e unless retry?(try_again, e, :access_token)

        @options.delete(:access_token)
        @options.delete(:encryption_key)
        ensure_credentials false
      end

      def encrypt_payload(body, credentials, options)
        key = OpenSSL::PKey::RSA.new(Base64.decode64(credentials[:key]))
        jwk = JOSE::JWK.from_key(key)
        alg_enc = { 'alg' => 'RSA-OAEP-256', 'enc' => 'A128CBC-HS256' }

        token = JOSE::JWE.block_encrypt(jwk, body.to_json, alg_enc).compact

        encrypted_body = {
          token: token,
          uuid: credentials[:uuid]
        }

        encrypted_body.to_json
      end

      def parse(body)
        JSON.parse(body, symbolize_names: true)
      end

      def post_data(params)
        params.map { |k, v| "#{k}=#{CGI.escape(v.to_s)}" }.join('&')
      end

      def commit(action, body, options, try_again = true)
        credentials = ensure_credentials
        payload = encrypt_payload(body, credentials, options)

        if options.dig :http, :method
          payload = body.to_json if options.dig :http, :prevent_encrypt
          response = parse ssl_request(options[:http][:method], url(action), payload, request_headers(credentials[:access_token]))
        else
          response = parse ssl_post(url(action), payload, request_headers(credentials[:access_token]))
        end

        resp = Response.new(
          success_from(action, response),
          message_from(response),
          response,
          authorization: authorization_from(response, options),
          test: test?
        )

        return resp unless credentials[:multiresp].present?

        multiresp = credentials[:multiresp]
        resp.params.merge!({
          'access_token' => credentials[:access_token],
          'encryption_key' => credentials[:key],
          'encryption_uuid' => credentials[:uuid]
        })
        multiresp.process { resp }

        multiresp
      rescue ActiveMerchant::OAuthResponseError => e
        raise OAuthResponseError.new(e)
      rescue ActiveMerchant::ResponseError => e
        # Retry on a possible expired encryption key
        if retry?(try_again, e, :encryption_key)
          @options.delete(:encryption_key)
          commit(action, body, options, false)
        else
          res = parse(e.response.body)
          Response.new(false, res[:messageUser] || res[:error], res, test: test?)
        end
      end

      def retry?(try_again, error, key)
        try_again && %w(401 404).include?(error.response.code) && @options[key].present?
      end

      def success_from(action, response)
        case action
        when 'capture/transaction/refund'
          response[:status] == 'ESTORNADA'
        when 'capture/transaction'
          response[:status] == 'CONFIRMADA'
        else
          false
        end
      end

      def message_from(response)
        response[:messages] || response[:messageUser]
      end

      def authorization_from(response, options)
        [response[:requestId]].join('#')
      end

      def url(action)
        return prelive_url if @options[:url_override] == 'prelive'

        "#{test? ? test_url : live_url}#{action}"
      end

      def request_headers(access_token)
        {
          'Accept' => 'application/json',
          'X-IBM-Client-Id' => @options[:client_id],
          'X-IBM-Client-Secret' => @options[:client_secret],
          'Content-Type' => 'application/json',
          'Authorization' => "Bearer #{access_token}"
        }
      end
    end
  end
end