Dynamic state machine proposal #1743

Closed
wants to merge 12 commits into from

4 participants

@radar
Spree Commerce member

This issue should address #1418.

See the code in this gist to see what it looks like. As I explain there:

The code works by calling the checkout_flow method which clears all current transitions. The go_to_state method then defines transitions for each step of the process. The first call will transition from cart, and subsequent calls will transition from the state before it.

If a state has a condition on it, then a record of all previous states are kept until there is a state that has no condition. Once this happens, the code will define state transitions for all state paths for the states in between. For instance, the follow transitions will be defined between delivery and complete:

  • Delivery to payment
  • Delivery to confirm
  • Delivery to complete
  • Payment to confirm
  • Payment to complete
  • Confirm to complete

The remove method will remove a transition that has previously been either automatically or manually defined. In the case of this state machine, the delivery to confirm transition has been automatically defined, but we don't want it. So we get rid of it. As in the above example, the "delivery to confirm" transition will be removed.

What this will allow for is for dynamic state machine definitions for Order objects by re-defining the checkout_flow block on them and using the new state definition API.

This issue is just opening that implementation up for consideration. Does it look sensible? Does it fit the use cases? That kind of thing.

Thoughts?

@radar radar closed this in fb59527 Jul 10, 2012
@radar radar reopened this Jul 10, 2012
@radar radar commented on the diff Jul 10, 2012
core/app/models/spree/order.rb
@@ -450,6 +413,14 @@ def payment_method
end
end
+ def process_payments!
@radar
Spree Commerce member
radar added a note Jul 10, 2012

This just moves the begin/rescue logic out of the state machine and into this method, which I think is neater.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@radar radar commented on an outdated diff Jul 10, 2012
core/app/models/spree/order.rb
@@ -576,13 +547,13 @@ def require_email
end
def has_available_shipment
- return unless :address == state_name.to_sym
+ return unless address?
@radar
Spree Commerce member
radar added a note Jul 10, 2012

mmm, query methods

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@radar radar commented on the diff Jul 10, 2012
core/app/models/spree/order/checkout.rb
+ module Checkout
+ def self.included(klass)
+ klass.class_eval do
+ cattr_accessor :next_event_transitions
+ cattr_accessor :previous_states
+ cattr_accessor :checkout_flow
+
+ def self.checkout_flow(&block)
+ if block_given?
+ @checkout_flow = block
+ else
+ @checkout_flow
+ end
+ end
+
+ def self.define_state_machine!
@radar
Spree Commerce member
radar added a note Jul 10, 2012

This method is the magic. This is called when Core::Engine does a to_prepare. This means that you can re-define the next state transitions as many times as you wish and every time to_prepare is called, the state machine will re-define the transitions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@radar radar commented on an outdated diff Jul 10, 2012
core/app/models/spree/order/checkout.rb
+ else
+ @checkout_flow
+ end
+ end
+
+ def self.define_state_machine!
+ @machine ||= begin
+ self.previous_states = [:cart]
+ instance_eval(&checkout_flow)
+ klass = self
+
+ 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|
@radar
Spree Commerce member
radar added a note Jul 10, 2012

I am not sure if this is actually required any more.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@radar radar commented on an outdated diff Jul 10, 2012
core/app/models/spree/order/checkout.rb
+ end
+
+ before_transition :to => :delivery, :do => :remove_invalid_shipments!
+
+ after_transition :to => :complete, :do => :finalize!
+ after_transition :to => :delivery, :do => :create_tax_charge!
+ after_transition :to => :payment, :do => :create_shipment!
+ after_transition :to => :resumed, :do => :after_resume
+ after_transition :to => :canceled, :do => :after_cancel
+ end
+ end
+ end
+
+ def self.go_to_state(name, options={})
+ if options[:if]
+ previous_states.each do |state|
@radar
Spree Commerce member
radar added a note Jul 10, 2012

I thought about moving this out into its own method but decided against it because I am lazy.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@radar radar commented on the diff Jul 10, 2012
core/app/models/spree/order/checkout.rb
+
+ def self.go_to_state(name, options={})
+ if options[:if]
+ previous_states.each do |state|
+ add_transition({:from => state, :to => name}.merge(options))
+ end
+ self.previous_states << name
+ else
+ previous_states.each do |state|
+ add_transition({:from => state, :to => name}.merge(options))
+ end
+ self.previous_states = [name]
+ end
+ end
+
+ def self.remove_transition(options={})
@radar
Spree Commerce member
radar added a note Jul 10, 2012

This was previously called remove, but I've renamed it to remove_transition so that there isn't a "mysterious" remove method on the Order class, and so that it fits more into the same theme as find_transition and add_transition.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@radar radar commented on the diff Jul 10, 2012
core/app/models/spree/order/checkout.rb
+ end
+ end
+
+ def self.remove_transition(options={})
+ if transition = find_transition(options)
+ self.next_event_transitions.delete(transition)
+ end
+ end
+
+ def self.find_transition(options={})
+ self.next_event_transitions.detect do |transition|
+ transition[options[:from].to_sym] == options[:to].to_sym
+ end
+ end
+
+ def self.next_event_transitions
@radar
Spree Commerce member
radar added a note Jul 10, 2012

This used to be called transitions. Same reasons for renaming the remove method.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@radar
Spree Commerce member

Just a note: this doesn't actually change the checkout breadcrumbs yet. I've not yet begun work on that, but will probably knock it off tomorrow.

radar added some commits Jul 10, 2012
@radar radar Implemented checkout_steps method
This replaces the code duplication in checkout_helper to determine what steps to show for an order during the checkout process
256d28f
@radar radar checkout_steps should always return a string b5b4e54
@radar
Spree Commerce member

Checkout breadcrumbs are in @256d28f. The build is passing on Travis.

@radar
Spree Commerce member

Kicking the tyres some more on this today.

@radar
Spree Commerce member

I can't think of anything more to do on this. Can @BDQ, @cmar, @LBRapid, @joneslee85 and/or @schof look this over and see if they can break it?

@joneslee85
Spree Commerce member

@radar Will spend sometime on this this weekend, see if I could break it

@schof
Spree Commerce member

I'm ok to merge this as long as we update the documentation and release notes adequately (plus resolve conflicts of course.)

@radar radar added a commit that referenced this pull request Aug 8, 2012
@radar radar Introduced customizable checkout flow
Merges #1743
fa1d66c
@radar
Spree Commerce member

Merged to master with @fa1d66c. Tests are running on master now. When they're good, I'll update the Checkout Customization guide appropriately.

@radar radar closed this Aug 8, 2012
@gsnarawat

I want to remove the Shipping page validations.. I used this code but still there is validations exist. plz help.

checkout_flow do
go_to_state :address, :if => lambda { |order| order.payment_required? }
go_to_state :payment, :if => lambda { |order| order.payment_required? }
go_to_state :confirm, :if => lambda { |order| order.confirmation_required? }
go_to_state :complete
remove_transition :from => :delivery, :to => :confirm
end

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment