# 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