# 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 ->(order) { order.save } 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