app/models/spree/order/checkout.rb
# frozen_string_literal: true
module Spree
class Order < ApplicationRecord
module Checkout
def self.included(klass)
klass.class_eval do
class_attribute :next_event_transitions
class_attribute :previous_states
class_attribute :checkout_flow
def self.checkout_flow(&block)
if block_given?
@checkout_flow = block
define_state_machine!
else
@checkout_flow
end
end
def self.define_state_machine!
self.next_event_transitions = []
self.previous_states = [:cart]
# Build the checkout flow using the checkout_flow defined either
# within the Order class, or a decorator for that class.
#
# This method may be called multiple times depending on if the
# checkout_flow is re-defined in a decorator or not.
instance_eval(&checkout_flow)
klass = self
# To avoid a ton of warnings when the state machine is re-defined
StateMachines::Machine.ignore_method_conflicts = true
# To avoid multiple occurrences of the same transition being defined
# On first definition, state_machines will not be defined
state_machines.clear if respond_to?(:state_machines)
state_machine :state, initial: :cart do
klass.next_event_transitions.each { |t| transition(t.merge(on: :next)) }
# Persist the state on the order
after_transition do |order|
order.state = order.state
order.save
end
event :cancel do
transition to: :canceled, if: :allow_cancel?
end
event :return do
transition to: :returned, from: :awaiting_return, unless: :awaiting_returns?
end
event :resume do
transition to: :resumed, from: :canceled, if: :allow_resume?
end
event :authorize_return do
transition to: :awaiting_return
end
event :restart_checkout do
transition to: :cart, unless: :completed?
end
event :confirm do
transition to: :complete, from: :confirmation
end
event :back_to_payment do
transition to: :payment, from: :confirmation
end
event :back_to_address do
transition to: :address, from: [:payment, :confirmation]
end
before_transition from: :cart, do: :ensure_line_items_present
before_transition to: :delivery, do: :create_proposed_shipments
before_transition to: :delivery, do: :ensure_available_shipping_rates
before_transition to: :confirmation, do: :validate_payment_method!
after_transition to: :payment do |order|
order.create_tax_charge!
order.update_totals_and_states
end
after_transition to: :complete, do: :finalize!
after_transition to: :resumed, do: :after_resume
after_transition to: :canceled, do: :after_cancel
end
end
def self.go_to_state(name, options = {})
previous_states.each do |state|
add_transition({ from: state, to: name }.merge(options))
end
if options[:if]
previous_states << name
else
self.previous_states = [name]
end
end
def self.next_event_transitions
@next_event_transitions ||= []
end
def self.add_transition(options)
next_event_transitions << { options.delete(:from) => options.delete(:to) }.
merge(options)
end
def restart_checkout_flow
update_columns(
state: "address",
updated_at: Time.zone.now,
)
end
def state_changed(name)
state = "#{name}_state"
return unless persisted?
old_state = __send__("#{state}_was")
state_changes.create(
previous_state: old_state,
next_state: __send__(state),
name:,
user_id:
)
end
private
def after_cancel
shipments.reject(&:canceled?).each(&:cancel!)
payments.checkout.each(&:void!)
OrderMailer.cancel_email(id).deliver_later if send_cancellation_email
update(payment_state: updater.update_payment_state)
end
def after_resume
shipments.each(&:resume!)
payments.void.each(&:resume!)
update(payment_state: updater.update_payment_state)
end
def validate_payment_method!
return unless checkout_processing
return if payments.any?
errors.add :payment_method, I18n.t('checkout.errors.select_a_payment_method')
throw :halt
end
end
end
end
end
end