diff --git a/core/app/models/spree/core/state_machines/order.rb b/core/app/models/spree/core/state_machines/order.rb index 691b20e212..49db2035dd 100644 --- a/core/app/models/spree/core/state_machines/order.rb +++ b/core/app/models/spree/core/state_machines/order.rb @@ -8,214 +8,6 @@ def self.included(klass) klass.extend ClassMethods end - module ClassMethods - attr_accessor :previous_states - attr_writer :next_event_transitions, :checkout_steps, :removed_transitions - - def checkout_flow(&block) - if block_given? - @checkout_flow = block - define_state_machine! - else - @checkout_flow - end - end - - def define_state_machine! - self.checkout_steps = {} - self.next_event_transitions = [] - self.previous_states = [:cart] - self.removed_transitions = [] - - # 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 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, use_transactions: false do - klass.next_event_transitions.each { |state| transition(state.merge(on: :next)) } - - # Persist the state on the order - after_transition do |order, transition| - # Hard to say if this is really necessary, it was introduced in this commit: - # https://github.com/mamhoff/solidus/commit/fa1d66c42e4c04ee7cd1c20d87e4cdb74a226d3d - # But it seems to be harmless, so we'll keep it for now. - order.state = order.state # rubocop:disable Lint/SelfAssignment - - order.state_changes.create( - previous_state: transition.from, - next_state: transition.to, - name: 'order', - user_id: order.user_id - ) - order.save - end - - event :cancel do - transition to: :canceled, if: :allow_cancel?, from: :complete - end - - event :return do - transition to: :returned, from: [:complete, :awaiting_return], if: :all_inventory_units_returned? - end - - event :resume do - transition to: :resumed, from: :canceled, if: :canceled? - end - - event :authorize_return do - transition to: :awaiting_return, from: :complete - end - - event :complete do - transition to: :complete, from: klass.checkout_steps.keys.last - end - - if states[:payment] - event :payment_failed do - transition to: :payment, from: :confirm - end - - after_transition to: :complete, do: :add_payment_sources_to_wallet - before_transition to: :payment, do: :add_default_payment_from_wallet - before_transition to: :payment, do: :ensure_billing_address - - before_transition to: :confirm, do: :add_store_credit_payments - - # see also process_payments_before_complete below which needs to - # be added in the correct sequence. - end - - before_transition from: :cart, do: :ensure_line_items_present - - if states[:address] - before_transition to: :address, do: :assign_default_user_addresses - before_transition from: :address, do: :persist_user_address! - end - - if states[:delivery] - before_transition to: :delivery, do: :ensure_shipping_address - before_transition to: :delivery, do: :create_proposed_shipments - before_transition to: :delivery, do: :ensure_available_shipping_rates - end - - before_transition to: :resumed, do: :ensure_line_item_variants_are_not_deleted - before_transition to: :resumed, do: :validate_line_item_availability - - # Sequence of before_transition to: :complete - # calls matter so that we do not process payments - # until validations have passed - before_transition to: :complete, do: :validate_line_item_availability - before_transition to: :complete, do: :ensure_promotions_eligible - before_transition to: :complete, do: :ensure_line_item_variants_are_not_deleted - before_transition to: :complete, do: :ensure_inventory_units - if states[:payment] - before_transition to: :complete, do: :process_payments_before_complete - end - - after_transition to: :complete, do: :finalize - after_transition to: :resumed, do: :after_resume - after_transition to: :canceled, do: :after_cancel - - after_transition from: any - :cart, to: any - [:confirm, :complete] do |order| - order.recalculate - end - - after_transition do |order, transition| - order.logger.debug "Order #{order.number} transitioned from #{transition.from} to #{transition.to} via #{transition.event}" - end - - after_failure do |order, transition| - order.logger.debug "Order #{order.number} halted transition on event #{transition.event} state #{transition.from}: #{order.errors.full_messages.join}" - end - end - end - - def go_to_state(name, options = {}) - checkout_steps[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 insert_checkout_step(name, options = {}) - before = options.delete(:before) - after = options.delete(:after) unless before - after = checkout_steps.keys.last unless before || after - - cloned_steps = checkout_steps.clone - cloned_removed_transitions = removed_transitions.clone - checkout_flow do - cloned_steps.each_pair do |key, value| - go_to_state(name, options) if key == before - go_to_state(key, value) - go_to_state(name, options) if key == after - end - cloned_removed_transitions.each do |transition| - remove_transition(transition) - end - end - end - - def remove_checkout_step(name) - cloned_steps = checkout_steps.clone - cloned_removed_transitions = removed_transitions.clone - checkout_flow do - cloned_steps.each_pair do |key, value| - go_to_state(key, value) unless key == name - end - cloned_removed_transitions.each do |transition| - remove_transition(transition) - end - end - end - - def remove_transition(options = {}) - removed_transitions << options - next_event_transitions.delete(find_transition(options)) - end - - def find_transition(options = {}) - return nil if options.nil? || !options.include?(:from) || !options.include?(:to) - - next_event_transitions.detect do |transition| - transition[options[:from].to_sym] == options[:to].to_sym - end - end - - def next_event_transitions - @next_event_transitions ||= [] - end - - def checkout_steps - @checkout_steps ||= {} - end - - def checkout_step_names - checkout_steps.keys - end - - def add_transition(options) - next_event_transitions << { options.delete(:from) => options.delete(:to) }.merge(options) - end - - def removed_transitions - @removed_transitions ||= [] - end - end - def checkout_steps steps = self.class.checkout_steps.each_with_object([]) { |(step, options), checkout_steps| next if options.include?(:if) && !options[:if].call(self) diff --git a/core/app/models/spree/core/state_machines/order/class_methods.rb b/core/app/models/spree/core/state_machines/order/class_methods.rb new file mode 100644 index 0000000000..689e4f1865 --- /dev/null +++ b/core/app/models/spree/core/state_machines/order/class_methods.rb @@ -0,0 +1,217 @@ +# frozen_string_literal: true + +module Spree + module Core + class StateMachines + module Order + module ClassMethods + attr_accessor :previous_states + attr_writer :next_event_transitions, :checkout_steps, :removed_transitions + + def checkout_flow(&block) + if block_given? + @checkout_flow = block + define_state_machine! + else + @checkout_flow + end + end + + def define_state_machine! + self.checkout_steps = {} + self.next_event_transitions = [] + self.previous_states = [:cart] + self.removed_transitions = [] + + # 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 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, use_transactions: false do + klass.next_event_transitions.each { |state| transition(state.merge(on: :next)) } + + # Persist the state on the order + after_transition do |order, transition| + # Hard to say if this is really necessary, it was introduced in this commit: + # https://github.com/mamhoff/solidus/commit/fa1d66c42e4c04ee7cd1c20d87e4cdb74a226d3d + # But it seems to be harmless, so we'll keep it for now. + order.state = order.state # rubocop:disable Lint/SelfAssignment + + order.state_changes.create( + previous_state: transition.from, + next_state: transition.to, + name: 'order', + user_id: order.user_id + ) + order.save + end + + event :cancel do + transition to: :canceled, if: :allow_cancel?, from: :complete + end + + event :return do + transition to: :returned, from: [:complete, :awaiting_return], if: :all_inventory_units_returned? + end + + event :resume do + transition to: :resumed, from: :canceled, if: :canceled? + end + + event :authorize_return do + transition to: :awaiting_return, from: :complete + end + + event :complete do + transition to: :complete, from: klass.checkout_steps.keys.last + end + + if states[:payment] + event :payment_failed do + transition to: :payment, from: :confirm + end + + after_transition to: :complete, do: :add_payment_sources_to_wallet + before_transition to: :payment, do: :add_default_payment_from_wallet + before_transition to: :payment, do: :ensure_billing_address + + before_transition to: :confirm, do: :add_store_credit_payments + + # see also process_payments_before_complete below which needs to + # be added in the correct sequence. + end + + before_transition from: :cart, do: :ensure_line_items_present + + if states[:address] + before_transition to: :address, do: :assign_default_user_addresses + before_transition from: :address, do: :persist_user_address! + end + + if states[:delivery] + before_transition to: :delivery, do: :ensure_shipping_address + before_transition to: :delivery, do: :create_proposed_shipments + before_transition to: :delivery, do: :ensure_available_shipping_rates + end + + before_transition to: :resumed, do: :ensure_line_item_variants_are_not_deleted + before_transition to: :resumed, do: :validate_line_item_availability + + # Sequence of before_transition to: :complete + # calls matter so that we do not process payments + # until validations have passed + before_transition to: :complete, do: :validate_line_item_availability + before_transition to: :complete, do: :ensure_promotions_eligible + before_transition to: :complete, do: :ensure_line_item_variants_are_not_deleted + before_transition to: :complete, do: :ensure_inventory_units + if states[:payment] + before_transition to: :complete, do: :process_payments_before_complete + end + + after_transition to: :complete, do: :finalize + after_transition to: :resumed, do: :after_resume + after_transition to: :canceled, do: :after_cancel + + after_transition from: any - :cart, to: any - [:confirm, :complete] do |order| + order.recalculate + end + + after_transition do |order, transition| + order.logger.debug "Order #{order.number} transitioned from #{transition.from} to #{transition.to} via #{transition.event}" + end + + after_failure do |order, transition| + order.logger.debug "Order #{order.number} halted transition on event #{transition.event} state #{transition.from}: #{order.errors.full_messages.join}" + end + end + end + + def go_to_state(name, options = {}) + checkout_steps[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 insert_checkout_step(name, options = {}) + before = options.delete(:before) + after = options.delete(:after) unless before + after = checkout_steps.keys.last unless before || after + + cloned_steps = checkout_steps.clone + cloned_removed_transitions = removed_transitions.clone + checkout_flow do + cloned_steps.each_pair do |key, value| + go_to_state(name, options) if key == before + go_to_state(key, value) + go_to_state(name, options) if key == after + end + cloned_removed_transitions.each do |transition| + remove_transition(transition) + end + end + end + + def remove_checkout_step(name) + cloned_steps = checkout_steps.clone + cloned_removed_transitions = removed_transitions.clone + checkout_flow do + cloned_steps.each_pair do |key, value| + go_to_state(key, value) unless key == name + end + cloned_removed_transitions.each do |transition| + remove_transition(transition) + end + end + end + + def remove_transition(options = {}) + removed_transitions << options + next_event_transitions.delete(find_transition(options)) + end + + def find_transition(options = {}) + return nil if options.nil? || !options.include?(:from) || !options.include?(:to) + + next_event_transitions.detect do |transition| + transition[options[:from].to_sym] == options[:to].to_sym + end + end + + def next_event_transitions + @next_event_transitions ||= [] + end + + def checkout_steps + @checkout_steps ||= {} + end + + def checkout_step_names + checkout_steps.keys + end + + def add_transition(options) + next_event_transitions << { options.delete(:from) => options.delete(:to) }.merge(options) + end + + def removed_transitions + @removed_transitions ||= [] + end + end + end + end + end +end