mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-02-06 22:36:07 +00:00
It would take ages to go through all files now and assess all belongs_to associations. So I just declare the old default and then we can move on and apply the new default for the application while these classes still use the old one. All new models will then use the new default which is the goal of this excercise and we can refactor old classes when we touch them anyway.
256 lines
8.6 KiB
Ruby
256 lines
8.6 KiB
Ruby
# 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
|
|
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: 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: 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.exists?(identifier: identifier)
|
|
end
|
|
|
|
def generate_identifier
|
|
Array.new(8){ IDENTIFIER_CHARS.sample }.join
|
|
end
|
|
end
|
|
end
|