module Killbill module Plugin module ActiveMerchant module ActiveRecord require 'active_record' require 'active_merchant' require 'money' require 'time' require 'killbill/helpers/active_merchant/active_record/models/helpers' class Response < ::ActiveRecord::Base extend ::Killbill::Plugin::ActiveMerchant::Helpers self.abstract_class = true self.record_timestamps = false @@quotes_cache = build_quotes_cache def self.from_response(api_call, kb_account_id, kb_payment_id, kb_payment_transaction_id, transaction_type, payment_processor_account_id, kb_tenant_id, response, extra_params = {}, model = Response) # Under high load, Rails sometimes fails to set timestamps. Unclear why... # But regardless, for performance reasons, we want to set these timestamps ourselves # See ActiveRecord::Timestamp current_time = Time.now.utc model.new({ :api_call => api_call, :kb_account_id => kb_account_id, :kb_payment_id => kb_payment_id, :kb_payment_transaction_id => kb_payment_transaction_id, :transaction_type => transaction_type, :payment_processor_account_id => payment_processor_account_id, :kb_tenant_id => kb_tenant_id, :message => response.message, :authorization => response.authorization, :fraud_review => response.fraud_review?, :test => response.test?, :avs_result_code => response.avs_result.kind_of?(::ActiveMerchant::Billing::AVSResult) ? response.avs_result.code : response.avs_result['code'], :avs_result_message => response.avs_result.kind_of?(::ActiveMerchant::Billing::AVSResult) ? response.avs_result.message : response.avs_result['message'], :avs_result_street_match => response.avs_result.kind_of?(::ActiveMerchant::Billing::AVSResult) ? response.avs_result.street_match : response.avs_result['street_match'], :avs_result_postal_match => response.avs_result.kind_of?(::ActiveMerchant::Billing::AVSResult) ? response.avs_result.postal_match : response.avs_result['postal_match'], :cvv_result_code => response.cvv_result.kind_of?(::ActiveMerchant::Billing::CVVResult) ? response.cvv_result.code : response.cvv_result['code'], :cvv_result_message => response.cvv_result.kind_of?(::ActiveMerchant::Billing::CVVResult) ? response.cvv_result.message : response.cvv_result['message'], :success => response.success?, :created_at => current_time, :updated_at => current_time }.merge!(extra_params.compact)) # Don't override with nil values end def self.create_response_and_transaction(identifier, transaction_model, api_call, kb_account_id, kb_payment_id, kb_payment_transaction_id, transaction_type, payment_processor_account_id, kb_tenant_id, gw_response, amount_in_cents, currency, extra_params = {}, model = Response) response, transaction, exception = nil # Rails wraps all create/save calls in a transaction. To speed things up, create a single transaction for both rows. # This has a small gotcha in the unhappy path though (see below). with_connection_and_transaction do # Save the response to our logs response = from_response(api_call, kb_account_id, kb_payment_id, kb_payment_transaction_id, transaction_type, payment_processor_account_id, kb_tenant_id, gw_response, extra_params, model) response.save!(shared_activerecord_options) transaction = nil txn_id = response.txn_id if response.success and !kb_payment_id.blank? # Record the transaction # Note that we want to avoid throwing an exception here because we don't want to rollback the response row begin # Originally, we used response.send("build_#{identifier}_transaction"), but the ActiveRecord magic was adding # about 20% overhead - instead, we now construct the transaction record manually transaction = transaction_model.new(:kb_account_id => kb_account_id, :kb_tenant_id => kb_tenant_id, :amount_in_cents => amount_in_cents, :currency => currency, :api_call => api_call, :kb_payment_id => kb_payment_id, :kb_payment_transaction_id => kb_payment_transaction_id, :transaction_type => transaction_type, :payment_processor_account_id => payment_processor_account_id, :txn_id => txn_id, "#{identifier}_response_id" => response.id, # See Response#from_response :created_at => response.created_at, :updated_at => response.updated_at) transaction.save!(shared_activerecord_options) rescue => e exception = e end end end raise exception unless exception.nil? return response, transaction end def self.from_kb_payment_id(transaction_model, kb_payment_id, kb_tenant_id) association = transaction_model.table_name.singularize.to_sym # Use eager_load to force Rails to issue a single query (see https://github.com/killbill/killbill-plugin-framework-ruby/issues/32) eager_load(association) .where(:kb_payment_id => kb_payment_id, :kb_tenant_id => kb_tenant_id) .order(:created_at) end def to_transaction_info_plugin(transaction=nil) error_details = {} if transaction.nil? amount_in_cents = nil currency = nil created_date = created_at # See Killbill::Plugin::ActiveMerchant::Gateway error_details = JSON.parse(message) rescue {} else amount_in_cents = transaction.amount_in_cents currency = transaction.currency created_date = transaction.created_at end # See https://github.com/killbill/killbill-plugin-framework-ruby/issues/43 # Note: status could also be :PENDING, but it would be handled only in the plugins which need it if !error_details['payment_plugin_status'].blank? status = error_details['payment_plugin_status'].to_sym else # Note: (success && transaction.nil?) _could_ happen (see above), but it would be an issue on our side # (the payment did go through in the gateway). status = success ? :PROCESSED : :ERROR end t_info_plugin = Killbill::Plugin::Model::PaymentTransactionInfoPlugin.new t_info_plugin.kb_payment_id = kb_payment_id t_info_plugin.kb_transaction_payment_id = kb_payment_transaction_id t_info_plugin.transaction_type = transaction_type.nil? ? nil : transaction_type.to_sym t_info_plugin.amount = Money.new(amount_in_cents, currency).to_d if currency t_info_plugin.currency = currency t_info_plugin.created_date = created_date t_info_plugin.effective_date = effective_date t_info_plugin.status = status t_info_plugin.gateway_error = error_details['exception_message'] || gateway_error t_info_plugin.gateway_error_code = error_details['exception_class'] || gateway_error_code t_info_plugin.first_payment_reference_id = first_reference_id t_info_plugin.second_payment_reference_id = second_reference_id properties = [] properties << create_plugin_property('payment_processor_account_id', payment_processor_account_id) properties << create_plugin_property('message', message) properties << create_plugin_property('authorization', authorization) properties << create_plugin_property('fraudReview', fraud_review) properties << create_plugin_property('test', self.read_attribute(:test)) properties << create_plugin_property('avsResultCode', avs_result_code) properties << create_plugin_property('avsResultMessage', avs_result_message) properties << create_plugin_property('avsResultStreetMatch', avs_result_street_match) properties << create_plugin_property('avsResultPostalMatch', avs_result_postal_match) properties << create_plugin_property('cvvResultCode', cvv_result_code) properties << create_plugin_property('cvvResultMessage', cvv_result_message) properties << create_plugin_property('success', success) t_info_plugin.properties = properties t_info_plugin end # Override in your plugin if needed def self.search_where_clause(t, search_key) # Exact matches only where_clause = t[:kb_payment_id].eq(search_key) .or(t[:kb_payment_transaction_id].eq(search_key)) .or(t[:message].eq(search_key)) .or(t[:authorization].eq(search_key)) .or(t[:fraud_review].eq(search_key)) # Only search successful payments and refunds where_clause = where_clause.and(t[:success].eq(true)) where_clause end # VisibleForTesting def self.search_query(search_key, kb_tenant_id, offset = nil, limit = nil) t = self.arel_table if kb_tenant_id.nil? query = t.where(search_where_clause(t, search_key)) .order(t[:id]) else query = t.where(search_where_clause(t, search_key).and(t[:kb_tenant_id].eq(kb_tenant_id))) .order(t[:id]) end if offset.blank? and limit.blank? # true is for count distinct query.project(t[:id].count(true)) else query.skip(offset) unless offset.blank? query.take(limit) unless limit.blank? query.project(t[Arel.star]) # Not chainable query.distinct end query end def self.search(search_key, kb_tenant_id, offset = 0, limit = 100) pagination = ::Killbill::Plugin::Model::Pagination.new pagination.current_offset = offset pagination.total_nb_records = self.count_by_sql(self.search_query(search_key, kb_tenant_id)) pagination.max_nb_records = self.where(:success => true).count pagination.next_offset = (!pagination.total_nb_records.nil? && offset + limit >= pagination.total_nb_records) ? nil : offset + limit # Reduce the limit if the specified value is larger than the number of records actual_limit = [pagination.max_nb_records, limit].min pagination.iterator = ::Killbill::Plugin::ActiveMerchant::ActiveRecord::StreamyResultSet.new(actual_limit) do |offset, limit| self.find_by_sql(self.search_query(search_key, kb_tenant_id, offset, limit)).map { |x| x.to_transaction_info_plugin } end pagination end # Override in your plugin if needed def txn_id authorization end # Override in your plugin if needed def first_reference_id nil end # Override in your plugin if needed def second_reference_id nil end # Override in your plugin if needed def effective_date created_at end # Override in your plugin if needed def gateway_error message end # Override in your plugin if needed def gateway_error_code nil end private def create_plugin_property(key, value) prop = Killbill::Plugin::Model::PluginProperty.new prop.key = key prop.value = value prop end class << self [:kb_payment_id, :kb_payment_transaction_id].each do |attribute| define_method("responses_from_#{attribute.to_s}") do |transaction_type, attribute_value, kb_tenant_id, how_many = :multiple| if kb_tenant_id.nil? if transaction_type.nil? transactions = where("kb_tenant_id is NULL AND #{attribute.to_s} = ?", attribute_value).order(:created_at) else transactions = where("transaction_type = #{@@quotes_cache[transaction_type]} AND kb_tenant_id is NULL AND #{attribute.to_s} = #{@@quotes_cache[attribute_value]}").order(:created_at) end else if transaction_type.nil? transactions = where("kb_tenant_id = #{@@quotes_cache[kb_tenant_id]} AND #{attribute.to_s} = #{@@quotes_cache[attribute_value]}").order(:created_at) else transactions = where("transaction_type = #{@@quotes_cache[transaction_type]} AND kb_tenant_id = #{@@quotes_cache[kb_tenant_id]} AND #{attribute.to_s} = #{@@quotes_cache[attribute_value]}").order(:created_at) end end if how_many == :single raise "Kill Bill #{attribute} = #{attribute_value} mapping to multiple plugin transactions" if transactions.size > 1 transactions[0] else transactions end end end end end end end end end