module Killbill::PaypalExpress class PaypalExpressResponse < ActiveRecord::Base has_one :paypal_express_transaction attr_accessible :api_call, :kb_payment_id, :message, # transaction_id, authorization_id (reauthorization) or refund_transaction_id :authorization, :fraud_review, :test, :token, :payer_id, :billing_agreement_id, :payer_name, :payer_email, :payer_country, :contact_phone, :ship_to_address_name, :ship_to_address_company, :ship_to_address_address1, :ship_to_address_address2, :ship_to_address_city, :ship_to_address_state, :ship_to_address_country, :ship_to_address_zip, :ship_to_address_phone, :receiver_info_business, :receiver_info_receiver, :receiver_info_receiverid, :payment_info_transactionid, :payment_info_parenttransactionid, :payment_info_receiptid, :payment_info_transactiontype, :payment_info_paymenttype, :payment_info_paymentdate, :payment_info_grossamount, :payment_info_feeamount, :payment_info_taxamount, :payment_info_exchangerate, :payment_info_paymentstatus, :payment_info_pendingreason, :payment_info_reasoncode, :payment_info_protectioneligibility, :payment_info_protectioneligibilitytype, :payment_info_shipamount, :payment_info_shiphandleamount, :payment_info_shipdiscount, :payment_info_insuranceamount, :payment_info_subject, :avs_result_code, :avs_result_message, :avs_result_street_match, :avs_result_postal_match, :cvv_result_code, :cvv_result_message, :success def self.from_response(api_call, kb_payment_id, response) PaypalExpressResponse.new({ :api_call => api_call, :kb_payment_id => kb_payment_id, :message => response.message, :authorization => response.authorization, :fraud_review => response.fraud_review?, :test => response.test?, :token => response.token, :payer_id => response.payer_id, :billing_agreement_id => response.params['billing_agreement_id'], :payer_name => response.name, :payer_email => response.email, :payer_country => response.payer_country, :contact_phone => response.contact_phone, :ship_to_address_name => response.address['name'], :ship_to_address_company => response.address['company'], :ship_to_address_address1 => response.address['address1'], :ship_to_address_address2 => response.address['address2'], :ship_to_address_city => response.address['city'], :ship_to_address_state => response.address['state'], :ship_to_address_country => response.address['country'], :ship_to_address_zip => response.address['zip'], :ship_to_address_phone => response.address['phone'], :receiver_info_business => receiver_info(response)['Business'], :receiver_info_receiver => receiver_info(response)['Receiver'], :receiver_info_receiverid => receiver_info(response)['ReceiverID'], :payment_info_transactionid => payment_info(response)['TransactionID'], :payment_info_parenttransactionid => payment_info(response)['ParentTransactionID'], :payment_info_receiptid => payment_info(response)['ReceiptID'], :payment_info_transactiontype => payment_info(response)['TransactionType'], :payment_info_paymenttype => payment_info(response)['PaymentType'], :payment_info_paymentdate => payment_info(response)['PaymentDate'], :payment_info_grossamount => payment_info(response)['GrossAmount'], :payment_info_feeamount => payment_info(response)['FeeAmount'], :payment_info_taxamount => payment_info(response)['TaxAmount'], :payment_info_exchangerate => payment_info(response)['ExchangeRate'], :payment_info_paymentstatus => payment_info(response)['PaymentStatus'], :payment_info_pendingreason => payment_info(response)['PendingReason'], :payment_info_reasoncode => payment_info(response)['ReasonCode'], :payment_info_protectioneligibility => payment_info(response)['ProtectionEligibility'], :payment_info_protectioneligibilitytype => payment_info(response)['ProtectionEligibilityType'], :payment_info_shipamount => payment_info(response)['ShipAmount'], :payment_info_shiphandleamount => payment_info(response)['ShipHandleAmount'], :payment_info_shipdiscount => payment_info(response)['ShipDiscount'], :payment_info_insuranceamount => payment_info(response)['InsuranceAmount'], :payment_info_subject => payment_info(response)['Subject'], :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? }) end def to_express_checkout_url url = Killbill::PaypalExpress.test ? Killbill::PaypalExpress.paypal_sandbox_url : Killbill::PaypalExpress.paypal_production_url "#{url}?cmd=_express-checkout&token=#{token}" end def to_payment_response to_killbill_response :payment end def to_refund_response to_killbill_response :refund end # VisibleForTesting def self.search_query(api_call, search_key, offset = nil, limit = nil) t = self.arel_table # Exact matches only where_clause = t[:authorization].eq(search_key) .or(t[:billing_agreement_id].eq(search_key)) .or(t[:payment_info_transactionid].eq(search_key)) # Only search successful payments and refunds where_clause = where_clause.and(t[:api_call].eq(api_call)) .and(t[:success].eq(true)) query = t.where(where_clause) .order(t[:id]) 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, offset = 0, limit = 100, type = :payment) api_call = type == :payment ? 'charge' : 'refund' pagination = Killbill::Plugin::Model::Pagination.new pagination.current_offset = offset pagination.total_nb_records = self.count_by_sql(self.search_query(api_call, search_key)) pagination.max_nb_records = self.where(:api_call => api_call, :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 = StreamyResultSet.new(actual_limit) do |offset,limit| self.find_by_sql(self.search_query(api_call, search_key, offset, limit)) .map { |x| type == :payment ? x.to_payment_response : x.to_refund_response } end pagination end private def to_killbill_response(type) if paypal_express_transaction.nil? # payment_info_grossamount may have a value but we cannot convert it to cents since we don't have the currency amount_in_cents = nil currency = nil created_date = created_at first_payment_reference_id = nil second_payment_reference_id = nil else amount_in_cents = paypal_express_transaction.amount_in_cents currency = paypal_express_transaction.currency created_date = paypal_express_transaction.created_at first_reference_id = paypal_express_transaction.paypal_express_txn_id second_reference_id = nil end effective_date = created_date gateway_error = message gateway_error_code = nil if type == :payment p_info_plugin = Killbill::Plugin::Model::PaymentInfoPlugin.new p_info_plugin.kb_payment_id = kb_payment_id p_info_plugin.amount = Money.new(amount_in_cents, currency).to_d if currency p_info_plugin.currency = currency p_info_plugin.created_date = created_date p_info_plugin.effective_date = effective_date p_info_plugin.status = (success ? :PROCESSED : :ERROR) p_info_plugin.gateway_error = gateway_error p_info_plugin.gateway_error_code = gateway_error_code p_info_plugin.first_payment_reference_id = first_reference_id p_info_plugin.second_payment_reference_id = second_reference_id p_info_plugin else r_info_plugin = Killbill::Plugin::Model::RefundInfoPlugin.new r_info_plugin.kb_payment_id = kb_payment_id r_info_plugin.amount = Money.new(amount_in_cents, currency).to_d if currency r_info_plugin.currency = currency r_info_plugin.created_date = created_date r_info_plugin.effective_date = effective_date r_info_plugin.status = (success ? :PROCESSED : :ERROR) r_info_plugin.gateway_error = gateway_error r_info_plugin.gateway_error_code = gateway_error_code r_info_plugin.first_refund_reference_id = first_reference_id r_info_plugin.second_refund_reference_id = second_reference_id r_info_plugin end end # Paypal has various response formats depending on the API call and the ActiveMerchant Paypal plugin doesn't try to # unify them, hence the gymnastic here def self.receiver_info(response) response.params['ReceiverInfo'] || (response.params['PaymentTransactionDetails'] || {})['ReceiverInfo'] || {} end def self.payment_info(response) response.params['PaymentInfo'] || (response.params['PaymentTransactionDetails'] || {})['PaymentInfo'] || {} end end end