Shopify/active_merchant

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

Summary

Maintainability
B
4 hrs
Test Coverage
require 'active_merchant/billing/gateways/migs/migs_codes'

module ActiveMerchant #:nodoc:
  module Billing #:nodoc:
    class MigsGateway < Gateway
      include MigsCodes

      API_VERSION = 1

      class_attribute :server_hosted_url, :merchant_hosted_url

      self.server_hosted_url = 'https://migs.mastercard.com.au/vpcpay'
      self.merchant_hosted_url = 'https://migs.mastercard.com.au/vpcdps'

      self.live_url = self.server_hosted_url

      # MiGS is supported throughout Asia Pacific, Middle East and Africa
      # MiGS is used in Australia (AU) by ANZ (eGate), CBA (CommWeb) and more
      # Source of Country List: http://www.scribd.com/doc/17811923
      self.supported_countries = %w(AU AE BD BN EG HK ID JO KW LB LK MU MV MY NZ OM PH QA SA SG TT VN)

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

      self.money_format = :cents
      self.currencies_without_fractions = %w(IDR)

      # The homepage URL of the gateway
      self.homepage_url = 'http://mastercard.com/mastercardsps'

      # The name of the gateway
      self.display_name = 'MasterCard Internet Gateway Service (MiGS)'

      # Creates a new MigsGateway
      # The advanced_login/advanced_password fields are needed for
      # advanced methods such as the capture, refund and status methods
      #
      # ==== Options
      #
      # * <tt>:login</tt> -- The MiGS Merchant ID (REQUIRED)
      # * <tt>:password</tt> -- The MiGS Access Code (REQUIRED)
      # * <tt>:secure_hash</tt> -- The MiGS Secure Hash
      # (Required for Server Hosted payments)
      # * <tt>:advanced_login</tt> -- The MiGS AMA User
      # * <tt>:advanced_password</tt> -- The MiGS AMA User's password
      def initialize(options = {})
        requires!(options, :login, :password)
        super
      end

      # ==== Options
      #
      # * <tt>:order_id</tt> -- A reference for tracking the order (REQUIRED)
      # * <tt>:unique_id</tt> -- A unique id for this request (Max 40 chars).
      # If not supplied one will be generated.
      def purchase(money, creditcard, options = {})
        requires!(options, :order_id)

        post = {}

        add_amount(post, money, options)
        add_invoice(post, options)
        add_creditcard(post, creditcard)
        add_standard_parameters('pay', post, options[:unique_id])
        add_3ds(post, options)
        add_tx_source(post, options)

        commit(post)
      end

      # MiGS works by merchants being either purchase only or authorize/capture
      # So authorize is the same as purchase when in authorize mode
      alias authorize purchase

      # ==== Options
      #
      # * <tt>:unique_id</tt> -- A unique id for this request (Max 40 chars).
      # If not supplied one will be generated.
      def capture(money, authorization, options = {})
        requires!(@options, :advanced_login, :advanced_password)

        post = options.merge(TransNo: authorization)

        add_amount(post, money, options)
        add_advanced_user(post)
        add_standard_parameters('capture', post, options[:unique_id])
        add_tx_source(post, options)

        commit(post)
      end

      # ==== Options
      #
      # * <tt>:unique_id</tt> -- A unique id for this request (Max 40 chars).
      # If not supplied one will be generated.
      def refund(money, authorization, options = {})
        requires!(@options, :advanced_login, :advanced_password)

        post = options.merge(TransNo: authorization)

        add_amount(post, money, options)
        add_advanced_user(post)
        add_standard_parameters('refund', post, options[:unique_id])
        add_tx_source(post, options)

        commit(post)
      end

      def void(authorization, options = {})
        requires!(@options, :advanced_login, :advanced_password)

        post = options.merge(TransNo: authorization)

        add_advanced_user(post)
        add_standard_parameters('voidAuthorisation', post, options[:unique_id])
        add_tx_source(post, options)

        commit(post)
      end

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

      def verify(credit_card, options = {})
        MultiResponse.run do |r|
          r.process { authorize(100, credit_card, options) }
          r.process(:ignore_result) { void(r.authorization, options) }
        end
      end

      # Checks the status of a previous transaction
      # This can be useful when a response is not received due to network issues
      #
      # ==== Parameters
      #
      # * <tt>unique_id</tt> -- Unique id of transaction to find.
      #   This is the value of the option supplied in other methods or
      #   if not supplied is returned with key :MerchTxnRef
      def status(unique_id)
        requires!(@options, :advanced_login, :advanced_password)

        post = {}
        add_advanced_user(post)
        add_standard_parameters('queryDR', post, unique_id)

        commit(post)
      end

      # Generates a URL to redirect user to MiGS to process payment
      # Once user is finished MiGS will redirect back to specified URL
      # With a response hash which can be turned into a Response object
      # with purchase_offsite_response
      #
      # ==== Options
      #
      # * <tt>:order_id</tt> -- A reference for tracking the order (REQUIRED)
      # * <tt>:locale</tt> -- Change the language of the redirected page
      #   Values are 2 digit locale, e.g. en, es
      # * <tt>:return_url</tt> -- the URL to return to once the payment is complete
      # * <tt>:card_type</tt> -- Providing this skips the card type step.
      #   Values are ActiveMerchant formats: e.g. master, visa, american_express, diners_club
      # * <tt>:unique_id</tt> -- Unique id of transaction to find.
      #   If not supplied one will be generated.
      def purchase_offsite_url(money, options = {})
        requires!(options, :order_id, :return_url)
        requires!(@options, :secure_hash)

        post = {}

        add_amount(post, money, options)
        add_invoice(post, options)
        add_creditcard_type(post, options[:card_type]) if options[:card_type]

        post[:Locale] = options[:locale] || 'en'
        post[:ReturnURL] = options[:return_url]

        add_standard_parameters('pay', post, options[:unique_id])

        add_secure_hash(post)

        self.server_hosted_url + '?' + post_data(post)
      end

      # Parses a response from purchase_offsite_url once user is redirected back
      #
      # ==== Parameters
      #
      # * <tt>data</tt> -- All params when offsite payment returns
      # e.g. returns to http://company.com/return?a=1&b=2, then input "a=1&b=2"
      def purchase_offsite_response(data)
        requires!(@options, :secure_hash)

        response_hash = parse(data)

        expected_secure_hash = calculate_secure_hash(response_hash, @options[:secure_hash])
        raise SecurityError, 'Secure Hash mismatch, response may be tampered with' unless response_hash[:SecureHash] == expected_secure_hash

        response_object(response_hash)
      end

      def test?
        @options[:login].start_with?('TEST')
      end

      def supports_scrubbing?
        true
      end

      def scrub(transcript)
        transcript.
          gsub(%r((&?CardNum=)\d*(&?)), '\1[FILTERED]\2').
          gsub(%r((&?CardSecurityCode=)\d*(&?)), '\1[FILTERED]\2').
          gsub(%r((&?AccessCode=)[^&]*(&?)), '\1[FILTERED]\2').
          gsub(%r((&?Password=)[^&]*(&?)), '\1[FILTERED]\2').
          gsub(%r((&?3DSXID=)[^&]*(&?)), '\1[FILTERED]\2').
          gsub(%r((&?VerToken=)[^&]*(&?)), '\1[FILTERED]\2')
      end

      private

      def add_amount(post, money, options)
        post[:Amount] = localized_amount(money, options[:currency])
        post[:Currency] = options[:currency] if options[:currency]
      end

      def add_advanced_user(post)
        post[:User] = @options[:advanced_login]
        post[:Password] = @options[:advanced_password]
      end

      def add_invoice(post, options)
        post[:OrderInfo] = options[:order_id]
      end

      def add_3ds(post, options)
        post[:VerType] = options[:ver_type] if options[:ver_type]
        post[:VerToken] = options[:ver_token] if options[:ver_token]
        post['3DSXID'] = options[:three_ds_xid] if options[:three_ds_xid]
        post['3DSECI'] = options[:three_ds_eci] if options[:three_ds_eci]
        post['3DSenrolled'] = options[:three_ds_enrolled] if options[:three_ds_enrolled]
        post['3DSstatus'] = options[:three_ds_status] if options[:three_ds_status]
      end

      def add_tx_source(post, options)
        post[:TxSource] = options[:tx_source] if options[:tx_source]
      end

      def add_creditcard(post, creditcard)
        post[:CardNum] = creditcard.number
        post[:CardSecurityCode] = creditcard.verification_value if creditcard.verification_value?
        post[:CardExp] = format(creditcard.year, :two_digits) + format(creditcard.month, :two_digits)
      end

      def add_creditcard_type(post, card_type)
        post[:Gateway] = 'ssl'
        post[:card] = CARD_TYPES.detect { |ct| ct.am_code == card_type }.migs_long_code
      end

      def parse(body)
        params = CGI::parse(body)
        hash = {}
        params.each do |key, value|
          hash[key.gsub('vpc_', '').to_sym] = value[0]
        end
        hash
      end

      def commit(post)
        add_secure_hash(post) if @options[:secure_hash]
        data = ssl_post self.merchant_hosted_url, post_data(post)
        response_hash = parse(data)
        response_object(response_hash)
      end

      def response_object(response)
        avs_response_code = response[:AVSResultCode]
        avs_response_code = 'S' if avs_response_code == 'Unsupported'

        cvv_result_code = response[:CSCResultCode]
        cvv_result_code = 'P' if cvv_result_code == 'Unsupported'

        Response.new(
          success?(response),
          response[:Message],
          response,
          test: test?,
          authorization: response[:TransactionNo],
          fraud_review: fraud_review?(response),
          avs_result: { code: avs_response_code },
          cvv_result: cvv_result_code
        )
      end

      def success?(response)
        response[:TxnResponseCode] == '0'
      end

      def fraud_review?(response)
        ISSUER_RESPONSE_CODES[response[:AcqResponseCode]] == 'Suspected Fraud'
      end

      def add_standard_parameters(action, post, unique_id = nil)
        post.merge!(
          Version: API_VERSION,
          Merchant: @options[:login],
          AccessCode: @options[:password],
          Command: action,
          MerchTxnRef: unique_id || generate_unique_id.slice(0, 40)
        )
      end

      def post_data(post)
        post.collect { |key, value| "vpc_#{key}=#{CGI.escape(value.to_s)}" }.join('&')
      end

      def add_secure_hash(post)
        post[:SecureHash] = calculate_secure_hash(post, @options[:secure_hash])
        post[:SecureHashType] = 'SHA256'
      end

      def calculate_secure_hash(post, secure_hash)
        input = post.
                reject { |k| %i[SecureHash SecureHashType].include?(k) }.
                sort.
                map { |(k, v)| "vpc_#{k}=#{v}" }.
                join('&')
        OpenSSL::HMAC.hexdigest('SHA256', [secure_hash].pack('H*'), input).upcase
      end
    end
  end
end