Skip to content

Instantly share code, notes, and snippets.

@radar
Created July 5, 2012 08:28
Show Gist options
  • Save radar/3052267 to your computer and use it in GitHub Desktop.
Save radar/3052267 to your computer and use it in GitHub Desktop.
Order.class_eval do
checkout_flow do
go_to_state :address
go_to_state :delivery
go_to_state :payment, :if => lambda { payment_required? }
go_to_state :confirm, :if => lambda { confirmation_required? }
go_to_state :complete
remove_transition :from => :delivery, :to => :confirm
end
end

This is an example implementation of an order class that can have custom states defined for its instances. This could allow a flexible workflow for Spree.

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.

Please review the code and attempt to run the specs and point out any issues you see.

require 'active_support/core_ext/class/attribute_accessors'
require 'state_machine'
module Checkout
def self.included(klass)
klass.class_eval do
attr_accessor :state
attr_accessor :transitions
attr_accessor :previous_states
cattr_accessor :checkout_flow
def self.checkout_flow(&block)
if block_given?
@checkout_flow = block
else
@checkout_flow
end
end
def initialize
super
machine
end
def transitions
@transitions ||= []
end
def add_transition(options)
self.transitions << { options.delete(:from) => options.delete(:to) }.merge(options)
end
# TODO: Delegate
def next!
machine.next!
end
def machine
@machine ||= begin
order = self
checkout_flow = order.class.checkout_flow
self.state = :cart
self.previous_states = [:cart]
order.instance_eval(&checkout_flow)
StateMachine.new(order, :initial => :cart) do
order.transitions.each { |attrs| transition(attrs) }
# Persist the state on the order
after_transition do
order.state = order.machine.state
order.save
end
end
end
end
def go_to_state(name, options={})
if options[:if]
previous_states.each do |state|
add_transition({:from => state, :to => name, :on => :next}.merge(options))
end
self.previous_states << name
else
previous_states.each do |state|
add_transition({:from => state, :to => name, :on => :next}.merge(options))
end
self.previous_states = [name]
end
end
def remove(options={})
if transition = find_transition(options)
self.transitions.delete(transition)
end
end
def find_transition(options={})
self.transitions.detect do |transition|
transition[options[:from].to_sym] == options[:to].to_sym
end
end
end
end
class StateMachine
def self.new(object, *args, &block)
machine = Class.new do
def definition
self.class.state_machine
end
end
machine.state_machine(*args, &block)
machine.new
end
end
end
class Order
include Checkout
def payment_required?
false
end
def confirmation_required?
false
end
def save
#noop
end
end
describe Order do
let(:order) { Order.new }
context "with default state machine" do
before do
Order.class_eval do
checkout_flow do
go_to_state :address
go_to_state :delivery
go_to_state :payment, :if => lambda { payment_required? }
go_to_state :confirm, :if => lambda { confirmation_required? }
go_to_state :complete
remove :from => :delivery, :to => :confirm
end
end
end
it "has the following transitions" do
transitions = [
{ :address => :delivery },
{ :delivery => :payment },
{ :payment => :confirm },
{ :confirm => :complete },
{ :payment => :complete },
{ :delivery => :complete }
]
transitions.each do |transition|
transition = order.find_transition(:from => transition.keys.first, :to => transition.values.first)
transition.should_not be_nil
end
end
it "does not have a transition from delivery to confirm" do
transition = order.find_transition(:from => :delivery, :to => :confirm)
transition.should be_nil
end
it "starts out at cart" do
order.state.should == :cart
end
it "transitions to address" do
order.next!
order.state.should == "address"
end
context "from address" do
before do
order.machine.state = 'address'
end
it "transitions to delivery" do
order.next!
order.state.should == "delivery"
end
end
context "from delivery" do
before do
order.machine.state = 'delivery'
end
context "with payment required" do
before do
order.stub :payment_required? => true
end
it "transitions to payment" do
order.next!
order.state.should == 'payment'
end
end
context "without payment required" do
before do
order.stub :payment_required? => false
end
it "transitions to complete" do
order.next!
order.state.should == "complete"
end
end
end
context "from payment" do
before do
order.machine.state = 'payment'
end
context "with confirmation required" do
before do
order.stub :confirmation_required? => true
end
it "transitions to confirm" do
order.next!
order.state.should == "confirm"
end
end
context "without confirmation required" do
before do
order.stub :confirmation_required? => false
end
it "transitions to complete" do
order.next!
order.state.should == "complete"
end
end
end
end
end
@BDQ
Copy link

BDQ commented Jul 9, 2012

Ryan - I'm happy with this now, great stuff.

@radar
Copy link
Author

radar commented Jul 9, 2012 via email

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