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