Shopify/active_merchant

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

Summary

Maintainability
A
3 hrs
Test Coverage
module ActiveMerchant #:nodoc:
  module Billing #:nodoc:
    class ReachGateway < Gateway
      self.test_url = 'https://checkout.rch.how/'
      self.live_url = 'https://checkout.rch.io/'

      self.supported_countries = %w(AE AG AL AM AT AU AW AZ BA BB BD BE BF BG BH BJ BM BN BO BR BS BW BZ CA CD CF
                                    CH CI CL CM CN CO CR CU CV CY CZ DE DJ DK DM DO DZ EE EG ES ET FI FJ FK FR GA
                                    GB GD GE GG GH GI GN GR GT GU GW GY HK HN HR HU ID IE IL IM IN IS IT JE JM JO
                                    JP KE KG KH KM KN KR KW KY KZ LA LC LK LR LT LU LV LY MA MD MK ML MN MO MR MS
                                    MT MU MV MW MX MY MZ NA NC NE NG NI NL NO NP NZ OM PA PE PF PG PH PK PL PT PY
                                    QA RO RS RW SA SB SC SE SG SH SI SK SL SN SO SR ST SV SY SZ TD TG TH TN TO TR
                                    TT TV TW TZ UG US UY UZ VC VN VU WF WS YE ZM)

      self.default_currency = 'USD'
      self.supported_cardtypes = %i[visa diners_club american_express jcb master discover maestro]

      self.homepage_url = 'https://www.withreach.com/'
      self.display_name = 'Reach'
      self.currencies_without_fractions = %w(BIF BYR CLF CLP CVE DJF GNF ISK JPY KMF KRW PYG RWF UGX UYI VND VUV XAF XOF XPF IDR MGA MRO)

      API_VERSION = 'v2.22'.freeze
      STANDARD_ERROR_CODE_MAPPING = {}
      PAYMENT_METHOD_MAP = {
        american_express: 'AMEX',
        cabal: 'CABAL',
        check: 'ACH',
        dankort: 'DANKORT',
        diners_club: 'DINERS',
        discover: 'DISC',
        elo: 'ELO',
        jcb: 'JCB',
        maestro: 'MAESTRO',
        master: 'MC',
        naranja: 'NARANJA',
        union_pay: 'UNIONPAY',
        visa: 'VISA'
      }

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

      def authorize(money, payment, options = {})
        request = build_checkout_request(money, payment, options)
        add_custom_fields_data(request, options)
        add_customer_data(request, options, payment)
        add_stored_credentials(request, options)
        post = { request: request, card: add_payment(payment, options) }
        if options[:stored_credential]
          MultiResponse.run(:use_first_response) do |r|
            r.process { commit('checkout', post) }
            r.process do
              r2 = get_network_payment_reference(r.responses[0])
              r.params[:network_transaction_id] = r2.message
              r2
            end
          end
        else
          commit('checkout', post)
        end
      end

      def purchase(money, payment, options = {})
        options[:capture] = true
        authorize(money, payment, options)
      end

      def capture(money, authorization, options = {})
        post = { request: { MerchantId: @options[:merchant_id], OrderId: authorization } }
        commit('capture', post)
      end

      def supports_scrubbing?
        true
      end

      def scrub(transcript)
        transcript.
          gsub(%r(((MerchantId)[% \w]+%\d{2})[\w -]+), '\1[FILTERED]').
          gsub(%r((signature=)[\w%]+), '\1[FILTERED]\2').
          gsub(%r((Number%22%3A%22)\d+), '\1[FILTERED]\2').
          gsub(%r((VerificationCode%22%3A)\d+), '\1[FILTERED]\2')
      end

      def refund(amount, authorization, options = {})
        currency = options[:currency] || currency(options[:amount])
        post = {
          request: {
            MerchantId: @options[:merchant_id],
            OrderId: authorization,
            ReferenceId: options[:order_id] || options[:reference_id],
            Amount: localized_amount(amount, currency)
          }
        }
        commit('refund', post)
      end

      def void(authorization, options = {})
        post = {
          request: {
            MerchantId: @options[:merchant_id],
            OrderId: authorization
          }
        }

        commit('cancel', 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

      private

      def build_checkout_request(amount, payment, options)
        currency = options[:currency] || currency(options[:amount])
        {
          MerchantId: @options[:merchant_id],
          ReferenceId: options[:order_id],
          ConsumerCurrency: currency,
          Capture: options[:capture] || false,
          PaymentMethod: PAYMENT_METHOD_MAP.fetch(payment.brand.to_sym, 'unsupported'),
          Items: [
            Sku: options[:item_sku] || SecureRandom.alphanumeric,
            ConsumerPrice: localized_amount(amount, currency),
            Quantity: (options[:item_quantity] || 1)
          ]
        }
      end

      def add_payment(payment, options)
        ntid = options.dig(:stored_credential, :network_transaction_id)
        cvv_or_previos_reference = (ntid ? { PreviousNetworkPaymentReference: ntid } : { VerificationCode: payment.verification_value })
        {
          Name: payment.name,
          Number: payment.number,
          Expiry: { Month: payment.month, Year: payment.year }
        }.merge!(cvv_or_previos_reference)
      end

      def add_customer_data(request, options, payment)
        address = options[:billing_address] || options[:address]

        return if address.blank?

        request[:Consumer] = {
          Name: payment.respond_to?(:name) ? payment.name : address[:name],
          Email: options[:email],
          Address: address[:address1],
          City: address[:city],
          Country: address[:country]
        }.compact
      end

      def add_stored_credentials(request, options)
        request[:PaymentModel] = payment_model(options) || ''
        request[:DeviceFingerprint] = options[:device_fingerprint] if options[:device_fingerprint]
      end

      def payment_model(options)
        stored_credential = options[:stored_credential]
        return options[:payment_model] if options[:payment_model]
        return 'CIT-One-Time' unless stored_credential

        payment_model_options = {
          initial_transaction: {
            'cardholder' => {
              'installment' => 'CIT-Setup-Scheduled',
              'unscheduled' => 'CIT-Setup-Unscheduled-MIT',
              'recurring' => 'CIT-Setup-Unscheduled'
            }
          },
          no_initial_transaction: {
            'cardholder' => {
              'unscheduled' => 'CIT-Subsequent-Unscheduled'
            },
            'merchant' => {
              'recurring' => 'MIT-Subsequent-Scheduled',
              'unscheduled' => 'MIT-Subsequent-Unscheduled'
            }
          }
        }
        initial = stored_credential[:initial_transaction] ? :initial_transaction : :no_initial_transaction
        payment_model_options[initial].dig(stored_credential[:initiator], stored_credential[:reason_type])
      end

      def add_custom_fields_data(request, options)
        add_shipping_data(request, options) if options[:taxes].present?
        request[:RateOfferId] = options[:rate_offer_id] if options[:rate_offer_id].present?
        request[:Items] = options[:items] if options[:items].present?
      end

      def add_shipping_data(request, options)
        request[:Shipping] = {
          ConsumerPrice: options[:price],
          ConsumerTaxes: options[:taxes],
          ConsumerDuty: options[:duty]
        }
        request[:Consignee] = {
          Name: options[:consignee_name],
          Address: options[:consignee_address],
          City: options[:consignee_city],
          Country: options[:consignee_country]
        }
      end

      def sign_body(body)
        Base64.strict_encode64(OpenSSL::HMAC.digest('sha256', @options[:secret].encode('utf-8'), body.encode('utf-8')))
      end

      def parse(body)
        hash_response = URI.decode_www_form(body).to_h
        hash_response['response'] = JSON.parse(hash_response['response'])

        hash_response
      end

      def format_and_sign(post)
        post[:request] = post[:request].to_json
        post[:card] = post[:card].to_json if post[:card].present?
        post[:signature] = sign_body(post[:request])
        post
      end

      def get_network_payment_reference(response)
        parameters = { request: { MerchantId: @options[:merchant_id], OrderId: response.params['response']['OrderId'] } }
        body = post_data format_and_sign(parameters)

        raw_response = ssl_request :post, url('query'), body, {}
        response = parse(raw_response)
        message = response.dig('response', 'Payment', 'NetworkPaymentReference')
        Response.new(true, message, {})
      end

      def commit(action, parameters)
        body = post_data format_and_sign(parameters)
        raw_response = ssl_post url(action), body
        response = parse(raw_response)

        Response.new(
          success_from(response),
          message_from(response) || '',
          response,
          authorization: authorization_from(response['response']),
          # avs_result: AVSResult.new(code: response['some_avs_response_key']),
          # cvv_result: CVVResult.new(response['some_cvv_response_key']),
          test: test?,
          error_code: error_code_from(response)
        )
      rescue ActiveMerchant::ResponseError => e
        Response.new(false, (e.response.body.present? ? e.response.body : e.response.msg), {}, test: test?)
      end

      def success_from(response)
        response.dig('response', 'Error').blank?
      end

      def message_from(response)
        success_from(response) ? '' : response.dig('response', 'Error', 'ReasonCode')
      end

      def authorization_from(response)
        response['OrderId']
      end

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

      def error_code_from(response)
        response['response']['Error']['Code'] unless success_from(response)
      end

      def url(action)
        "#{test? ? test_url : live_url}#{API_VERSION}/#{action}"
      end
    end
  end
end