# 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 "opened" or "closed". # Once an adjustment is closed, it will not be automatically updated. # # 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 < Spree::Base with_options polymorphic: true do belongs_to :adjustable, touch: true belongs_to :source end belongs_to :order, class_name: 'Spree::Order', inverse_of: :all_adjustments validates :adjustable, :order, :label, presence: true validates :amount, numericality: true state_machine :state, initial: :open do event :close do transition from: :open, to: :closed end event :open do transition from: :closed, to: :open end end after_create :update_adjustable_adjustment_total after_destroy :update_adjustable_adjustment_total class_attribute :competing_promos_source_types self.competing_promos_source_types = ['Spree::PromotionAction'] scope :open, -> { ActiveSupport::Deprecation.warn 'Adjustment.open is deprecated. Please use Adjustment.not_finalized instead', caller not_finalized } scope :closed, -> { ActiveSupport::Deprecation.warn 'Adjustment.closed is deprecated. Please use Adjustment.finalized instead', caller finalized } scope :not_finalized, -> { where(state: 'open') } scope :finalized, -> { where(state: 'closed') } scope :tax, -> { where(source_type: 'Spree::TaxRate') } scope :non_tax, -> do source_type = arel_table[:source_type] where(source_type.not_eq('Spree::TaxRate').or(source_type.eq(nil))) end scope :price, -> { where(adjustable_type: 'Spree::LineItem') } scope :shipping, -> { where(adjustable_type: 'Spree::Shipment') } scope :optional, -> { where(mandatory: false) } scope :eligible, -> { where(eligible: true) } scope :charge, -> { where("#{quoted_table_name}.amount >= 0") } scope :credit, -> { where("#{quoted_table_name}.amount < 0") } scope :nonzero, -> { where("#{quoted_table_name}.amount != 0") } scope :promotion, -> { where(source_type: 'Spree::PromotionAction') } scope :return_authorization, -> { where(source_type: 'Spree::ReturnAuthorization') } scope :is_included, -> { where(included: true) } scope :additional, -> { where(included: false) } scope :competing_promos, -> { where(source_type: competing_promos_source_types) } scope :for_complete_order, -> { joins(:order).merge(Spree::Order.complete) } scope :for_incomplete_order, -> { joins(:order).merge(Spree::Order.incomplete) } extend DisplayMoney money_methods :amount def amount=(amount) self[:amount] = Spree::LocalizedNumber.parse(amount) end def currency adjustable ? adjustable.currency : order.currency end def promotion? source_type == 'Spree::PromotionAction' end # Passing a target here would always be recommended as it would avoid # hitting the database again and would ensure you're compute values over # the specific object amount passed here. def update!(target = adjustable) return amount if closed? || source.blank? amount = source.compute_amount(target) attributes = { amount: amount, updated_at: Time.current } attributes[:eligible] = source.promotion.eligible?(target) if promotion? update_columns(attributes) amount end private def update_adjustable_adjustment_total # Cause adjustable's total to be recalculated Adjustable::AdjustmentsUpdater.update(adjustable) end end end