Shopify/active_merchant

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

Summary

Maintainability
A
0 mins
Test Coverage
require 'digest/md5'

module ActiveMerchant #:nodoc:
  module Billing #:nodoc:
    # This gateway accepts the following arguments:
    #   :login    => your PayJunction username
    #   :password => your PayJunction pass
    # Example use:
    #
    #   gateway = ActiveMerchant::Billing::Base.gateway(:pay_gate_xml).new(
    #               :login => "my_account",
    #               :password => "my_pass"
    #            )
    #
    #   # set up credit card obj as in main ActiveMerchant example
    #   creditcard = ActiveMerchant::Billing::CreditCard.new(
    #     :type       => 'visa',
    #     :number     => '4242424242424242',
    #     :month      => 8,
    #     :year       => 2009,
    #     :first_name => 'Bob',
    #     :last_name  => 'Bobsen'
    #   )
    #
    #   # run request
    #   response = gateway.purchase(1000, creditcard) # charge 10 dollars
    #
    # 1) Check whether the transaction was successful
    #
    #   response.success?
    #
    # 2) Retrieve the message returned by PayJunction
    #
    #   response.message
    #
    # 3) Retrieve the unique transaction ID returned by PayGateXML
    #
    #   response.authorization
    #
    # This gateway has many other features which are not implemented here yet
    # The basic setup here only supports auth/capture transactions.
    #
    # Test Transactions
    #
    # PayGateXML has a global test user/pass, but you can also sign up for your own.
    # The class and the test come equipped with the global test creds
    #
    # Usage Details
    #
    # Below is a map of only SOME of the values accepted by PayGateXML and how you should submit
    # each to ActiveMerchant
    #
    # PayGateXML Field        ActiveMerchant Use
    #
    # pgid                    use :login value to gateway instantiation
    # pwd                     use :password value to gateway instantiation
    #
    # cname                   credit_card.name
    # cc                      credit_card.number
    # exp                     credit_card values formatted to YYYYMMDD
    # budp                    South Africa only - set to 0 if purchase is not on budget
    # amt                     include as argument to method for your transaction type
    # ver                     do nothing, always set to current API version
    #
    # cref                    provide as :invoice in options, varchar(80)
    # cur                     3 char field, currently only ZAR
    # cvv                     credit_card.verification
    # bno                     batch processing number, i.e. you supply this
    #
    # others -- not used in this implementation
    # nurl, rurl - must remain blank or absent or they will use an alternative authentication mechanism
    # email, ip  - must remain blank or absent or they will use a PayGate extra service call PayProtector
    # threed     - must remain blank unless you are using your own 3D Secure server
    #
    class PayGateXmlGateway < Gateway
      self.live_url = 'https://www.paygate.co.za/payxml/process.trans'

      # The countries the gateway supports merchants from as 2 digit ISO country codes
      self.supported_countries = %w[US ZA]

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

      # The homepage URL of the gateway
      self.homepage_url = 'http://paygate.co.za/'

      # The name of the gateway
      self.display_name = 'PayGate PayXML'

      # PayGate only supports Rands
      self.default_currency = 'ZAR'

      # PayGate accepts only lowest denomination
      self.money_format = :cents

      # PayGateXML public test account - you can get a private one too
      TEST_ID_3DSECURE  = '10011013800'
      TEST_ID           = '10011021600'
      TEST_PWD          = 'test'

      API_VERSION = '4.0'

      DECLINE_CODES = {
        # Credit Card Errors - These RESULT_CODEs are returned if the transaction cannot be authorized due to a problem with the card.  The TRANSACTION_STATUS will be 2
        900001  => 'Call for Approval',
        900002  => 'Card Expired',
        900003  => 'Insufficient Funds',
        900004  => 'Invalid Card Number',
        900005  => 'Bank Interface Timeout', # indicates a communications failure between the banks systems
        900006  => 'Invalid Card',
        900007  => 'Declined',
        900009  => 'Lost Card',
        900010  => 'Invalid Card Length',
        900011  => 'Suspected Fraud',
        900012  => 'Card Reported As Stolen',
        900013  => 'Restricted Card',
        900014  => 'Excessive Card Usage',
        900015  => 'Card Blacklisted',

        900207  => 'Declined; authentication failed', # indicates the cardholder did not enter their MasterCard SecureCode / Verified by Visa password correctly

        990020  => 'Auth Declined',

        991001  => 'Invalid expiry date',
        991002  => 'Invalid amount',

        # Communication Errors - These RESULT_CODEs are returned if the transaction cannot be completed due to an unexpected error.  TRANSACTION_STATUS will be 0.
        900205  => 'Unexpected authentication result (phase 1)',
        900206  => 'Unexpected authentication result (phase 1)',

        990001  => 'Could not insert into Database',

        990022  => 'Bank not available',

        990053  => 'Error processing transaction',

        # Miscellaneous - Unless otherwise noted, the TRANSACTION_STATUS will be 0.
        900209  => 'Transaction verification failed (phase 2)', # Indicates the verification data returned from MasterCard SecureCode / Verified by Visa has been altered
        900210  => 'Authentication complete; transaction must be restarted', # Indicates that the MasterCard SecuerCode / Verified by Visa transaction has already been completed.  Most likely caused by the customer clicking the refresh button

        990024  => 'Duplicate Transaction Detected.  Please check before submitting',

        990028  => 'Transaction cancelled' # Customer clicks the 'Cancel' button on the payment page
      }

      SUCCESS_CODES = %w(990004 990005 990017 990012 990018 990031)

      TRANSACTION_CODES = {
        0 => 'Not Done',
        1 => 'Approved',
        2 => 'Declined',
        3 => 'Paid',
        4 => 'Refunded',
        5 => 'Received by PayGate',
        6 => 'Replied to Client'
      }

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

      def purchase(money, creditcard, options = {})
        MultiResponse.run do |r|
          r.process { authorize(money, creditcard, options) }
          r.process { capture(money, r.authorization, options) }
        end
      end

      def authorize(money, creditcard, options = {})
        action = 'authtx'

        options[:money] = money
        options[:creditcard] = creditcard
        commit(action, build_request(action, options))
      end

      def capture(money, authorization, options = {})
        action = 'settletx'

        options[:money] = money
        options[:authorization] = authorization
        commit(action, build_request(action, options), authorization)
      end

      def refund(money, authorization, options = {})
        action = 'refundtx'

        options[:money] = money
        options[:authorization] = authorization
        commit(action, build_request(action, options))
      end

      private

      def successful?(response)
        SUCCESS_CODES.include?(response[:res])
      end

      def build_request(action, options = {})
        xml = Builder::XmlMarkup.new
        xml.instruct!

        xml.tag! 'protocol', ver: API_VERSION, pgid: (test? ? TEST_ID : @options[:login]), pwd: @options[:password] do |protocol|
          money         = options.delete(:money)
          authorization = options.delete(:authorization)
          creditcard    = options.delete(:creditcard)
          case action
          when 'authtx'
            build_authorization(protocol, money, creditcard, options)
          when 'settletx'
            build_capture(protocol, money, authorization, options)
          when 'refundtx'
            build_refund(protocol, money, authorization, options)
          else
            raise 'no action specified for build_request'
          end
        end

        xml.target!
      end

      def build_authorization(xml, money, creditcard, options = {})
        xml.tag! 'authtx', {
          cref: options[:order_id],
          cname: creditcard.name,
          cc: creditcard.number,
          exp: "#{format(creditcard.month, :two_digits)}#{format(creditcard.year, :four_digits)}",
          budp: 0,
          amt: amount(money),
          cur: (options[:currency] || currency(money)),
          cvv: creditcard.verification_value,
          email: options[:email],
          ip: options[:ip]
        }
      end

      def build_capture(xml, money, authorization, options = {})
        xml.tag! 'settletx', {
          tid: authorization
        }
      end

      def build_refund(xml, money, authorization, options = {})
        xml.tag! 'refundtx', {
          tid: authorization,
          amt: amount(money)
        }
      end

      def parse(action, body)
        hash  = {}
        xml   = REXML::Document.new(body)

        response_action = action.gsub(/tx/, 'rx')
        root  = REXML::XPath.first(xml.root, response_action)
        # we might have gotten an error
        root  = REXML::XPath.first(xml.root, 'errorrx') if root.nil?
        root.attributes.each do |name, value|
          hash[name.to_sym] = value
        end
        hash
      end

      def commit(action, request, authorization = nil)
        response = parse(action, ssl_post(self.live_url, request))
        Response.new(
          successful?(response),
          message_from(response),
          response,
          test: test?,
          authorization: authorization || response[:tid]
        )
      end

      def message_from(response)
        (response[:rdesc] || response[:edesc])
      end
    end
  end
end