# frozen_string_literal: true module Spree class Promotion < Spree::Base # This class encapsulates all the things the promotion system does to # an order. It is called from the `Spree::OrderUpdater` before taxes are # calculated, such that taxes always respect promotions. # This class iterates over all existing promotion adjustments and recalculates # their amount and eligibility using their adjustment source. class OrderAdjustmentsRecalculator def initialize(order) @order = order end def call all_items = line_items + shipments all_items.each do |item| promotion_adjustments = item.adjustments.select(&:promotion?) promotion_adjustments.each { |adjustment| recalculate(adjustment) } Spree::Config.promotions.promotion_chooser_class.new(promotion_adjustments).update item.promo_total = promotion_adjustments.select(&:eligible?).sum(&:amount) end # Update and select the best promotion adjustment for the order. # We don't update the order.promo_total yet. Order totals are updated later # in #update_adjustment_total since they include the totals from the order's # line items and/or shipments. order_promotion_adjustments = order.adjustments.select(&:promotion?) order_promotion_adjustments.each { |adjustment| recalculate(adjustment) } Spree::Config.promotions.promotion_chooser_class.new(order_promotion_adjustments).update order.promo_total = all_items.sum(&:promo_total) + order_promotion_adjustments. select(&:eligible?). select(&:promotion?). sum(&:amount) order end private attr_reader :order delegate :line_items, :shipments, to: :order # Recalculate and persist the amount from this adjustment's source based on # the adjustable ({Order}, {Shipment}, or {LineItem}) # # If the adjustment has no source (such as when created manually from the # admin) or is closed, this is a noop. # # @return [BigDecimal] New amount of this adjustment def recalculate(adjustment) if adjustment.finalized? return adjustment.amount end # If the adjustment has no source, do not attempt to re-calculate the # amount. # Some scenarios where this happens: # - Adjustments that are manually created via the admin backend # - PromotionAction adjustments where the PromotionAction was deleted # after the order was completed. if adjustment.source.present? adjustment.amount = adjustment.source.compute_amount(adjustment.adjustable) adjustment.eligible = calculate_eligibility(adjustment) # Persist only if changed # This is only not a save! to avoid the extra queries to load the order # (for validations) and to touch the adjustment. adjustment.update_columns(eligible: adjustment.eligible, amount: adjustment.amount, updated_at: Time.current) if adjustment.changed? end adjustment.amount end # Calculates based on attached promotion (if this is a promotion # adjustment) whether this promotion is still eligible. # @api private # @return [true,false] Whether this adjustment is eligible def calculate_eligibility(adjustment) if !adjustment.finalized? && adjustment.source adjustment.source.promotion.eligible?(adjustment.adjustable, promotion_code: adjustment.promotion_code) else adjustment.eligible? end end end end end