Shopify/active_merchant

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

Summary

Maintainability
D
2 days
Test Coverage
module ActiveMerchant #:nodoc:
  module Billing #:nodoc:
    # For more information on the Iridium Gateway please download the
    # documentation from their Merchant Management System.
    #
    # The login and password are not the username and password you use to
    # login to the Iridium Merchant Management System. Instead, you will
    # use the API username and password you were issued separately.
    class IridiumGateway < Gateway
      self.live_url = self.test_url = 'https://gw1.iridiumcorp.net/'

      # The countries the gateway supports merchants from as 2 digit ISO country codes
      self.supported_countries = %w[GB ES]
      self.default_currency = 'EUR'
      self.money_format = :cents

      # The card types supported by the payment gateway
      self.supported_cardtypes = %i[visa master american_express discover maestro jcb diners_club]

      # The homepage URL of the gateway
      self.homepage_url = 'http://www.iridiumcorp.co.uk/'

      # The name of the gateway
      self.display_name = 'Iridium'

      CURRENCY_CODES = {
        'AED' => '784',
        'AFN' => '971',
        'ALL' => '008',
        'AMD' => '051',
        'ANG' => '532',
        'AOA' => '973',
        'ARS' => '032',
        'AUD' => '036',
        'AWG' => '533',
        'AZN' => '944',
        'BAM' => '977',
        'BBD' => '052',
        'BDT' => '050',
        'BGN' => '975',
        'BHD' => '048',
        'BIF' => '108',
        'BMD' => '060',
        'BND' => '096',
        'BOB' => '068',
        'BOV' => '984',
        'BRL' => '986',
        'BSD' => '044',
        'BTN' => '064',
        'BWP' => '072',
        'BYR' => '974',
        'BZD' => '084',
        'CAD' => '124',
        'CDF' => '976',
        'CHE' => '947',
        'CHF' => '756',
        'CHW' => '948',
        'CLF' => '990',
        'CLP' => '152',
        'CNY' => '156',
        'COP' => '170',
        'COU' => '970',
        'CRC' => '188',
        'CUP' => '192',
        'CVE' => '132',
        'CYP' => '196',
        'CZK' => '203',
        'DJF' => '262',
        'DKK' => '208',
        'DOP' => '214',
        'DZD' => '012',
        'EEK' => '233',
        'EGP' => '818',
        'ERN' => '232',
        'ETB' => '230',
        'EUR' => '978',
        'FJD' => '242',
        'FKP' => '238',
        'GBP' => '826',
        'GEL' => '981',
        'GHS' => '288',
        'GIP' => '292',
        'GMD' => '270',
        'GNF' => '324',
        'GTQ' => '320',
        'GYD' => '328',
        'HKD' => '344',
        'HNL' => '340',
        'HRK' => '191',
        'HTG' => '332',
        'HUF' => '348',
        'IDR' => '360',
        'ILS' => '376',
        'INR' => '356',
        'IQD' => '368',
        'IRR' => '364',
        'ISK' => '352',
        'JMD' => '388',
        'JOD' => '400',
        'JPY' => '392',
        'KES' => '404',
        'KGS' => '417',
        'KHR' => '116',
        'KMF' => '174',
        'KPW' => '408',
        'KRW' => '410',
        'KWD' => '414',
        'KYD' => '136',
        'KZT' => '398',
        'LAK' => '418',
        'LBP' => '422',
        'LKR' => '144',
        'LRD' => '430',
        'LSL' => '426',
        'LTL' => '440',
        'LVL' => '428',
        'LYD' => '434',
        'MAD' => '504',
        'MDL' => '498',
        'MGA' => '969',
        'MKD' => '807',
        'MMK' => '104',
        'MNT' => '496',
        'MOP' => '446',
        'MRO' => '478',
        'MTL' => '470',
        'MUR' => '480',
        'MVR' => '462',
        'MWK' => '454',
        'MXN' => '484',
        'MXV' => '979',
        'MYR' => '458',
        'MZN' => '943',
        'NAD' => '516',
        'NGN' => '566',
        'NIO' => '558',
        'NOK' => '578',
        'NPR' => '524',
        'NZD' => '554',
        'OMR' => '512',
        'PAB' => '590',
        'PEN' => '604',
        'PGK' => '598',
        'PHP' => '608',
        'PKR' => '586',
        'PLN' => '985',
        'PYG' => '600',
        'QAR' => '634',
        'ROL' => '642',
        'RON' => '946',
        'RSD' => '941',
        'RUB' => '643',
        'RWF' => '646',
        'SAR' => '682',
        'SBD' => '090',
        'SCR' => '690',
        'SDG' => '938',
        'SEK' => '752',
        'SGD' => '702',
        'SHP' => '654',
        'SKK' => '703',
        'SLL' => '694',
        'SOS' => '706',
        'SRD' => '968',
        'STD' => '678',
        'SYP' => '760',
        'SZL' => '748',
        'THB' => '764',
        'TJS' => '972',
        'TMM' => '795',
        'TND' => '788',
        'TOP' => '776',
        'TRY' => '949',
        'TTD' => '780',
        'TWD' => '901',
        'TZS' => '834',
        'UAH' => '980',
        'UGX' => '800',
        'USD' => '840',
        'USN' => '997',
        'USS' => '998',
        'UYU' => '858',
        'UZS' => '860',
        'VEB' => '862',
        'VND' => '704',
        'VUV' => '548',
        'WST' => '882',
        'XAF' => '950',
        'XAG' => '961',
        'XAU' => '959',
        'XBA' => '955',
        'XBB' => '956',
        'XBC' => '957',
        'XBD' => '958',
        'XCD' => '951',
        'XDR' => '960',
        'XOF' => '952',
        'XPD' => '964',
        'XPF' => '953',
        'XPT' => '962',
        'XTS' => '963',
        'XXX' => '999',
        'YER' => '886',
        'ZAR' => '710',
        'ZMK' => '894',
        'ZWD' => '716'
      }

      AVS_CODE = {
        'PASSED' => 'Y',
        'FAILED' => 'N',
        'PARTIAL' => 'X',
        'NOT_CHECKED' => 'X',
        'UNKNOWN' => 'X'
      }

      CVV_CODE = {
        'PASSED' => 'M',
        'FAILED' => 'N',
        'PARTIAL' => 'I',
        'NOT_CHECKED' => 'P',
        'UNKNOWN' => 'U'
      }

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

      def authorize(money, payment_source, options = {})
        setup_address_hash(options)

        if payment_source.respond_to?(:number)
          commit(build_purchase_request('PREAUTH', money, payment_source, options), options)
        else
          commit(build_reference_request('PREAUTH', money, payment_source, options), options)
        end
      end

      def purchase(money, payment_source, options = {})
        setup_address_hash(options)

        if payment_source.respond_to?(:number)
          commit(build_purchase_request('SALE', money, payment_source, options), options)
        else
          commit(build_reference_request('SALE', money, payment_source, options), options)
        end
      end

      def capture(money, authorization, options = {})
        commit(build_reference_request('COLLECTION', money, authorization, options), options)
      end

      def credit(money, authorization, options = {})
        ActiveMerchant.deprecated CREDIT_DEPRECATION_MESSAGE
        refund(money, authorization, options)
      end

      def refund(money, authorization, options = {})
        commit(build_reference_request('REFUND', money, authorization, options), options)
      end

      def void(authorization, options = {})
        commit(build_reference_request('VOID', nil, authorization, options), options)
      end

      def supports_scrubbing
        true
      end

      def scrub(transcript)
        transcript.
          gsub(%r((Authorization: Basic )\w+), '\1[FILTERED]').
          gsub(%r((<CardNumber>)\d+(</CardNumber>)), '\1[FILTERED]\2').
          gsub(%r((<CV2>)\d+(</CV2>)), '\1[FILTERED]\2')
      end

      private

      def build_purchase_request(type, money, creditcard, options)
        options[:action] = 'CardDetailsTransaction'
        build_request(options) do |xml|
          add_purchase_data(xml, type, money, options)
          add_creditcard(xml, creditcard)
          add_customerdetails(xml, creditcard, options[:billing_address], options)
        end
      end

      def build_reference_request(type, money, authorization, options)
        options[:action] = 'CrossReferenceTransaction'
        order_id, cross_reference, = authorization.split(';')
        build_request(options) do |xml|
          if money
            currency = options[:currency] || currency(money)
            details = { 'CurrencyCode' => currency_code(currency), 'Amount' => localized_amount(money, currency) }
          else
            details = { 'CurrencyCode' => currency_code(default_currency), 'Amount' => '0' }
          end
          xml.tag! 'TransactionDetails', details do
            xml.tag! 'MessageDetails', { 'TransactionType' => type, 'CrossReference' => cross_reference }
            xml.tag! 'OrderID', (options[:order_id] || order_id)
          end
        end
      end

      def build_request(options)
        requires!(options, :action)
        xml = Builder::XmlMarkup.new indent: 2
        xml.instruct!(:xml, version: '1.0', encoding: 'utf-8')
        xml.tag! 'soap:Envelope', { 'xmlns:soap' => 'http://schemas.xmlsoap.org/soap/envelope/',
                                    'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance',
                                    'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema' } do
          xml.tag! 'soap:Body' do
            xml.tag! options[:action], { 'xmlns' => 'https://www.thepaymentgateway.net/' } do
              xml.tag! 'PaymentMessage' do
                add_merchant_data(xml, options)
                yield(xml)
              end
            end
          end
        end
        xml.target!
      end

      def setup_address_hash(options)
        options[:billing_address] = options[:billing_address] || options[:address] || {}
        options[:shipping_address] = options[:shipping_address] || {}
      end

      def add_purchase_data(xml, type, money, options)
        currency = options[:currency] || currency(money)
        requires!(options, :order_id)
        xml.tag! 'TransactionDetails', { 'Amount' => localized_amount(money, currency), 'CurrencyCode' => currency_code(currency) } do
          xml.tag! 'MessageDetails', { 'TransactionType' => type }
          xml.tag! 'OrderID', options[:order_id]
          xml.tag! 'TransactionControl' do
            xml.tag! 'ThreeDSecureOverridePolicy', 'FALSE'
            xml.tag! 'EchoAVSCheckResult', 'TRUE'
            xml.tag! 'EchoCV2CheckResult', 'TRUE'
          end
        end
      end

      def add_customerdetails(xml, creditcard, address, options, shipTo = false)
        xml.tag! 'CustomerDetails' do
          if address
            country_code = Country.find(address[:country]).code(:numeric) unless address[:country].blank?
            xml.tag! 'BillingAddress' do
              xml.tag! 'Address1', address[:address1]
              xml.tag! 'Address2', address[:address2]
              xml.tag! 'City', address[:city]
              xml.tag! 'State', address[:state]
              xml.tag! 'PostCode', address[:zip]
              xml.tag! 'CountryCode', country_code if country_code
            end
            xml.tag! 'PhoneNumber', address[:phone]
          end

          xml.tag! 'EmailAddress', options[:email]
          xml.tag! 'CustomerIPAddress', options[:ip] || '127.0.0.1'
        end
      end

      def add_creditcard(xml, creditcard)
        xml.tag! 'CardDetails' do
          xml.tag! 'CardName', creditcard.name
          xml.tag! 'CV2', creditcard.verification_value if creditcard.verification_value
          xml.tag! 'CardNumber', creditcard.number
          xml.tag! 'ExpiryDate', { 'Month' => creditcard.month.to_s.rjust(2, '0'), 'Year' => creditcard.year.to_s[/\d\d$/] }
        end
      end

      def add_merchant_data(xml, options)
        xml.tag! 'MerchantAuthentication', { 'MerchantID' => @options[:login], 'Password' => @options[:password] }
      end

      def commit(request, options)
        requires!(options, :action)
        response = parse(
          ssl_post(
            test? ? self.test_url : self.live_url, request,
            {
              'SOAPAction' => 'https://www.thepaymentgateway.net/' + options[:action],
              'Content-Type' => 'text/xml; charset=utf-8'
            }
          )
        )

        success = response[:transaction_result][:status_code] == '0'
        message = response[:transaction_result][:message]
        authorization = success ? [options[:order_id], response[:transaction_output_data][:cross_reference], response[:transaction_output_data][:auth_code]].compact.join(';') : nil

        Response.new(
          success,
          message,
          response,
          test: test?,
          authorization: authorization,
          avs_result: {
            street_match: AVS_CODE[ response[:transaction_output_data][:address_numeric_check_result] ],
            postal_match: AVS_CODE[ response[:transaction_output_data][:post_code_check_result] ]
          },
          cvv_result: CVV_CODE[ response[:transaction_output_data][:cv2_check_result] ]
        )
      end

      def parse(xml)
        reply = {}
        xml = REXML::Document.new(xml)
        if (root = REXML::XPath.first(xml, '//CardDetailsTransactionResponse')) ||
           (root = REXML::XPath.first(xml, '//CrossReferenceTransactionResponse'))
          root.elements.to_a.each do |node|
            case node.name
            when 'Message'
              reply[:message] = reply(node.text)
            else
              parse_element(reply, node)
            end
          end
        elsif root = REXML::XPath.first(xml, '//soap:Fault')
          parse_element(reply, root)
          reply[:message] = "#{reply[:faultcode]}: #{reply[:faultstring]}"
        end
        reply
      end

      def parse_element(reply, node)
        case node.name
        when 'CrossReferenceTransactionResult'
          reply[:transaction_result] = {}
          node.attributes.each do |a, b|
            reply[:transaction_result][a.underscore.to_sym] = b
          end
          node.elements.each { |e| parse_element(reply[:transaction_result], e) } if node.has_elements?

        when 'CardDetailsTransactionResult'
          reply[:transaction_result] = {}
          node.attributes.each do |a, b|
            reply[:transaction_result][a.underscore.to_sym] = b
          end
          node.elements.each { |e| parse_element(reply[:transaction_result], e) } if node.has_elements?

        when 'TransactionOutputData'
          reply[:transaction_output_data] = {}
          node.attributes.each { |a, b| reply[:transaction_output_data][a.underscore.to_sym] = b }
          node.elements.each { |e| parse_element(reply[:transaction_output_data], e) } if node.has_elements?
        when 'CustomVariables'
          reply[:custom_variables] = {}
          node.attributes.each { |a, b| reply[:custom_variables][a.underscore.to_sym] = b }
          node.elements.each { |e| parse_element(reply[:custom_variables], e) } if node.has_elements?
        when 'GatewayEntryPoints'
          reply[:gateway_entry_points] = {}
          node.attributes.each { |a, b| reply[:gateway_entry_points][a.underscore.to_sym] = b }
          node.elements.each { |e| parse_element(reply[:gateway_entry_points], e) } if node.has_elements?
        else
          k = node.name.underscore.to_sym
          if node.has_elements?
            reply[k] = {}
            node.elements.each { |e| parse_element(reply[k], e) }
          else
            if node.has_attributes?
              reply[k] = {}
              node.attributes.each { |a, b| reply[k][a.underscore.to_sym] = b }
            else
              reply[k] = node.text
            end
          end
        end
        reply
      end

      def currency_code(currency)
        CURRENCY_CODES[currency]
      end
    end
  end
end