Skip to content

Commit

Permalink
Introduced customizable checkout flow
Browse files Browse the repository at this point in the history
Merges #1743
  • Loading branch information
radar committed Aug 7, 2012
1 parent 61b6173 commit fa1d66c
Show file tree
Hide file tree
Showing 9 changed files with 319 additions and 69 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Expand Up @@ -24,7 +24,7 @@ notifications:
branches:
only:
- 1-0-stable
- auth-take-two
- 1-1-stable
- master
rvm:
- 1.8.7
Expand Down
10 changes: 10 additions & 0 deletions core/app/controllers/spree/checkout_controller.rb
Expand Up @@ -6,6 +6,7 @@ class CheckoutController < BaseController
ssl_required

before_filter :load_order
before_filter :ensure_valid_state
before_filter :associate_user
rescue_from Spree::Core::GatewayError, :with => :rescue_from_spree_gateway_error

Expand Down Expand Up @@ -37,6 +38,15 @@ def update
end

private
def ensure_valid_state
if params[:state] && params[:state] != 'cart' &&
!@order.checkout_steps.include?(params[:state])
params[:state] == 'cart'
@order.state = 'cart'
redirect_to checkout_path
end
end

def load_order
@order = current_order
redirect_to cart_path and return unless @order and @order.checkout_allowed?
Expand Down
6 changes: 1 addition & 5 deletions core/app/helpers/spree/checkout_helper.rb
@@ -1,11 +1,7 @@
module Spree
module CheckoutHelper
def checkout_states
if @order.payment and @order.payment.payment_method.payment_profiles_supported?
%w(address delivery payment confirm complete)
else
%w(address delivery payment complete)
end
@order.checkout_steps
end

def checkout_progress
Expand Down
85 changes: 30 additions & 55 deletions core/app/models/spree/order.rb
Expand Up @@ -2,6 +2,16 @@

module Spree
class Order < ActiveRecord::Base
include Checkout
checkout_flow do
go_to_state :address
go_to_state :delivery
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

token_resource

attr_accessible :line_items, :bill_address_attributes, :ship_address_attributes, :payments_attributes,
Expand Down Expand Up @@ -54,54 +64,6 @@ class Order < ActiveRecord::Base
class_attribute :update_hooks
self.update_hooks = Set.new

# order state machine (see http://github.com/pluginaweek/state_machine/tree/master for details)
state_machine :initial => 'cart', :use_transactions => false do

event :next do
transition :from => 'cart', :to => 'address'
transition :from => 'address', :to => 'delivery'
transition :from => 'delivery', :to => 'payment', :if => :payment_required?
transition :from => 'delivery', :to => 'complete'
transition :from => 'confirm', :to => 'complete'

# note: some payment methods will not support a confirm step
transition :from => 'payment', :to => 'confirm',
:if => Proc.new { |order| order.payment_method && order.payment_method.payment_profiles_supported? }

transition :from => 'payment', :to => 'complete'
end

event :cancel do
transition :to => 'canceled', :if => :allow_cancel?
end
event :return do
transition :to => 'returned', :from => 'awaiting_return'
end
event :resume do
transition :to => 'resumed', :from => 'canceled', :if => :allow_resume?
end
event :authorize_return do
transition :to => 'awaiting_return'
end

before_transition :to => 'complete' do |order|
begin
order.process_payments!
rescue Core::GatewayError
!!Spree::Config[:allow_checkout_on_gateway_error]
end
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

def self.by_number(number)
where(:number => number)
end
Expand Down Expand Up @@ -156,6 +118,11 @@ def payment_required?
total.to_f > 0.0
end

# If true, causes the confirmation step to happen during the checkout process
def confirmation_required?
payment_method && payment_method.payment_profiles_supported?
end

# Indicates the number of items in the order
def item_count
line_items.sum(:quantity)
Expand Down Expand Up @@ -344,7 +311,6 @@ def create_shipment!
:address => self.ship_address,
:inventory_units => self.inventory_units}, :without_protection => true)
end

end

def outstanding_balance
Expand All @@ -366,10 +332,6 @@ def credit_cards
CreditCard.scoped(:conditions => { :id => credit_card_ids })
end

def process_payments!
ret = payments.each(&:process!)
end

# Finalizes an in progress order after checkout is complete.
# Called after transition to complete state when payments will have been processed
def finalize!
Expand Down Expand Up @@ -433,6 +395,14 @@ def payment_method
end
end

def process_payments!
begin
payments.each(&:process!)
rescue Core::GatewayError
!!Spree::Config[:allow_checkout_on_gateway_error]
end
end

def billing_firstname
bill_address.try(:firstname)
end
Expand Down Expand Up @@ -468,6 +438,10 @@ def clear_adjustments!
price_adjustments.each(&:destroy)
end

def has_step?(step)
checkout_steps.include?(step)
end

private
def link_by_email
self.email = user.email if self.user
Expand Down Expand Up @@ -571,13 +545,14 @@ def require_email
end

def has_available_shipment
return unless :address == state_name.to_sym
return unless has_step?("delivery")
return unless address?
return unless ship_address && ship_address.valid?
errors.add(:base, :no_shipping_methods_available) if available_shipping_methods.empty?
end

def has_available_payment
return unless :delivery == state_name.to_sym
return unless delivery?
errors.add(:base, :no_payment_methods_available) if available_payment_methods.empty?
end

Expand Down
124 changes: 124 additions & 0 deletions core/app/models/spree/order/checkout.rb
@@ -0,0 +1,124 @@
module Spree
class Order < ActiveRecord::Base
module Checkout
def self.included(klass)
klass.class_eval do
cattr_accessor :next_event_transitions
cattr_accessor :previous_states
cattr_accessor :checkout_flow
cattr_accessor :checkout_steps

def self.checkout_flow(&block)
if block_given?
@checkout_flow = block
else
@checkout_flow
end
end

def self.define_state_machine!
self.checkout_steps = []
self.next_event_transitions = []
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|
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
end

event :resume do
transition :to => :resumed, :from => :canceled, :if => :allow_resume?
end

event :authorize_return do
transition :to => :awaiting_return
end

before_transition :to => :complete do |order|
begin
order.process_payments!
rescue Spree::Core::GatewayError
!!Spree::Config[:allow_checkout_on_gateway_error]
end
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 => :resumed, :do => :after_resume
after_transition :to => :canceled, :do => :after_cancel

after_transition :from => :delivery, :do => :create_shipment!
end
end

def self.go_to_state(name, options={})
self.checkout_steps[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={})
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
@next_event_transitions ||= []
end

def self.checkout_steps
@checkout_steps ||= ActiveSupport::OrderedHash.new
end

def self.add_transition(options)
self.next_event_transitions << { options.delete(:from) => options.delete(:to) }.merge(options)
end

def checkout_steps
checkout_steps = []
# TODO: replace this with each_with_object once Ruby 1.9 is standard
self.class.checkout_steps.each do |step, options|
if options[:if]
next unless options[:if].call(self)
end
checkout_steps << step
end
checkout_steps.map(&:to_s)
end
end
end
end
end
end
14 changes: 8 additions & 6 deletions core/app/views/spree/shared/_order_details.html.erb
Expand Up @@ -14,12 +14,14 @@
</div>
</div>

<div class="columns alpha four">
<h6><%= t(:shipping_method) %> <%= link_to "(#{t(:edit)})", checkout_state_path(:delivery) unless @order.completed? %></h6>
<div class="delivery">
<%= order.shipping_method.name %>
<% if @order.has_step?("delivery") %>
<div class="columns alpha four">
<h6><%= t(:shipping_method) %> <%= link_to "(#{t(:edit)})", checkout_state_path(:delivery) unless @order.completed? %></h6>
<div class="delivery">
<%= order.shipping_method.name %>
</div>
</div>
</div>
<% end %>

<div class="columns omega four">
<h6><%= t(:payment_information) %> <%= link_to "(#{t(:edit)})", checkout_state_path(:payment) unless @order.completed? %></h6>
Expand Down Expand Up @@ -110,4 +112,4 @@
</tr>
<% end %>
</tfoot>
</table>
</table>
4 changes: 3 additions & 1 deletion core/lib/spree/core/engine.rb
Expand Up @@ -10,19 +10,21 @@ class Engine < ::Rails::Engine
config.autoload_paths += %W(#{config.root}/lib)

def self.activate
Spree::Order.define_state_machine!
end

config.to_prepare &method(:activate).to_proc

config.after_initialize do
Spree::Order.define_state_machine!

ActiveSupport::Notifications.subscribe(/^spree\./) do |*args|
event_name, start_time, end_time, id, payload = args
Activator.active.event_name_starts_with(event_name).each do |activator|
payload[:event_name] = event_name
activator.activate(payload)
end
end

end

# We need to reload the routes here due to how Spree sets them up.
Expand Down

0 comments on commit fa1d66c

Please sign in to comment.