Files
openfoodnetwork/app/models/spree/adjustment.rb
Gaetan Craig-Riou 1279ab21a6 Fix delete fee logic
A fee can be associated to both the incoming and outgoing exchange, the
previous logic did not account for that, resulting in the fee not being
correctly removed.
Now the delete logic also check for the metadata enterprise role to see
if any additional fee need to be removed.
2025-04-01 13:46:34 +11:00

177 lines
5.7 KiB
Ruby

# frozen_string_literal: true
require 'spree/localized_number'
# Adjustments represent a change to the +item_total+ of an Order. Each adjustment
# has an +amount+ that can be either positive or negative.
#
# Adjustments can be open/closed/finalized
#
# Once an adjustment is finalized, it cannot be changed, but an adjustment can
# toggle between open/closed as needed
#
# Boolean attributes:
#
# +mandatory+
#
# If this flag is set to true then it means the the charge is required and will not
# be removed from the order, even if the amount is zero. In other words a record
# will be created even if the amount is zero. This is useful for representing things
# such as shipping and tax charges where you may want to make it explicitly clear
# that no charge was made for such things.
#
# +eligible?+
#
# This boolean attributes stores whether this adjustment is currently eligible
# for its order. Only eligible adjustments count towards the order's adjustment
# total. This allows an adjustment to be preserved if it becomes ineligible so
# it might be reinstated.
module Spree
class Adjustment < ApplicationRecord
extend Spree::LocalizedNumber
self.belongs_to_required_by_default = false
# Deletion of metadata is handled in the database.
# So we don't need the option `dependent: :destroy` as long as
# AdjustmentMetadata has no destroy logic itself.
has_one :metadata, class_name: 'AdjustmentMetadata', dependent: nil
has_many :adjustments, as: :adjustable, dependent: :destroy
belongs_to :adjustable, polymorphic: true
belongs_to :originator, -> { with_deleted }, polymorphic: true
belongs_to :order, class_name: "Spree::Order"
belongs_to :tax_category, class_name: 'Spree::TaxCategory'
validates :label, presence: true
validates :amount, numericality: true
after_create :update_adjustable_adjustment_total
state_machine :state, initial: :open do
event :close do
transition from: :open, to: :closed
end
event :open do
transition from: :closed, to: :open
end
event :finalize do
transition from: [:open, :closed], to: :finalized
end
end
scope :tax, -> { where(originator_type: 'Spree::TaxRate') }
scope :price, -> { where(adjustable_type: 'Spree::LineItem') }
scope :optional, -> { where(mandatory: false) }
scope :charge, -> { where('amount >= 0') }
scope :credit, -> { where('amount < 0') }
scope :return_authorization, -> { where(originator_type: "Spree::ReturnAuthorization") }
scope :voucher, -> { where(originator_type: "Voucher") }
scope :voucher_tax, -> {
voucher.joins(:metadata).where(metadata: { fee_name: "Tax", fee_type: "Voucher" })
}
scope :non_voucher, -> { where.not(originator_type: "Voucher") }
scope :inclusive, -> { where(included: true) }
scope :additional, -> { where(included: false) }
scope :legacy_tax, -> { additional.tax.where(adjustable_type: "Spree::Order") }
scope :enterprise_fee, -> { where(originator_type: 'EnterpriseFee') }
scope :admin, -> { where(originator_type: nil) }
scope :payment_fee, -> { where(AdjustmentScopes::PAYMENT_FEE_SCOPE) }
scope :shipping, -> { where(AdjustmentScopes::SHIPPING_SCOPE) }
scope :eligible, -> { where(AdjustmentScopes::ELIGIBLE_SCOPE) }
localize_number :amount
def self.by_originator_and_enterprise_role(originator, enterprise_role)
joins(:metadata)
.find_by(
originator:,
adjustment_metadata: { enterprise_role: }
)
end
def self.by_originator_and_not_enterprise_role(originator, enterprise_role)
joins(:metadata)
.where(originator:)
.where.not(
spree_adjustments: { adjustment_metadata: { enterprise_role: } }
).first
end
# Update both the eligibility and amount of the adjustment. Adjustments
# delegate updating of amount to their Originator when present, but only if
# +locked+ is false. Adjustments that are +locked+ will never change their amount.
#
# Adjustments delegate updating of amount to their Originator when present,
# but only if when they're in "open" state, closed or finalized adjustments
# are not recalculated.
#
# It receives +calculable+ as the updated source here so calculations can be
# performed on the current values of that source. If we used +source+ it
# could load the old record from db for the association. e.g. when updating
# more than on line items at once via accepted_nested_attributes the order
# object on the association would be in a old state and therefore the
# adjustment calculations would not performed on proper values
def update_adjustment!(calculable = nil, force: false)
return amount if immutable? && !force
if calculable.nil? && adjustable.nil?
delete
return 0.0
end
if originator.present?
amount = originator.compute_amount(calculable || adjustable)
update_columns(
amount:,
updated_at: Time.zone.now,
)
end
amount
end
def currency
adjustable ? adjustable.currency : CurrentConfig.get(:currency)
end
def display_amount
Spree::Money.new(amount, currency:)
end
def admin?
originator_type.nil?
end
def immutable?
state != "open"
end
def has_tax?
tax_total.positive?
end
def included_tax_total
adjustments.tax.inclusive.sum(:amount)
end
def additional_tax_total
adjustments.tax.additional.sum(:amount)
end
private
def tax_total
adjustments.tax.sum(:amount)
end
def update_adjustable_adjustment_total
Spree::ItemAdjustments.new(adjustable).update if adjustable
end
end
end