# frozen_string_literal: true module Spree class OrderUpdater attr_reader :order delegate :payments, :line_items, :adjustments, :all_adjustments, :shipments, :quantity, to: :order def initialize(order) @order = order end # This is a multi-purpose method for processing logic related to changes in the Order. # It is meant to be called from various observers so that the Order is aware of changes # that affect totals and other values stored in the Order. # # This method should never do anything to the Order that results in a save call on the # object with callbacks (otherwise you will end up in an infinite recursion as the # associations try to save and then in turn try to call +update!+ again.) def recalculate order.transaction do update_item_count update_shipment_amounts update_totals if order.completed? update_payment_state update_shipments update_shipment_state end Spree::Bus.publish(:order_recalculated, order:) persist_totals end end alias_method :update, :recalculate deprecate update: :recalculate, deprecator: Spree.deprecator # Updates the +shipment_state+ attribute according to the following logic: # # shipped when all Shipments are in the "shipped" state # partial when at least one Shipment has a state of "shipped" and there is another Shipment with a state other than "shipped" # or there are InventoryUnits associated with the order that have a state of "sold" but are not associated with a Shipment. # ready when all Shipments are in the "ready" state # backorder when there is backordered inventory associated with an order # pending when all Shipments are in the "pending" state # # The +shipment_state+ value helps with reporting, etc. since it provides a quick and easy way to locate Orders needing attention. def update_shipment_state log_state_change('shipment') do order.shipment_state = determine_shipment_state end order.shipment_state end # Updates the +payment_state+ attribute according to the following logic: # # paid when +payment_total+ is equal to +total+ # balance_due when +payment_total+ is less than +total+ # credit_owed when +payment_total+ is greater than +total+ # failed when most recent payment is in the failed state # void when the order has been canceled and the payment total is 0 # # The +payment_state+ value helps with reporting, etc. since it provides a quick and easy way to locate Orders needing attention. def update_payment_state log_state_change('payment') do order.payment_state = determine_payment_state end order.payment_state end private def determine_payment_state if payments.present? && payments.valid.empty? && order.outstanding_balance != 0 'failed' elsif order.state == 'canceled' && order.payment_total.zero? 'void' elsif order.outstanding_balance > 0 'balance_due' elsif order.outstanding_balance < 0 'credit_owed' else # outstanding_balance == 0 'paid' end end def determine_shipment_state if order.backordered? 'backorder' else # get all the shipment states for this order shipment_states = shipments.states if shipment_states.size > 1 # multiple shiment states means it's most likely partially shipped 'partial' else # will return nil if no shipments are found shipment_states.first end end end # This will update and select the best promotion adjustment, update tax # adjustments, update cancellation adjustments, and then update the total # fields (promo_total, included_tax_total, additional_tax_total, and # adjustment_total) on the item. # @return [void] def recalculate_adjustments # Promotion adjustments must be applied first, then tax adjustments. # This fits the criteria for VAT tax as outlined here: # http://www.hmrc.gov.uk/vat/managing/charging/discounts-etc.htm#1 # It also fits the criteria for sales tax as outlined here: # http://www.boe.ca.gov/formspubs/pub113/ update_promotions update_taxes update_item_totals end # Updates the following Order total values: # # +payment_total+ The total value of all finalized Payments (NOTE: non-finalized Payments are excluded) # +item_total+ The total value of all LineItems # +adjustment_total+ The total value of all adjustments (promotions, credits, etc.) # +promo_total+ The total value of all promotion adjustments # +total+ The so-called "order total." This is equivalent to +item_total+ plus +adjustment_total+. def update_totals update_payment_total update_item_total update_shipment_total update_adjustment_total end def update_shipment_amounts shipments.each(&:update_amounts) end # give each of the shipments a chance to update themselves def update_shipments shipments.each(&:update_state) end def update_payment_total order.payment_total = payments.completed.includes(:refunds).sum { |payment| payment.amount - payment.refunds.sum(:amount) } end def update_shipment_total order.shipment_total = shipments.to_a.sum(&:cost) update_order_total end def update_order_total order.total = order.item_total + order.shipment_total + order.adjustment_total end def update_adjustment_total recalculate_adjustments all_items = line_items + shipments order_tax_adjustments = adjustments.select(&:tax?) order.adjustment_total = all_items.sum(&:adjustment_total) + adjustments.sum(&:amount) order.included_tax_total = all_items.sum(&:included_tax_total) + order_tax_adjustments.select(&:included?).sum(&:amount) order.additional_tax_total = all_items.sum(&:additional_tax_total) + order_tax_adjustments.reject(&:included?).sum(&:amount) update_order_total end def update_item_count order.item_count = line_items.to_a.sum(&:quantity) end def update_item_total order.item_total = line_items.to_a.sum(&:amount) update_order_total end def persist_totals order.save! end def log_state_change(name) state = "#{name}_state" old_state = order.public_send(state) yield new_state = order.public_send(state) if old_state != new_state order.state_changes.new( previous_state: old_state, next_state: new_state, name:, user_id: order.user_id ) end end def update_promotions Spree::Config.promotions.order_adjuster_class.new(order).call end def update_taxes Spree::Config.tax_adjuster_class.new(order).adjust! [*line_items, *shipments].each do |item| tax_adjustments = item.adjustments.select(&:tax?) # Tax adjustments come in not one but *two* exciting flavours: # Included & additional # Included tax adjustments are those which are included in the price. # These ones should not affect the eventual total price. # # Additional tax adjustments are the opposite, affecting the final total. item.included_tax_total = tax_adjustments.select(&:included?).sum(&:amount) item.additional_tax_total = tax_adjustments.reject(&:included?).sum(&:amount) end end def update_cancellations end deprecate :update_cancellations, deprecator: Spree.deprecator def update_item_totals [*line_items, *shipments].each do |item| # The cancellation_total isn't persisted anywhere but is included in # the adjustment_total item.adjustment_total = item.adjustments. reject(&:included?). sum(&:amount) if item.changed? item.update_columns( promo_total: item.promo_total, included_tax_total: item.included_tax_total, additional_tax_total: item.additional_tax_total, adjustment_total: item.adjustment_total, updated_at: Time.current, ) end end end end end