mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-02-06 22:36:07 +00:00
Bring order checkout workflow and some of its specs from spree_core
This commit is contained in:
182
app/models/spree/order/checkout.rb
Normal file
182
app/models/spree/order/checkout.rb
Normal file
@@ -0,0 +1,182 @@
|
||||
module Spree
|
||||
class Order < ActiveRecord::Base
|
||||
module Checkout
|
||||
def self.included(klass)
|
||||
klass.class_eval do
|
||||
class_attribute :next_event_transitions
|
||||
class_attribute :previous_states
|
||||
class_attribute :checkout_flow
|
||||
class_attribute :checkout_steps
|
||||
class_attribute :removed_transitions
|
||||
|
||||
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.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 a ton of warnings when the state machine is re-defined
|
||||
StateMachine::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 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, :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
|
||||
|
||||
if states[:payment]
|
||||
before_transition :to => :complete do |order|
|
||||
order.process_payments! if order.payment_required?
|
||||
end
|
||||
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
|
||||
|
||||
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
|
||||
end
|
||||
end
|
||||
|
||||
def self.go_to_state(name, options={})
|
||||
self.checkout_steps[name] = options
|
||||
previous_states.each do |state|
|
||||
add_transition({:from => state, :to => name}.merge(options))
|
||||
end
|
||||
if options[:if]
|
||||
self.previous_states << name
|
||||
else
|
||||
self.previous_states = [name]
|
||||
end
|
||||
end
|
||||
|
||||
def self.insert_checkout_step(name, options = {})
|
||||
before = options.delete(:before)
|
||||
after = options.delete(:after) unless before
|
||||
after = self.checkout_steps.keys.last unless before || after
|
||||
|
||||
cloned_steps = self.checkout_steps.clone
|
||||
cloned_removed_transitions = self.removed_transitions.clone
|
||||
self.checkout_flow do
|
||||
cloned_steps.each_pair do |key, value|
|
||||
self.go_to_state(name, options) if key == before
|
||||
self.go_to_state(key, value)
|
||||
self.go_to_state(name, options) if key == after
|
||||
end
|
||||
cloned_removed_transitions.each do |transition|
|
||||
self.remove_transition(transition)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.remove_checkout_step(name)
|
||||
cloned_steps = self.checkout_steps.clone
|
||||
cloned_removed_transitions = self.removed_transitions.clone
|
||||
self.checkout_flow do
|
||||
cloned_steps.each_pair do |key, value|
|
||||
self.go_to_state(key, value) unless key == name
|
||||
end
|
||||
cloned_removed_transitions.each do |transition|
|
||||
self.remove_transition(transition)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.remove_transition(options={})
|
||||
self.removed_transitions << options
|
||||
self.next_event_transitions.delete(find_transition(options))
|
||||
end
|
||||
|
||||
def self.find_transition(options={})
|
||||
return nil if options.nil? || !options.include?(:from) || !options.include?(:to)
|
||||
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 ||= {}
|
||||
end
|
||||
|
||||
def self.add_transition(options)
|
||||
self.next_event_transitions << { options.delete(:from) => options.delete(:to) }.merge(options)
|
||||
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)
|
||||
checkout_steps << step
|
||||
}.map(&:to_s)
|
||||
# Ensure there is always a complete step
|
||||
steps << "complete" unless steps.include?("complete")
|
||||
steps
|
||||
end
|
||||
|
||||
def has_checkout_step?(step)
|
||||
step.present? ? self.checkout_steps.include?(step) : false
|
||||
end
|
||||
|
||||
def checkout_step_index(step)
|
||||
self.checkout_steps.index(step)
|
||||
end
|
||||
|
||||
def self.removed_transitions
|
||||
@removed_transitions ||= []
|
||||
end
|
||||
|
||||
def can_go_to_state?(state)
|
||||
return false unless self.state.present? && has_checkout_step?(state) && has_checkout_step?(self.state)
|
||||
checkout_step_index(state) > checkout_step_index(self.state)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,6 +1,337 @@
|
||||
require 'spec_helper'
|
||||
|
||||
describe Spree::Order do
|
||||
let(:order) { Spree::Order.new }
|
||||
|
||||
context "with default state machine" do
|
||||
let(:transitions) do
|
||||
[
|
||||
{ :address => :delivery },
|
||||
{ :delivery => :payment },
|
||||
{ :payment => :confirm },
|
||||
{ :confirm => :complete },
|
||||
{ :payment => :complete },
|
||||
{ :delivery => :complete }
|
||||
]
|
||||
end
|
||||
|
||||
it "has the following transitions" do
|
||||
transitions.each do |transition|
|
||||
transition = Spree::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 = Spree::Order.find_transition(:from => :delivery, :to => :confirm)
|
||||
transition.should be_nil
|
||||
end
|
||||
|
||||
it '.find_transition when contract was broken' do
|
||||
Spree::Order.find_transition({foo: :bar, baz: :dog}).should be_false
|
||||
end
|
||||
|
||||
it '.remove_transition' do
|
||||
options = {:from => transitions.first.keys.first, :to => transitions.first.values.first}
|
||||
Spree::Order.stub(:next_event_transition).and_return([options])
|
||||
Spree::Order.remove_transition(options).should be_true
|
||||
end
|
||||
|
||||
it '.remove_transition when contract was broken' do
|
||||
Spree::Order.remove_transition(nil).should be_false
|
||||
end
|
||||
|
||||
context "#checkout_steps" do
|
||||
context "when confirmation not required" do
|
||||
before do
|
||||
order.stub :confirmation_required? => false
|
||||
order.stub :payment_required? => true
|
||||
end
|
||||
|
||||
specify do
|
||||
order.checkout_steps.should == %w(address delivery payment complete)
|
||||
end
|
||||
end
|
||||
|
||||
context "when confirmation required" do
|
||||
before do
|
||||
order.stub :confirmation_required? => true
|
||||
order.stub :payment_required? => true
|
||||
end
|
||||
|
||||
specify do
|
||||
order.checkout_steps.should == %w(address delivery payment confirm complete)
|
||||
end
|
||||
end
|
||||
|
||||
context "when payment not required" do
|
||||
before { order.stub :payment_required? => false }
|
||||
specify do
|
||||
order.checkout_steps.should == %w(address delivery complete)
|
||||
end
|
||||
end
|
||||
|
||||
context "when payment required" do
|
||||
before { order.stub :payment_required? => true }
|
||||
specify do
|
||||
order.checkout_steps.should == %w(address delivery payment complete)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it "starts out at cart" do
|
||||
order.state.should == "cart"
|
||||
end
|
||||
|
||||
it "transitions to address" do
|
||||
order.line_items << FactoryGirl.create(:line_item)
|
||||
order.email = "user@example.com"
|
||||
order.next!
|
||||
order.state.should == "address"
|
||||
end
|
||||
|
||||
it "cannot transition to address without any line items" do
|
||||
order.line_items.should be_blank
|
||||
lambda { order.next! }.should raise_error(StateMachine::InvalidTransition, /#{Spree.t(:there_are_no_items_for_this_order)}/)
|
||||
end
|
||||
|
||||
context "from address" do
|
||||
before do
|
||||
order.state = 'address'
|
||||
order.stub(:has_available_payment)
|
||||
shipment = FactoryGirl.create(:shipment, :order => order)
|
||||
order.email = "user@example.com"
|
||||
order.save!
|
||||
end
|
||||
|
||||
it "transitions to delivery" do
|
||||
order.stub(:ensure_available_shipping_rates => true)
|
||||
order.next!
|
||||
order.state.should == "delivery"
|
||||
end
|
||||
|
||||
context "cannot transition to delivery" do
|
||||
context "if there are no shipping rates for any shipment" do
|
||||
specify do
|
||||
transition = lambda { order.next! }
|
||||
transition.should raise_error(StateMachine::InvalidTransition, /#{Spree.t(:items_cannot_be_shipped)}/)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "from delivery" do
|
||||
before do
|
||||
order.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.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
|
||||
order.stub :payment_required? => true
|
||||
end
|
||||
|
||||
it "transitions to complete" do
|
||||
order.should_receive(:process_payments!).once.and_return true
|
||||
order.next!
|
||||
order.state.should == "complete"
|
||||
end
|
||||
end
|
||||
|
||||
# Regression test for #2028
|
||||
context "when payment is not required" do
|
||||
before do
|
||||
order.stub :payment_required? => false
|
||||
end
|
||||
|
||||
it "does not call process payments" do
|
||||
order.should_not_receive(:process_payments!)
|
||||
order.next!
|
||||
order.state.should == "complete"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "subclassed order" do
|
||||
# This causes another test above to fail, but fixing this test should make
|
||||
# the other test pass
|
||||
class SubclassedOrder < Spree::Order
|
||||
checkout_flow do
|
||||
go_to_state :payment
|
||||
go_to_state :complete
|
||||
end
|
||||
end
|
||||
|
||||
it "should only call default transitions once when checkout_flow is redefined" do
|
||||
order = SubclassedOrder.new
|
||||
order.stub :payment_required? => true
|
||||
order.should_receive(:process_payments!).once
|
||||
order.state = "payment"
|
||||
order.next!
|
||||
order.state.should == "complete"
|
||||
end
|
||||
end
|
||||
|
||||
context "re-define checkout flow" do
|
||||
before do
|
||||
@old_checkout_flow = Spree::Order.checkout_flow
|
||||
Spree::Order.class_eval do
|
||||
checkout_flow do
|
||||
go_to_state :payment
|
||||
go_to_state :complete
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
after do
|
||||
Spree::Order.checkout_flow(&@old_checkout_flow)
|
||||
end
|
||||
|
||||
it "should not keep old event transitions when checkout_flow is redefined" do
|
||||
Spree::Order.next_event_transitions.should == [{:cart=>:payment}, {:payment=>:complete}]
|
||||
end
|
||||
|
||||
it "should not keep old events when checkout_flow is redefined" do
|
||||
state_machine = Spree::Order.state_machine
|
||||
state_machine.states.any? { |s| s.name == :address }.should be_false
|
||||
known_states = state_machine.events[:next].branches.map(&:known_states).flatten
|
||||
known_states.should_not include(:address)
|
||||
known_states.should_not include(:delivery)
|
||||
known_states.should_not include(:confirm)
|
||||
end
|
||||
end
|
||||
|
||||
# Regression test for #3665
|
||||
context "with only a complete step" do
|
||||
before do
|
||||
@old_checkout_flow = Spree::Order.checkout_flow
|
||||
Spree::Order.class_eval do
|
||||
checkout_flow do
|
||||
go_to_state :complete
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
after do
|
||||
Spree::Order.checkout_flow(&@old_checkout_flow)
|
||||
end
|
||||
|
||||
it "does not attempt to process payments" do
|
||||
order.stub_chain(:line_items, :present?).and_return(true)
|
||||
order.should_not_receive(:payment_required?)
|
||||
order.should_not_receive(:process_payments!)
|
||||
order.next!
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context "insert checkout step" do
|
||||
before do
|
||||
@old_checkout_flow = Spree::Order.checkout_flow
|
||||
Spree::Order.class_eval do
|
||||
insert_checkout_step :new_step, before: :address
|
||||
end
|
||||
end
|
||||
|
||||
after do
|
||||
Spree::Order.checkout_flow(&@old_checkout_flow)
|
||||
end
|
||||
|
||||
it "should maintain removed transitions" do
|
||||
transition = Spree::Order.find_transition(:from => :delivery, :to => :confirm)
|
||||
transition.should be_nil
|
||||
end
|
||||
|
||||
context "before" do
|
||||
before do
|
||||
Spree::Order.class_eval do
|
||||
insert_checkout_step :before_address, before: :address
|
||||
end
|
||||
end
|
||||
|
||||
specify do
|
||||
order = Spree::Order.new
|
||||
order.checkout_steps.should == %w(new_step before_address address delivery complete)
|
||||
end
|
||||
end
|
||||
|
||||
context "after" do
|
||||
before do
|
||||
Spree::Order.class_eval do
|
||||
insert_checkout_step :after_address, after: :address
|
||||
end
|
||||
end
|
||||
|
||||
specify do
|
||||
order = Spree::Order.new
|
||||
order.checkout_steps.should == %w(new_step address after_address delivery complete)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "remove checkout step" do
|
||||
before do
|
||||
@old_checkout_flow = Spree::Order.checkout_flow
|
||||
Spree::Order.class_eval do
|
||||
remove_checkout_step :address
|
||||
end
|
||||
end
|
||||
|
||||
after do
|
||||
Spree::Order.checkout_flow(&@old_checkout_flow)
|
||||
end
|
||||
|
||||
it "should maintain removed transitions" do
|
||||
transition = Spree::Order.find_transition(:from => :delivery, :to => :confirm)
|
||||
transition.should be_nil
|
||||
end
|
||||
|
||||
specify do
|
||||
order = Spree::Order.new
|
||||
order.checkout_steps.should == %w(delivery complete)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'event :restart_checkout' do
|
||||
let(:order) { create(:order) }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user