Shopify/active_merchant

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

Summary

Maintainability
D
1 day
Test Coverage
module ActiveMerchant #:nodoc:
  module Billing #:nodoc:
    class JetpayV2Gateway < Gateway
      self.test_url = 'https://test1.jetpay.com/jetpay'
      self.live_url = 'https://gateway20.jetpay.com/jetpay'

      self.money_format = :cents
      self.default_currency = 'USD'
      self.supported_countries = %w[US CA]
      self.supported_cardtypes = %i[visa master american_express discover]

      self.homepage_url = 'http://www.jetpay.com'
      self.display_name = 'JetPay'

      API_VERSION = '2.2'

      ACTION_CODE_MESSAGES = {
        '000' =>  'Approved.',
        '001' =>  'Refer to card issuer.',
        '002' =>  'Refer to card issuer, special condition.',
        '003' =>  'Invalid merchant or service provider.',
        '004' =>  'Pick up card.',
        '005' =>  'Do not honor.',
        '006' =>  'Error.',
        '007' =>  'Pick up card, special condition.',
        '008' =>  'Honor with ID (Show ID).',
        '010' =>  'Partial approval.',
        '011' =>  'VIP approval.',
        '012' =>  'Invalid transaction.',
        '013' =>  'Invalid amount or exceeds maximum for card program.',
        '014' =>  'Invalid account number (no such number).',
        '015' =>  'No such issuer.',
        '019' =>  'Re-enter Transaction.',
        '021' =>  'No action taken (unable to back out prior transaction).',
        '025' =>  'Transaction Not Found.',
        '027' =>  'File update field edit error.',
        '028' =>  'File is temporarily unavailable.',
        '030' =>  'Format error.',
        '039' =>  'No credit account.',
        '041' =>  'Pick up card (lost card).',
        '043' =>  'Pick up card (stolen card).',
        '051' =>  'Insufficient funds.',
        '052' =>  'No checking account.',
        '053' =>  'No savings account.',
        '054' =>  'Expired Card.',
        '055' =>  'Incorrect PIN.',
        '057' =>  'Transaction not permitted to cardholder.',
        '058' =>  'Transaction not allowed at terminal.',
        '061' =>  'Exceeds withdrawal limit.',
        '062' =>  'Restricted card (eg, Country Exclusion).',
        '063' =>  'Security violation.',
        '065' =>  'Activity count limit exceeded.',
        '068' =>  'Response late.',
        '070' =>  'Contact card issuer.',
        '071' =>  'PIN not changed.',
        '075' =>  'Allowable number of PIN-entry tries exceeded.',
        '076' =>  'Unable to locate previous message (no matching retrieval reference number).',
        '077' =>  'Repeat or reversal data are inconsistent with original message.',
        '078' =>  'Blocked (first use), or non-existent account.',
        '079' =>  'Key exchange validation failed.',
        '080' =>  'Credit issuer unavailable or invalid date.',
        '081' =>  'PIN cryptographic error found.',
        '082' =>  'Negative online CVV results.',
        '084' =>  'Invalid auth life cycle.',
        '085' =>  'No reason to decline - CVV or AVS approved.',
        '086' =>  'Cannot verify PIN.',
        '087' =>  'Cashback not allowed.',
        '089' =>  'Issuer Down.',
        '091' =>  'Issuer Down.',
        '092' =>  'Unable to route transaction.',
        '093' =>  'Transaction cannot be completed - violation of law.',
        '094' =>  'Duplicate transmission.',
        '096' =>  'System error.',
        '100' =>  'Deny.',
        '101' =>  'Expired Card.',
        '103' =>  'Deny - Invalid manual Entry 4DBC.',
        '104' =>  'Deny - New card issued.',
        '105' =>  'Deny - Account Cancelled.',
        '106' =>  'Exceeded PIN Attempts.',
        '107' =>  'Please Call Issuer.',
        '109' =>  'Invalid merchant.',
        '110' =>  'Invalid amount.',
        '111' =>  'Invalid account.',
        '115' =>  'Service not permitted.',
        '117' =>  'Invalid PIN.',
        '119' =>  'Card member not enrolled.',
        '122' =>  'Invalid card (CID) security code.',
        '125' =>  'Invalid effective date.',
        '181' =>  'Format error.',
        '182' =>  'Please wait.',
        '183' =>  'Invalid currency code.',
        '187' =>  'Deny - new card issued.',
        '188' =>  'Deny - Expiration date required.',
        '189' =>  'Deny - Cancelled or Closed Merchant/SE.',
        '200' =>  'Deny - Pick up card.',
        '400' =>  'Reversal accepted.',
        '601' =>  'Reject - EMV Chip Declined Transaction.',
        '602' =>  'Reject - Suspected Fraud.',
        '603' =>  'Reject - Communications Error.',
        '604' =>  'Reject - Insufficient Approval.',
        '750' =>  'Velocity Check Fail.',
        '899' =>  'Misc Decline.',
        '900' =>  'Invalid Message Type.',
        '901' =>  'Invalid Merchant ID.',
        '903' =>  'Debit not supported.',
        '904' =>  'Private label not supported.',
        '905' =>  'Invalid card type.',
        '906' =>  'Unit not active.',
        '908' =>  'Manual card entry invalid.',
        '909' =>  'Invalid track information.',
        '911' =>  'Master merchant not found.',
        '912' =>  'Invalid card format.',
        '913' =>  'Invalid card type.',
        '914' =>  'Invalid card length.',
        '917' =>  'Expired card.',
        '919' =>  'Invalid entry type.',
        '920' =>  'Invalid amount.',
        '921' =>  'Invalid messge format.',
        '923' =>  'Invalid ABA.',
        '924' =>  'Invalid DDA.',
        '925' =>  'Invalid TID.',
        '926' =>  'Invalid Password.',
        '930' =>  'Invalid zipcode.',
        '931' =>  'Invalid Address.',
        '932' =>  'Invalid ZIP and Address.',
        '933' =>  'Invalid CVV2.',
        '934' =>  'Program Not Allowed.',
        '935' =>  'Invalid Device/App.',
        '940' =>  'Record Not Found.',
        '941' =>  'Merchant ID error.',
        '942' =>  'Refund Not Allowed.',
        '943' =>  'Refund denied.',
        '955' =>  'Invalid PIN block.',
        '956' =>  'Invalid KSN.',
        '958' =>  'Bad Status.',
        '959' =>  'Seek Record limit exceeded.',
        '960' =>  'Internal Key Database Error.',
        '961' =>  'TRANS not Supported. Cash Disbursement required a specific MCC.',
        '962' =>  'Invalid PIN key (Unknown KSN).',
        '981' =>  'Invalid AVS.',
        '987' =>  'Issuer Unavailable.',
        '988' =>  'System error SD.',
        '989' =>  'Database Error.',
        '992' =>  'Transaction Timeout.',
        '996' =>  'Bad Terminal ID.',
        '997' =>  'Message rejected by association.',
        '999' =>  'Communication failure',
        nil   =>  'No response returned (missing credentials?).'
      }

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

      def purchase(money, payment, options = {})
        commit(money, build_sale_request(money, payment, options))
      end

      def authorize(money, payment, options = {})
        commit(money, build_authonly_request(money, payment, options))
      end

      def capture(money, reference, options = {})
        transaction_id, _, _, token = reference.split(';')
        commit(money, build_capture_request(money, transaction_id, options), token)
      end

      def void(reference, options = {})
        transaction_id, _, amount, token = reference.split(';')
        commit(amount.to_i, build_void_request(amount.to_i, transaction_id, options), token)
      end

      def credit(money, payment, options = {})
        commit(money, build_credit_request(money, nil, payment, options))
      end

      def refund(money, reference, options = {})
        transaction_id, _, _, token = reference.split(';')
        commit(money, build_credit_request(money, transaction_id, token, options), token)
      end

      def verify(credit_card, options = {})
        authorize(0, credit_card, options)
      end

      def store(credit_card, options = {})
        commit(nil, build_store_request(credit_card, options))
      end

      def supports_scrubbing
        true
      end

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

      private

      def build_xml_request(transaction_type, options = {}, transaction_id = nil, &block)
        xml = Builder::XmlMarkup.new
        xml.tag! 'JetPay', 'Version' => API_VERSION do
          # Basic values needed for any request
          xml.tag! 'TerminalID', @options[:login]
          xml.tag! 'TransactionType', transaction_type
          xml.tag! 'TransactionID', transaction_id.nil? ? generate_unique_id.slice(0, 18) : transaction_id
          xml.tag! 'Origin', options[:origin] || 'INTERNET'
          xml.tag! 'IndustryInfo', 'Type' => options[:industry_info] || 'ECOMMERCE'
          xml.tag! 'Application', (options[:application] || 'n/a'), { 'Version' => options[:application_version] || '1.0' }
          xml.tag! 'Device', (options[:device] || 'n/a'), { 'Version' => options[:device_version] || '1.0' }
          xml.tag! 'Library', 'VirtPOS SDK', 'Version' => '1.5'
          xml.tag! 'Gateway', 'JetPay'
          xml.tag! 'DeveloperID', options[:developer_id] || 'n/a'

          if block_given?
            yield xml
          else
            xml.target!
          end
        end
      end

      def build_sale_request(money, payment, options)
        build_xml_request('SALE', options) do |xml|
          add_payment(xml, payment)
          add_addresses(xml, options)
          add_customer_data(xml, options)
          add_invoice_data(xml, options)
          add_user_defined_fields(xml, options)
          xml.tag! 'TotalAmount', amount(money)

          xml.target!
        end
      end

      def build_authonly_request(money, payment, options)
        build_xml_request('AUTHONLY', options) do |xml|
          add_payment(xml, payment)
          add_addresses(xml, options)
          add_customer_data(xml, options)
          add_invoice_data(xml, options)
          add_user_defined_fields(xml, options)
          xml.tag! 'TotalAmount', amount(money)

          xml.target!
        end
      end

      def build_capture_request(money, transaction_id, options)
        build_xml_request('CAPT', options, transaction_id) do |xml|
          add_invoice_data(xml, options)
          add_purchase_order(xml, options)
          add_user_defined_fields(xml, options)
          xml.tag! 'TotalAmount', amount(money)

          xml.target!
        end
      end

      def build_void_request(money, transaction_id, options)
        build_xml_request('VOID', options, transaction_id) do |xml|
          xml.tag! 'TotalAmount', amount(money)
          xml.target!
        end
      end

      def build_credit_request(money, transaction_id, payment, options)
        build_xml_request('CREDIT', options, transaction_id) do |xml|
          add_payment(xml, payment)
          add_invoice_data(xml, options)
          add_addresses(xml, options)
          add_customer_data(xml, options)
          add_user_defined_fields(xml, options)
          xml.tag! 'TotalAmount', amount(money)

          xml.target!
        end
      end

      def build_store_request(credit_card, options)
        build_xml_request('TOKENIZE', options) do |xml|
          add_payment(xml, credit_card)
          add_addresses(xml, options)
          add_customer_data(xml, options)

          xml.target!
        end
      end

      def commit(money, request, token = nil)
        response = parse(ssl_post(url, request))

        success = success?(response)
        Response.new(
          success,
          success ? 'APPROVED' : message_from(response),
          response,
          test: test?,
          authorization: authorization_from(response, money, token),
          avs_result: AVSResult.new(code: response[:avs]),
          cvv_result: CVVResult.new(response[:cvv2]),
          error_code: success ? nil : error_code_from(response)
        )
      end

      def url
        test? ? test_url : live_url
      end

      def parse(body)
        return {} if body.blank?

        xml = REXML::Document.new(body)

        response = {}
        xml.root.elements.to_a.each do |node|
          parse_element(response, node)
        end
        response
      end

      def parse_element(response, node)
        if node.has_elements?
          node.elements.each { |element| parse_element(response, element) }
        else
          response[node.name.underscore.to_sym] = node.text
        end
      end

      def format_exp(value)
        format(value, :two_digits)
      end

      def success?(response)
        response[:action_code] == '000'
      end

      def message_from(response)
        ACTION_CODE_MESSAGES[response[:action_code]]
      end

      def authorization_from(response, money, previous_token)
        original_amount = amount(money) if money
        [response[:transaction_id], response[:approval], original_amount, (response[:token] || previous_token)].join(';')
      end

      def error_code_from(response)
        response[:action_code]
      end

      def add_payment(xml, payment)
        return unless payment

        if payment.is_a? String
          token = payment
          _, _, _, token = payment.split(';') if payment.include? ';'
          xml.tag! 'Token', token if token
        else
          add_credit_card(xml, payment)
        end
      end

      def add_credit_card(xml, credit_card)
        xml.tag! 'CardNum', credit_card.number, 'CardPresent' => false, 'Tokenize' => true
        xml.tag! 'CardExpMonth', format_exp(credit_card.month)
        xml.tag! 'CardExpYear', format_exp(credit_card.year)

        xml.tag! 'CardName', [credit_card.first_name, credit_card.last_name].compact.join(' ') if credit_card.first_name || credit_card.last_name

        xml.tag! 'CVV2', credit_card.verification_value unless credit_card.verification_value.nil? || (credit_card.verification_value.length == 0)
      end

      def add_addresses(xml, options)
        if billing_address = options[:billing_address] || options[:address]
          xml.tag! 'Billing' do
            xml.tag! 'Address', [billing_address[:address1], billing_address[:address2]].compact.join(' ')
            xml.tag! 'City', billing_address[:city]
            xml.tag! 'StateProv', billing_address[:state]
            xml.tag! 'PostalCode', billing_address[:zip]
            xml.tag! 'Country', lookup_country_code(billing_address[:country])
            xml.tag! 'Phone', billing_address[:phone]
            xml.tag! 'Email', options[:email] if options[:email]
          end
        end

        if shipping_address = options[:shipping_address]
          xml.tag! 'Shipping' do
            xml.tag! 'Name', shipping_address[:name]
            xml.tag! 'Address', [shipping_address[:address1], shipping_address[:address2]].compact.join(' ')
            xml.tag! 'City', shipping_address[:city]
            xml.tag! 'StateProv', shipping_address[:state]
            xml.tag! 'PostalCode', shipping_address[:zip]
            xml.tag! 'Country', lookup_country_code(shipping_address[:country])
            xml.tag! 'Phone', shipping_address[:phone]
          end
        end
      end

      def add_customer_data(xml, options)
        xml.tag! 'UserIPAddress', options[:ip] if options[:ip]
      end

      def add_invoice_data(xml, options)
        xml.tag! 'OrderNumber', options[:order_id] if options[:order_id]
        if tax_amount = options[:tax_amount]
          xml.tag! 'TaxAmount', tax_amount, { 'ExemptInd' => options[:tax_exempt] || 'false' }
        end
      end

      def add_purchase_order(xml, options)
        if purchase_order = options[:purchase_order]
          xml.tag! 'Billing' do
            xml.tag! 'CustomerPO', purchase_order
          end
        end
      end

      def add_user_defined_fields(xml, options)
        xml.tag! 'UDField1', options[:ud_field_1] if options[:ud_field_1]
        xml.tag! 'UDField2', options[:ud_field_2] if options[:ud_field_2]
        xml.tag! 'UDField3', options[:ud_field_3] if options[:ud_field_3]
      end

      def lookup_country_code(code)
        country = Country.find(code) rescue nil
        country&.code(:alpha3)
      end
    end
  end
end