app/models/spree/payment.rb
# frozen_string_literal: true
require "spree/localized_number"
module Spree
class Payment < ApplicationRecord
include Spree::Payment::Processing
extend Spree::LocalizedNumber
self.belongs_to_required_by_default = false
localize_number :amount
IDENTIFIER_CHARS = (('A'..'Z').to_a + ('0'..'9').to_a - %w(0 1 I O)).freeze
delegate :line_items, to: :order
delegate :currency, to: :order
belongs_to :order, class_name: 'Spree::Order'
belongs_to :source, polymorphic: true
belongs_to :payment_method, class_name: 'Spree::PaymentMethod'
has_many :offsets, -> { where("source_type = 'Spree::Payment' AND amount < 0").completed },
class_name: "Spree::Payment", foreign_key: :source_id,
dependent: :restrict_with_exception
has_many :log_entries, as: :source, dependent: :destroy
has_one :adjustment, as: :adjustable, dependent: :destroy
validate :validate_source
after_initialize :build_source
before_create :set_unique_identifier
# invalidate previously entered payments
after_create :invalidate_old_payments
after_save :create_payment_profile, if: :profiles_supported?
# update the order totals, etc.
after_save :ensure_correct_adjustment, :update_order
# Skips the validation of the source (for example, credit card) of the payment.
#
# This is used in refunds as the validation of the card can fail but the refund can go through,
# we trust the payment gateway in these cases. For example, Stripe is accepting refunds with
# source cards that were valid when the payment was placed but are now expired, and we
# consider them invalid.
attr_accessor :skip_source_validation
attr_accessor :source_attributes
scope :from_credit_card, -> { where(source_type: 'Spree::CreditCard') }
scope :with_state, ->(s) { where(state: s.to_s) }
scope :completed, -> { with_state('completed') }
scope :incomplete, -> { where(state: %w(checkout pending requires_authorization)) }
scope :checkout, -> { with_state('checkout') }
scope :pending, -> { with_state('pending') }
scope :failed, -> { with_state('failed') }
scope :valid, -> { where.not(state: %w(failed invalid)) }
scope :void, -> { with_state('void') }
scope :authorization_action_required, -> { where.not(cvv_response_message: nil) }
scope :requires_authorization, -> { with_state("requires_authorization") }
scope :with_payment_intent, ->(code) { where(response_code: code) }
# order state machine (see http://github.com/pluginaweek/state_machine/tree/master for details)
state_machine initial: :checkout do
# With card payments, happens before purchase or authorization happens
event :started_processing do
transition from: [:checkout, :pending, :completed, :processing, :requires_authorization],
to: :processing
end
# When processing during checkout fails
event :failure do
transition from: [:pending, :processing, :requires_authorization], to: :failed
end
# With card payments this represents authorizing the payment
event :pend do
transition from: [:checkout, :processing], to: :pending
end
# With card payments this represents completing a purchase or capture transaction
event :complete do
transition from: [:processing, :pending, :checkout, :requires_authorization], to: :completed
end
event :void do
transition from: [:pending, :completed, :requires_authorization, :checkout], to: :void
end
# when the card brand isnt supported
event :invalidate do
transition from: [:checkout], to: :invalid
end
event :require_authorization do
transition from: [:checkout, :processing], to: :requires_authorization
end
event :fail_authorization do
transition from: [:requires_authorization], to: :failed
end
event :complete_authorization do
transition from: [:requires_authorization], to: :completed
end
event :resume do
transition from: [:void], to: :checkout
end
after_transition to: :completed, do: :set_captured_at
end
def money
Spree::Money.new(amount, currency:)
end
alias display_amount money
def offsets_total
offsets.pluck(:amount).sum
end
def credit_allowed
amount - offsets_total
end
def can_credit?
credit_allowed.positive?
end
def build_source
return if source_attributes.nil?
return unless payment_method&.payment_source_class
self.source = payment_method.payment_source_class.new(source_attributes)
source.payment_method_id = payment_method.id
source.user_id = order.user_id if order
end
def actions
return [] unless payment_source&.respond_to?(:actions)
payment_source.actions.select do |action|
!payment_source.respond_to?("can_#{action}?") ||
payment_source.__send__("can_#{action}?", self)
end
end
def resend_authorization_email!
return unless requires_authorization?
PaymentMailer.authorize_payment(self).deliver_later
end
def payment_source
res = source.is_a?(Payment) ? source.source : source
res || payment_method
end
def ensure_correct_adjustment
revoke_adjustment_eligibility if ['failed', 'invalid', 'void'].include?(state)
return if adjustment.try(:finalized?)
if adjustment
adjustment.originator = payment_method
adjustment.label = adjustment_label
adjustment.save
elsif !processing_refund? && payment_method.present?
payment_method.create_adjustment(adjustment_label, self, true)
adjustment.reload
end
end
def adjustment_label
I18n.t('payment_method_fee')
end
def clear_authorization_url
update_attribute(:cvv_response_message, nil)
end
private
def processing_refund?
amount.negative?
end
# Don't charge fees for invalid or failed payments.
# This is called twice for failed payments, because the persistence of the 'failed'
# state is acheived through some trickery using an after_rollback callback on the
# payment model. See Spree::Payment#persist_invalid
def revoke_adjustment_eligibility
return unless adjustment.try(:reload)
return if adjustment.finalized?
adjustment.update(
eligible: false,
state: "finalized"
)
end
def validate_source
if source && !skip_source_validation && !source.valid?
source.errors.each do |error|
field_name =
I18n.t("activerecord.attributes.#{source.class.to_s.underscore}.#{error.attribute}")
errors.add(Spree.t(source.class.to_s.demodulize.underscore),
"#{field_name} #{error.message}")
end
end
errors.blank?
end
def profiles_supported?
payment_method.respond_to?(:payment_profiles_supported?) &&
payment_method.payment_profiles_supported?
end
def create_payment_profile
return unless source.is_a?(CreditCard)
return unless source.try(:save_requested_by_customer?)
return unless source.number || source.gateway_payment_profile_id
return unless source.gateway_customer_profile_id.nil?
payment_method.create_profile(self)
rescue ActiveMerchant::ConnectionError => e
gateway_error e
end
# Makes newly entered payments invalidate previously entered payments so the most recent payment
# is used by the gateway.
def invalidate_old_payments
order.payments.incomplete.where.not(id:).each do |payment|
# Using update_column skips validations and so it skips validate_source. As we are just
# invalidating past payments here, we don't want to validate all of them at this stage.
payment.update_columns(
state: 'invalid',
updated_at: Time.zone.now
)
payment.ensure_correct_adjustment
end
end
def update_order
OrderManagement::Order::Updater.new(order).after_payment_update(self)
end
def set_captured_at
update_column(:captured_at, Time.zone.now)
end
# Necessary because some payment gateways will refuse payments with
# duplicate IDs. We *were* using the Order number, but that's set once and
# is unchanging. What we need is a unique identifier on a per-payment basis,
# and this is it. Related to #1998.
# See https://github.com/spree/spree/issues/1998#issuecomment-12869105
def set_unique_identifier
self.identifier = generate_identifier while self.class.where(identifier:).exists?
end
def generate_identifier
Array.new(8){ IDENTIFIER_CHARS.sample }.join
end
end
end