activemerchant/active_merchant

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

Summary

Maintainability
C
1 day
Test Coverage
module ActiveMerchant #:nodoc:
  module Billing #:nodoc:
    class MercadoPagoGateway < Gateway
      self.live_url = self.test_url = 'https://api.mercadopago.com/v1'

      self.supported_countries = %w[AR BR CL CO MX PE UY]
      self.supported_cardtypes = %i[visa master american_express elo cabal naranja creditel]

      self.homepage_url = 'https://www.mercadopago.com/'
      self.display_name = 'Mercado Pago'
      self.money_format = :dollars

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

      def purchase(money, payment, options = {})
        MultiResponse.run do |r|
          r.process { commit('tokenize', 'card_tokens', card_token_request(money, payment, options)) }
          options[:card_token] = r.authorization.split('|').first
          r.process { commit('purchase', 'payments', purchase_request(money, payment, options)) }
        end
      end

      def authorize(money, payment, options = {})
        MultiResponse.run do |r|
          r.process { commit('tokenize', 'card_tokens', card_token_request(money, payment, options)) }
          options[:card_token] = r.authorization.split('|').first
          r.process { commit('authorize', 'payments', authorize_request(money, payment, options)) }
        end
      end

      def capture(money, authorization, options = {})
        post = {}
        authorization, = authorization.split('|')
        post[:capture] = true
        post[:transaction_amount] = amount(money).to_f
        commit('capture', "payments/#{authorization}", post)
      end

      def refund(money, authorization, options = {})
        post = {}
        authorization, original_amount = authorization.split('|')
        post[:amount] = amount(money).to_f if original_amount && original_amount.to_f > amount(money).to_f
        commit('refund', "payments/#{authorization}/refunds", post)
      end

      def void(authorization, options = {})
        authorization, = authorization.split('|')
        post = { status: 'cancelled' }
        commit('void', "payments/#{authorization}", post)
      end

      def verify(credit_card, options = {})
        verify_amount = 100
        verify_amount = options[:amount].to_i if options[:amount]
        MultiResponse.run(:use_first_response) do |r|
          r.process { authorize(verify_amount, credit_card, options) }
          r.process(:ignore_result) { void(r.authorization, options) }
        end
      end

      def inquire(authorization, options = {})
        commit('inquire', inquire_path(authorization, options), {})
      end

      def supports_scrubbing?
        true
      end

      def scrub(transcript)
        transcript.
          gsub(%r((access_token=).*?([^\s]+)), '\1[FILTERED]').
          gsub(%r((\"card_number\\\":\\\")\d+), '\1[FILTERED]').
          gsub(%r((\"security_code\\\":\\\")\d+), '\1[FILTERED]')
      end

      private

      def card_token_request(money, payment, options = {})
        post = {}
        post[:card_number] = payment.number
        post[:security_code] = payment.verification_value
        post[:expiration_month] = payment.month
        post[:expiration_year] = payment.year
        post[:cardholder] = {
          name: payment.name,
          identification: {
            type: options[:cardholder_identification_type],
            number: options[:cardholder_identification_number]
          }
        }
        post
      end

      def purchase_request(money, payment, options = {})
        post = {}
        add_invoice(post, money, options)
        add_payment(post, options)
        add_additional_data(post, options)
        add_customer_data(post, payment, options)
        add_address(post, options)
        add_processing_mode(post, options)
        add_net_amount(post, options)
        add_taxes(post, options)
        add_notification_url(post, options)
        post[:binary_mode] = (options[:binary_mode].nil? ? true : options[:binary_mode])
        post
      end

      def authorize_request(money, payment, options = {})
        post = purchase_request(money, payment, options)
        post[:capture] = options[:capture] || false
        post
      end

      def add_processing_mode(post, options)
        return unless options[:processing_mode]

        post[:processing_mode] = options[:processing_mode]
        post[:merchant_account_id] = options[:merchant_account_id] if options[:merchant_account_id]
        post[:payment_method_option_id] = options[:payment_method_option_id] if options[:payment_method_option_id]
        add_merchant_services(post, options)
      end

      def add_merchant_services(post, options)
        return unless options[:fraud_scoring] || options[:fraud_manual_review]

        merchant_services = {}
        merchant_services[:fraud_scoring] = options[:fraud_scoring] if options[:fraud_scoring]
        merchant_services[:fraud_manual_review] = options[:fraud_manual_review] if options[:fraud_manual_review]
        post[:merchant_services] = merchant_services
      end

      def add_additional_data(post, options)
        post[:sponsor_id] = options[:sponsor_id]
        post[:metadata] = options[:metadata] if options[:metadata]
        post[:device_id] = options[:device_id] if options[:device_id]
        post[:additional_info] = {
          ip_address: options[:ip_address]
        }.merge(options[:additional_info] || {})

        add_address(post, options)
        add_shipping_address(post, options)
      end

      def add_customer_data(post, payment, options)
        post[:payer] = {
          email: options[:email],
          first_name: payment.first_name,
          last_name: payment.last_name
        }.merge(options[:payer] || {})
      end

      def add_address(post, options)
        if address = (options[:billing_address] || options[:address])

          post[:additional_info].merge!({
            payer: {
              address: {
                zip_code: address[:zip],
                street_name: "#{address[:address1]} #{address[:address2]}"
              }
            }
          })
        end
      end

      def add_shipping_address(post, options)
        if address = options[:shipping_address]

          post[:additional_info].merge!({
            shipments: {
              receiver_address: {
                zip_code: address[:zip],
                street_name: "#{address[:address1]} #{address[:address2]}"
              }
            }
          })
        end
      end

      def split_street_address(address1)
        street_number = address1.split(' ').first

        if street_name = address1.split(' ')[1..-1]
          street_name = street_name.join(' ')
        else
          nil
        end

        [street_number, street_name]
      end

      def add_invoice(post, money, options)
        post[:transaction_amount] = amount(money).to_f
        post[:description] = options[:description]
        post[:installments] = options[:installments] ? options[:installments].to_i : 1
        post[:statement_descriptor] = options[:statement_descriptor] if options[:statement_descriptor]
        post[:external_reference] = options[:order_id] || options[:external_reference] || SecureRandom.hex(16)
      end

      def add_payment(post, options)
        post[:token] = options[:card_token]
        post[:issuer_id] = options[:issuer_id] if options[:issuer_id]
        post[:payment_method_id] = options[:payment_method_id] if options[:payment_method_id]
      end

      def add_net_amount(post, options)
        post[:net_amount] = Float(options[:net_amount]) if options[:net_amount]
      end

      def add_notification_url(post, options)
        post[:notification_url] = options[:notification_url] if options[:notification_url]
      end

      def add_taxes(post, options)
        return unless (tax_object = options[:taxes])

        if tax_object.is_a?(Array)
          post[:taxes] = process_taxes_array(tax_object)
        elsif tax_object.is_a?(Hash)
          post[:taxes] = process_taxes_hash(tax_object)
        else
          raise taxes_error
        end
      end

      def process_taxes_hash(tax_object)
        [sanitize_taxes_hash(tax_object)]
      end

      def process_taxes_array(taxes_array)
        taxes_array.map do |tax_object|
          raise taxes_error unless tax_object.is_a?(Hash)

          sanitize_taxes_hash(tax_object)
        end
      end

      def sanitize_taxes_hash(tax_object)
        tax_value = tax_object['value'] || tax_object[:value]
        tax_type = tax_object['type'] || tax_object[:type]

        raise taxes_error if tax_value.nil? || tax_type.nil?

        { value: Float(tax_value), type: tax_type }
      end

      def taxes_error
        ArgumentError.new("Taxes should be a single object or array of objects with the shape: { value: 500, type: 'IVA' }")
      end

      def parse(body)
        JSON.parse(body)
      rescue JSON::ParserError
        {
          'status' => 'error',
          'status_detail' => 'json_parse_error',
          'message' => "A non-JSON response was received from Mercado Pago where one was expected. The raw response was:\n\n#{body}"
        }
      end

      def commit(action, path, parameters)
        if %w[capture void].include?(action)
          response = parse(ssl_request(:put, url(path), post_data(parameters), headers))
        elsif action == 'inquire'
          response = parse(ssl_get(url(path), headers))

          response = response[0]['results'][0] if response.is_a?(Array)
        else
          response = parse(ssl_post(url(path), post_data(parameters), headers(parameters)))
        end

        Response.new(
          success_from(action, response),
          message_from(response),
          response,
          authorization: authorization_from(response, parameters),
          test: test?,
          error_code: error_code_from(action, response)
        )
      end

      def success_from(action, response)
        if action == 'refund'
          response['status'] != 404 && response['error'].nil?
        else
          %w[active approved authorized cancelled in_process].include?(response['status'])
        end
      end

      def message_from(response)
        (response['status_detail']) || (response['message'])
      end

      def authorization_from(response, params)
        [response['id'], params[:transaction_amount]].join('|')
      end

      def post_data(parameters = {})
        parameters.clone.tap { |p| p.delete(:device_id) }.to_json
      end

      def inquire_path(authorization, options)
        if authorization
          authorization, = authorization.split('|')
          "payments/#{authorization}"
        else
          "payments/search?external_reference=#{options[:order_id] || options[:external_reference]}"
        end
      end

      def error_code_from(action, response)
        unless success_from(action, response)
          if cause = response['cause']
            cause.empty? ? nil : cause.first['code']
          else
            response['status']
          end
        end
      end

      def url(action)
        full_url = (test? ? test_url : live_url)
        full_url + "/#{action}?access_token=#{CGI.escape(@options[:access_token])}"
      end

      def headers(options = {})
        headers = {
          'Content-Type' => 'application/json'
        }
        headers['X-meli-session-id'] = options[:device_id] if options[:device_id]
        headers
      end

      def handle_response(response)
        case response.code.to_i
        when 200..499
          response.body
        else
          raise ResponseError.new(response)
        end
      end
    end
  end
end