activemerchant/active_merchant

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

Summary

Maintainability
C
1 day
Test Coverage
require 'nokogiri'

module ActiveMerchant #:nodoc:
  module Billing #:nodoc:
    #
    # == Monei gateway
    # This class implements Monei gateway for Active Merchant. For more information about Monei
    # gateway please go to http://www.monei.com
    #
    # === Setup
    # In order to set-up the gateway you need only one paramater: the api_key
    # Request that data to Monei.
    class MoneiGateway < Gateway
      self.live_url = self.test_url = 'https://api.monei.com/v1/payments'

      self.supported_countries = %w[AD AT BE BG CA CH CY CZ DE DK EE ES FI FO FR GB GI GR HU IE IL IS IT LI LT LU LV MT NL NO PL PT RO SE SI SK TR US VA]
      self.default_currency = 'EUR'
      self.money_format = :cents
      self.supported_cardtypes = %i[visa master maestro jcb american_express]

      self.homepage_url = 'https://monei.com/'
      self.display_name = 'MONEI'

      # Constructor
      #
      # options - Hash containing the gateway credentials, ALL MANDATORY
      #           :api_key      Account's API KEY
      #
      def initialize(options = {})
        requires!(options, :api_key)
        super
      end

      # Public: Performs purchase operation
      #
      # money       - Amount of purchase
      # payment_method - Credit card
      # options     - Hash containing purchase options
      #               :order_id         Merchant created id for the purchase
      #               :billing_address  Hash with billing address information
      #               :description      Merchant created purchase description (optional)
      #               :currency         Sale currency to override money object or default (optional)
      #
      # Returns Active Merchant response object
      def purchase(money, payment_method, options = {})
        execute_new_order(:purchase, money, payment_method, options)
      end

      # Public: Performs authorization operation
      #
      # money       - Amount to authorize
      # payment_method - Credit card
      # options     - Hash containing authorization options
      #               :order_id         Merchant created id for the authorization
      #               :billing_address  Hash with billing address information
      #               :description      Merchant created authorization description (optional)
      #               :currency         Sale currency to override money object or default (optional)
      #
      # Returns Active Merchant response object
      def authorize(money, payment_method, options = {})
        execute_new_order(:authorize, money, payment_method, options)
      end

      # Public: Performs capture operation on previous authorization
      #
      # money         - Amount to capture
      # authorization - Reference to previous authorization, obtained from response object returned by authorize
      # options       - Hash containing capture options
      #                 :order_id         Merchant created id for the authorization (optional)
      #                 :description      Merchant created authorization description (optional)
      #                 :currency         Sale currency to override money object or default (optional)
      #
      # Note: you should pass either order_id or description
      #
      # Returns Active Merchant response object
      def capture(money, authorization, options = {})
        execute_dependant(:capture, money, authorization, options)
      end

      # Public: Refunds from previous purchase
      #
      # money         - Amount to refund
      # authorization - Reference to previous purchase, obtained from response object returned by purchase
      # options       - Hash containing refund options
      #                 :order_id         Merchant created id for the authorization (optional)
      #                 :description      Merchant created authorization description (optional)
      #                 :currency         Sale currency to override money object or default (optional)
      #
      # Note: you should pass either order_id or description
      #
      # Returns Active Merchant response object
      def refund(money, authorization, options = {})
        execute_dependant(:refund, money, authorization, options)
      end

      # Public: Voids previous authorization
      #
      # authorization - Reference to previous authorization, obtained from response object returned by authorize
      # options       - Hash containing capture options
      #                 :order_id         Merchant created id for the authorization (optional)
      #
      # Returns Active Merchant response object
      def void(authorization, options = {})
        execute_dependant(:void, nil, authorization, options)
      end

      # Public: Verifies credit card. Does this by doing a authorization of 1.00 Euro and then voiding it.
      #
      # payment_method - Credit card
      # options     - Hash containing authorization options
      #               :order_id         Merchant created id for the authorization
      #               :billing_address  Hash with billing address information
      #               :description      Merchant created authorization description (optional)
      #               :currency         Sale currency to override money object or default (optional)
      #
      # Returns Active Merchant response object of Authorization operation
      def verify(payment_method, options = {})
        MultiResponse.run(:use_first_response) do |r|
          r.process { authorize(100, payment_method, options) }
          r.process(:ignore_result) { void(r.authorization, options) }
        end
      end

      def store(payment_method, options = {})
        execute_new_order(:store, 0, payment_method, options)
      end

      def supports_scrubbing?
        true
      end

      def scrub(transcript)
        transcript.
          gsub(%r((Authorization: )\w+), '\1[FILTERED]').
          gsub(%r(("number\\?":\\?")[^"]*)i, '\1[FILTERED]').
          gsub(%r(("cvc\\?":\\?")[^"]*)i, '\1[FILTERED]').
          gsub(%r(("cavv\\?":\\?")[^"]*)i, '\1[FILTERED]')
      end

      private

      # Private: Execute purchase or authorize operation
      def execute_new_order(action, money, payment_method, options)
        request = build_request
        add_identification_new_order(request, options)
        add_transaction(request, action, money, options)
        add_payment(request, payment_method)
        add_customer(request, payment_method, options)
        add_3ds_authenticated_data(request, options)
        add_browser_info(request, options)
        commit(request, action, options)
      end

      # Private: Execute operation that depends on authorization code from previous purchase or authorize operation
      def execute_dependant(action, money, authorization, options)
        request = build_request

        add_identification_authorization(request, authorization, options)
        add_transaction(request, action, money, options)

        commit(request, action, options)
      end

      # Private: Build request object
      def build_request
        request = {}
        request[:livemode] = test? ? 'false' : 'true'
        request
      end

      # Private: Add identification part to request for new orders
      def add_identification_new_order(request, options)
        requires!(options, :order_id)
        request[:orderId] = options[:order_id]
      end

      # Private: Add identification part to request for orders that depend on authorization from previous operation
      def add_identification_authorization(request, authorization, options)
        options[:paymentId] = authorization
        request[:orderId] = options[:order_id] if options[:order_id]
      end

      # Private: Add payment part to request
      def add_transaction(request, action, money, options)
        request[:transactionType] = translate_payment_code(action)
        request[:description] = options[:description] || options[:order_id]
        unless money.nil?
          request[:amount] = amount(money).to_i
          request[:currency] = options[:currency] || currency(money)
        end
      end

      # Private: Add payment method to request
      def add_payment(request, payment_method)
        if payment_method.is_a? String
          request[:paymentToken] = payment_method
        else
          request[:paymentMethod] = {}
          request[:paymentMethod][:card] = {}
          request[:paymentMethod][:card][:number] = payment_method.number
          request[:paymentMethod][:card][:expMonth] = format(payment_method.month, :two_digits)
          request[:paymentMethod][:card][:expYear] = format(payment_method.year, :two_digits)
          request[:paymentMethod][:card][:cvc] = payment_method.verification_value.to_s
          request[:paymentMethod][:card][:cardholderName] = payment_method.name
        end
      end

      # Private: Add customer part to request
      def add_customer(request, payment_method, options)
        address = options[:billing_address] || options[:address]

        request[:customer] = {}
        request[:customer][:email] = options[:email] || 'support@monei.net'

        if address
          request[:customer][:name] = address[:name].to_s if address[:name]

          request[:billingDetails] = {}
          request[:billingDetails][:email] = options[:email] if options[:email]
          request[:billingDetails][:name] = address[:name] if address[:name]
          request[:billingDetails][:company] = address[:company] if address[:company]
          request[:billingDetails][:phone] = address[:phone] if address[:phone]
          request[:billingDetails][:address] = {}
          request[:billingDetails][:address][:line1] = address[:address1] if address[:address1]
          request[:billingDetails][:address][:line2] = address[:address2] if address[:address2]
          request[:billingDetails][:address][:city] = address[:city] if address[:city]
          request[:billingDetails][:address][:state] = address[:state] if address[:state].present?
          request[:billingDetails][:address][:zip] = address[:zip].to_s if address[:zip]
          request[:billingDetails][:address][:country] = address[:country] if address[:country]
        end

        request[:sessionDetails] = {}
        request[:sessionDetails][:ip] = options[:ip] if options[:ip]
      end

      # Private : Convert ECI to ResultIndicator
      # Possible ECI values:
      # 02 or 05 - Fully Authenticated Transaction
      # 00 or 07 - Non 3D Secure Transaction
      # Possible ResultIndicator values:
      # 01 = MASTER_3D_ATTEMPT
      # 02 = MASTER_3D_SUCCESS
      # 05 = VISA_3D_SUCCESS
      # 06 = VISA_3D_ATTEMPT
      # 07 = DEFAULT_E_COMMERCE
      def eci_to_result_indicator(eci)
        case eci
        when '02', '05'
          return eci
        else
          return '07'
        end
      end

      # Private: add the already validated 3DSecure info to request
      def add_3ds_authenticated_data(request, options)
        if options[:three_d_secure] && options[:three_d_secure][:eci] && options[:three_d_secure][:xid]
          add_3ds1_authenticated_data(request, options)
        elsif options[:three_d_secure]
          add_3ds2_authenticated_data(request, options)
        end
      end

      def add_3ds1_authenticated_data(request, options)
        three_d_secure_options = options[:three_d_secure]
        request[:paymentMethod][:card][:auth] = {
          cavv: three_d_secure_options[:cavv],
          cavvAlgorithm: three_d_secure_options[:cavv_algorithm],
          eci: three_d_secure_options[:eci],
          xid: three_d_secure_options[:xid],
          directoryResponse: three_d_secure_options[:enrolled],
          authenticationResponse: three_d_secure_options[:authentication_response_status]
        }
      end

      def add_3ds2_authenticated_data(request, options)
        three_d_secure_options = options[:three_d_secure]
        # If the transaction was authenticated in a frictionless flow, send the transStatus from the ARes.
        if three_d_secure_options[:authentication_response_status].nil?
          authentication_response = three_d_secure_options[:directory_response_status]
        else
          authentication_response = three_d_secure_options[:authentication_response_status]
        end
        request[:paymentMethod][:card][:auth] = {
          threeDSVersion: three_d_secure_options[:version],
          eci: three_d_secure_options[:eci],
          cavv: three_d_secure_options[:cavv],
          dsTransID: three_d_secure_options[:ds_transaction_id],
          directoryResponse: three_d_secure_options[:directory_response_status],
          authenticationResponse: authentication_response
        }
      end

      def add_browser_info(request, options)
        request[:sessionDetails][:ip] = options[:ip] if options[:ip]
        request[:sessionDetails][:userAgent] = options[:user_agent] if options[:user_agent]
        request[:sessionDetails][:lang] = options[:lang] if options[:lang]
      end

      # Private: Parse JSON response from Monei servers
      def parse(body)
        JSON.parse(body)
      end

      def json_error(raw_response)
        msg = 'Invalid response received from the MONEI API. Please contact support@monei.net if you continue to receive this message.'
        msg += " (The raw response returned by the API was #{raw_response.inspect})"
        {
          'status' => 'error',
          'message' => msg
        }
      end

      def response_error(raw_response)
        parse(raw_response)
      rescue JSON::ParserError
        json_error(raw_response)
      end

      def api_request(url, parameters, options = {})
        raw_response = response = nil
        begin
          raw_response = ssl_post(url, post_data(parameters), options)
          response = parse(raw_response)
        rescue ResponseError => e
          raw_response = e.response.body
          response = response_error(raw_response)
        rescue JSON::ParserError
          response = json_error(raw_response)
        end
        response
      end

      # Private: Send transaction to Monei servers and create AM response
      def commit(request, action, options)
        url = (test? ? test_url : live_url)
        endpoint = translate_action_endpoint(action, options)
        headers = {
          'Content-Type': 'application/json;charset=UTF-8',
          Authorization: @options[:api_key],
          'User-Agent': 'MONEI/Shopify/0.1.0'
        }

        response = api_request(url + endpoint, params(request, action), headers)
        success = success_from(response)

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

      # Private: Decide success from servers response
      def success_from(response)
        %w[
          SUCCEEDED
          AUTHORIZED
          REFUNDED
          PARTIALLY_REFUNDED
          CANCELED
        ].include? response['status']
      end

      # Private: Get message from servers response
      def message_from(response, success)
        success ? 'Transaction approved' : response.fetch('statusMessage', response.fetch('message', 'No error details'))
      end

      # Private: Get error code from servers response
      def error_code_from(response, success)
        success ? nil : STANDARD_ERROR_CODE[:card_declined]
      end

      # Private: Get authorization code from servers response
      def authorization_from(response, action)
        case action
        when :store
          return response['paymentToken']
        else
          return response['id']
        end
      end

      # Private: Encode POST parameters
      def post_data(params)
        params.clone.to_json
      end

      # Private: generate request params depending on action
      def params(request, action)
        request[:generatePaymentToken] = true if action == :store
        request
      end

      # Private: Translate AM operations to Monei operations codes
      def translate_payment_code(action)
        {
          purchase: 'SALE',
          store: 'SALE',
          authorize: 'AUTH',
          capture: 'CAPTURE',
          refund: 'REFUND',
          void: 'CANCEL'
        }[action]
      end

      # Private: Translate AM operations to Monei endpoints
      def translate_action_endpoint(action, options)
        {
          purchase: '',
          store: '',
          authorize: '',
          capture: "/#{options[:paymentId]}/capture",
          refund: "/#{options[:paymentId]}/refund",
          void: "/#{options[:paymentId]}/cancel"
        }[action]
      end
    end
  end
end