# frozen_string_literal: true module Spree class Payment < Spree::Base module Processing # "process!" means: # - Do nothing when: # - There is no payment method # - The payment method does not require a source # - The payment is in the "processing" state # - 'auto_capture?' is false and the payment is already authorized. # - Raise an exception when: # - The source is missing or invalid # - The payment is in a state that cannot transition to 'processing' # (failed/void/invalid states). Note: 'completed' can transition to # 'processing' and thus calling #process! on a completed Payment # will attempt to re-authorize/re-purchase the payment. # - Otherwise: # - If 'auto_capture?' is true: # - Call #purchase on the payment gateway. (i.e. authorize+capture) # even if the payment is already completed. # - Else: # - Call #authorize on the payment gateway even if the payment is # already completed. def process! return if payment_method.nil? if payment_method.auto_capture? purchase! elsif pending? # do nothing. already authorized. else authorize! end end def authorize! return unless check_payment_preconditions! started_processing! protect_from_connection_error do response = payment_method.authorize( money.money.cents, source, gateway_options, ) pend! if handle_response(response) end end # Captures the entire amount of a payment. def purchase! return unless check_payment_preconditions! started_processing! protect_from_connection_error do response = payment_method.purchase( money.money.cents, source, gateway_options, ) complete! if handle_response(response) end capture_events.create!(amount: amount) end # Takes the amount in cents to capture. # Can be used to capture partial amounts of a payment, and will create # a new pending payment record for the remaining amount to capture later. def capture!(capture_amount = nil) return true if completed? return false unless amount.positive? capture_amount ||= money.money.cents started_processing! protect_from_connection_error do # Standard ActiveMerchant capture usage response = payment_method.capture( capture_amount, response_code, gateway_options ) money = ::Money.new(capture_amount, currency) capture_events.create!(amount: money.to_d) update!(amount: captured_amount) complete! if handle_response(response) end end def void_transaction! return true if void? return false unless amount.positive? protect_from_connection_error do if payment_method.payment_profiles_supported? # Gateways supporting payment profiles will need access to credit card object because this stores the payment profile information # so supply the authorization itself as well as the credit card, rather than just the authorization code response = payment_method.void(response_code, source, gateway_options) else # Standard ActiveMerchant void usage response = payment_method.void(response_code, gateway_options) end handle_void_response(response) end end def cancel! Spree::Config.payment_canceller.cancel(self) end def gateway_options order.reload options = { email: order.email, customer: order.email, customer_id: order.user_id, ip: order.last_ip_address, # Need to pass in a unique identifier here to make some # payment gateways happy. # # For more information, please see Spree::Payment#set_unique_identifier order_id: gateway_order_id, # The originator is passed to options used by the payment method. # One example of a place that it is used is in: # app/models/spree/payment_method/store_credit.rb originator: self } options[:shipping] = order.ship_total * 100 options[:tax] = order.additional_tax_total * 100 options[:subtotal] = order.item_total * 100 options[:discount] = order.promo_total * 100 options[:currency] = currency bill_address = source.try(:address) bill_address ||= order.bill_address options[:billing_address] = bill_address.try!(:active_merchant_hash) options[:shipping_address] = order.ship_address.try!(:active_merchant_hash) options end # The unique identifier to be passed in to the payment gateway def gateway_order_id "#{order.number}-#{number}" end def handle_void_response(response) record_response(response) if response.success? self.response_code = response.authorization void else gateway_error(response) end end private # @raises Spree::Core::GatewayError def check_payment_preconditions! return if processing? return unless payment_method return unless payment_method.source_required? unless source gateway_error(I18n.t('spree.payment_processing_failed')) end unless payment_method.supports?(source) invalidate! gateway_error(I18n.t('spree.payment_method_not_supported')) end true end # @returns true if the response is successful # @returns false (and calls #failure) if the response is not successful def handle_response(response) record_response(response) unless response.success? failure gateway_error(response) return false end unless response.authorization.nil? self.response_code = response.authorization self.avs_response = response.avs_result['code'] if response.cvv_result self.cvv_response_code = response.cvv_result['code'] self.cvv_response_message = response.cvv_result['message'] end end true end def record_response(response) log_entries.create!(parsed_payment_response_details_with_fallback: response) end def protect_from_connection_error yield rescue ActiveMerchant::ConnectionError => error gateway_error(error) end def gateway_error(error) message, log = case error when ActiveMerchant::Billing::Response [ error.params['message'] || error.params['response_reason_text'] || error.message, basic_response_info(error) ] when ActiveMerchant::ConnectionError [I18n.t('spree.unable_to_connect_to_gateway')] * 2 else [error.to_s, error] end logger.error("#{I18n.t('spree.gateway_error')}: #{log}") raise Core::GatewayError.new(message) end # The gateway response information without the params since the params # can contain PII. def basic_response_info(response) { message: response.message, test: response.test, authorization: response.authorization, avs_result: response.avs_result, cvv_result: response.cvv_result, error_code: response.error_code, emv_authorization: response.emv_authorization, gateway_order_id: gateway_order_id, order_number: order.number } end end end end