engines/order_management/app/services/order_management/order/updater.rb
# frozen_string_literal: true
module OrderManagement
module Order
class Updater
attr_reader :order
delegate :payments, :line_items, :adjustments, :all_adjustments, :shipments, 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 update
update_all_adjustments
update_totals_and_states
end
def update_totals_and_states
handle_legacy_taxes
update_totals
if order.completed?
update_payment_state
update_shipments
update_shipment_state
end
persist_totals
update_pending_payment
end
# Updates the following Order total values:
#
# - payment_total - total value of all finalized Payments (excludes non-finalized Payments)
# - item_total - total value of all LineItems
# - adjustment_total - total value of all adjustments
# - total - order total, it's the equivalent to item_total plus adjustment_total
def update_totals
update_payment_total
update_item_total
update_adjustment_total
update_order_total
end
# Give each of the shipments a chance to update themselves
def update_shipments
shipments.each { |shipment| shipment.update!(order) }
end
def update_payment_total
order.payment_total = payments.completed.sum(:amount)
end
def update_item_total
order.item_total = line_items.sum('price * quantity')
update_order_total
end
def update_adjustment_total
order.adjustment_total = all_adjustments.additional.eligible.sum(:amount)
order.additional_tax_total = all_adjustments.tax.additional.sum(:amount)
order.included_tax_total = all_adjustments.tax.inclusive.sum(:amount)
end
def update_order_total
order.total = order.item_total + order.adjustment_total
end
def persist_totals
order.update_columns(
payment_state: order.payment_state,
shipment_state: order.shipment_state,
item_total: order.item_total,
adjustment_total: order.adjustment_total,
included_tax_total: order.included_tax_total,
additional_tax_total: order.additional_tax_total,
payment_total: order.payment_total,
total: order.total,
updated_at: Time.zone.now
)
end
# Updates the +shipment_state+ attribute according to the following logic:
#
# - shipped - when the order shipment is in the "shipped" state
# - ready - when the order shipment is in the "ready" state
# - backorder - when there is backordered inventory associated with an order
# - pending - when the shipment is 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
order.shipment_state = if order.shipment&.backordered?
'backorder'
else
# It returns nil if there is no shipment
order.shipment&.state
end
order.state_changed('shipment')
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
#
# 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
last_payment_state = order.payment_state
order.payment_state = infer_payment_state
cancel_payments_requiring_auth unless last_payment_state == "paid"
track_payment_state_change(last_payment_state)
order.payment_state
end
def update_all_adjustments
# Voucher are modelled as a Spree::Adjustment but they don't behave like all the other
# adjustments, so we don't want voucher adjustment to be updated here.
# Calculation are handled by VoucherAdjustmentsService.calculate
order.all_adjustments.non_voucher.reload.each(&:update_adjustment!)
end
# Sets the distributor's address as shipping address of the order for those
# shipments using a shipping method that doesn't require address, such us
# a pickup.
def shipping_address_from_distributor
return if order.shipping_method.blank? || order.shipping_method.require_ship_address
order.ship_address = order.address_from_distributor
end
def after_payment_update(payment)
if payment.completed? || payment.void?
update_payment_total
end
if order.completed?
update_payment_state
update_shipments
update_shipment_state
end
return unless payment.completed? || order.completed?
persist_totals
end
private
def cancel_payments_requiring_auth
return unless order.payment_state == "paid"
payments.to_a.select(&:requires_authorization?).each(&:void_transaction!)
end
def round_money(value)
(value * 100).round / 100.0
end
def infer_payment_state
if failed_payments?
'failed'
elsif canceled_and_not_paid_for?
'void'
elsif requires_authorization?
'requires_authorization'
else
infer_payment_state_from_balance
end
end
def infer_payment_state_from_balance
# This part added so that we don't need to override
# order.outstanding_balance
balance = order.new_outstanding_balance
infer_state(balance)
end
def infer_state(balance)
if balance.positive?
'balance_due'
elsif balance.negative?
'credit_owed'
elsif balance.zero?
'paid'
end
end
# Tracks the state transition through a state_change for this order. It
# does so until the last state is reached. That is, when the infered next
# state is the same as the order has now.
#
# @param last_payment_state [String]
def track_payment_state_change(last_payment_state)
return if last_payment_state == order.payment_state
order.state_changed('payment')
end
def canceled_and_not_paid_for?
order.state == 'canceled' && order.payment_total.zero?
end
def failed_payments?
payments.present? && payments.valid.empty?
end
# Re-applies tax if any legacy taxes are present
def handle_legacy_taxes
return unless order.completed? && order.adjustments.legacy_tax.any?
order.create_tax_charge!
end
def requires_authorization?
payments.requires_authorization.any? && payments.completed.empty?
end
def update_pending_payment
# We only want to update complete order pending payment when it's a cash payment. We assume
# that if the payment was a credit card it would alread have been processed, so we don't
# bother checking the payment type
return unless order.state.in? ["payment", "confirmation", "complete"]
return unless order.pending_payments.any?
order.pending_payments.first.update_attribute :amount, order.total
end
end
end
end