lib/killbill/helpers/active_merchant/gateway.rb in killbill-4.0.0 vs lib/killbill/helpers/active_merchant/gateway.rb in killbill-4.1.0

- old
+ new

@@ -2,19 +2,20 @@ module Plugin module ActiveMerchant require 'active_merchant' class Gateway - def self.wrap(gateway_builder, config) - Gateway.new(config, gateway_builder.call(config)) + def self.wrap(gateway_builder, config, logger) + Gateway.new(config, gateway_builder.call(config), logger) end attr_reader :config - def initialize(config, am_gateway) + def initialize(config, am_gateway, logger) @config = config @gateway = am_gateway + @logger = logger # Override urls if needed (there is no easy way to do it earlier, because AM uses class_attribute) @gateway.class.test_url = @config[:test_url] unless @config[:test_url].nil? @gateway.class.live_url = @config[:live_url] unless @config[:live_url].nil? end @@ -55,21 +56,91 @@ def refund(*args, &block) method_missing(:refund, *args, &block) end def method_missing(m, *args, &block) + options = {} + # The options hash should be the last argument, iterate through all to be safe args.reverse.each do |arg| - if arg.respond_to?(:has_key?) && Utils.normalized(arg, :skip_gw) - return ::ActiveMerchant::Billing::Response.new(true, 'Skipped Gateway call') + if arg.respond_to?(:has_key?) + options = arg + return ::ActiveMerchant::Billing::Response.new(true, 'Skipped Gateway call') if Utils.normalized(arg, :skip_gw) end end @gateway.send(m, *args, &block) + rescue ::ActiveMerchant::ConnectionError => e + # Need to unwrap it + until e.triggering_exception.nil? + e = e.triggering_exception + break unless e.is_a?(::ActiveMerchant::ConnectionError) + end + handle_exception(e, options) + rescue => e + handle_exception(e, options) end def respond_to?(method, include_private=false) @gateway.respond_to?(method, include_private) || super + end + + private + + UNKNOWN_CONNECTION_ERRORS = [ + # Corrupted stream (e.g. Zlib::BufError) + ::ActiveMerchant::InvalidResponseError, + # We attempted a payment, but the gateway replied >= 300. This is gateway specific, hopefully the individual + # gateway implementation knows how to rescue from it and this is not risen + ::ActiveMerchant::ResponseError, + # Should not be risen directly + ::ActiveMerchant::RetriableConnectionError, + ::ActiveMerchant::ActiveMerchantError + ] + + PROBABLY_UNKNOWN_CONNECTION_ERRORS = [ + EOFError, + Errno::ECONNRESET, + Timeout::Error, + Errno::ETIMEDOUT + ] + + SAFE_CONNECTION_ERRORS = [ + SocketError, + Errno::EHOSTUNREACH, + Errno::ECONNREFUSED, + ::OpenSSL::SSL::SSLError, + # Invalid certificate (e.g. OpenSSL::X509::CertificateError) + ::ActiveMerchant::ClientCertificateError + ] + + # See https://github.com/killbill/killbill-plugin-framework-ruby/issues/44 + def handle_exception(e, options = {}) + message = "#{e.class} #{e.message}" + + if SAFE_CONNECTION_ERRORS.include?(e.class) + # Easy case: we didn't attempt the payment + @logger.warn("Connection error with the gateway: #{message}") + payment_plugin_status = :CANCELED + else + # For anything else, tell Kill Bill we don't know. If the gateway supports retrieving a payment status, + # the plugin should implement get_payment_info accordingly for the Janitor. + # Otherwise, the transaction will need to be fixed manually using the admin APIs. + + # Note that PROBABLY_UNKNOWN_CONNECTION_ERRORS/UNKNOWN_CONNECTION_ERRORS are a bit _better_, as they can be expected and we don't have any control over them. + # Any other exception might be caused by a bug in our code! + if PROBABLY_UNKNOWN_CONNECTION_ERRORS.include?(e.class) || UNKNOWN_CONNECTION_ERRORS.include?(e.class) + @logger.warn("Unstable connection with the gateway: #{message}") + else + @logger.warn("Unexpected exception: #{message}") + end + + # Allow clients to force a PLUGIN_FAILURE instead of UNKNOWN (the default is a conservative behavior) + payment_plugin_status = Utils.normalized(options, :connection_errors_safe) ? :CANCELED : :UNDEFINED + end + + response_message = { :exception_class => e.class.to_s, :exception_message => e.message, :payment_plugin_status => payment_plugin_status }.to_json + ::ActiveMerchant::Billing::Response.new(false, response_message) end end end end end