- This post is based on Spree v0.40.2
One of the more frequently discussed topics on the Spree Google Group is the checkout process in Spree. It is one of the most basic and important features of any e-commerce application, but it can also be very complicated under the hood. This post attempts to break down the various aspects of the checkout process in Spree.
In Spree, the checkout process is driven by the CheckoutController. The CheckoutController is unique within Spree in that it is not a typical RESTful controller. There is no ‘checkouts’ table, nor is there a Checkout model. The default CheckoutController implements a couple of actions (edit and update) that, at the most basic level, advance the state of an Order object.
The Order model is a typical ActiveRecord class that encapsulates the attributes and business logic of an order. For purposes of this post, relevant bits of the Order model are: the definition of the states, events, and transitions (and related callbacks) for the Order state machine. (The Order model utilizes the state_machine gem to define the states, transitions, etc. For more information on state_machine, please see the state_machine Github page.) Below is the state_machine block for the Order model from Spree v0.40.2.
<p>state_machine :initial => ‘cart’, :use_transactions => false do</p> event :next do transition :from => ‘cart’, :to => ‘address’ transition :from => ‘address’, :to => ‘delivery’ transition :from => ‘delivery’, :to => ‘payment’ transition :from => ‘payment’, :to => ‘confirm’ transition :from => ‘confirm’, :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 Spree::GatewayError if Spree::Config[:allow_checkout_on_gateway_error] true else false end end end after_transition :to => ‘complete’, :do => :finalize! after_transition :to => ‘delivery’, :do => :create_tax_charge! after_transition :to => ‘payment’, :do => :create_shipment! after_transition :to => ‘canceled’, :do => :after_cancel <p>end</p>
The most important event during checkout (in fact the only event called during a normal checkout) is the ‘next’ event. By examining the code above, you will notice that the ‘next’ event transitions the order through the various checkout steps: ‘cart’, ‘address’, ‘delivery’, ‘payment’, ‘confirm’ and ‘complete’.
Assuming a customer has at least one item in their cart, they should be able to access the first checkout step at http://my-spree-store.com/checkout. Looking at the routes file in Spree Core, you will see that a GET request on /checkout/:state route’s to the CheckoutController’s edit action. Leaving the :state parameter blank will pre-populate the :state parameter with ‘address’:
<p>match ‘/checkout/update/:state’ => ‘checkout#update’, :as => :update_checkout<br /> match ‘/checkout/:state’ => ‘checkout#edit’, :as => :checkout_state<br /> match ‘/checkout’ => ‘checkout#edit’, :state => ‘address’, :as => :checkout</p>
Rendering the checkout form
When the checkout/edit.html.erb template is rendered, a form is built that renders fields defined in a partial named with the same value of @order.state. For example http://my-spree-store.com/checkout/address renders checkout/edit.html.erb, which in turn renders [checkout/address.html.erb](https://github.com/railsdog/spree/blob/v0.40.2/core/app/views/checkout/address.html.erb).
Checkout Form Submission
Navigating through the various checkout screens by submitting the form performs a PUT request to the CheckoutController’s ‘update’ action. The first thing the ‘update’ action does after loading the order, update the order’s attributes with params[:order]. Assuming this is successful, the ‘next’ method is called on the updated order object, and the request is redirected to the next step in the checkout.
The Final Checkout Step
The final checkout step is the the ‘confirm’ step. During this step (http://my-spree-store.com/checkout/confim) the customer is allowed to review their order details one last time before placing the order. When the customer submits the form from this step, one final
CheckoutController#update is executed, and the order transitions to the ‘complete’ state. At this point, the request is redirected back to the order_path and the checkout is complete.
The Order state machine also defines various callbacks. For example, there is an
after_transition callback that executes the ‘Order#finalize!’ when an order transitions to the ‘complete’ state. I will leave it as an exercise for the reader to follow-up on what these callbacks actually do, but it is worth pointing out that they are present.
In addition to the callbacks in the Order state_machine, there are also various callbacks that are executed before transitioning to/from various states. Again, the reader should investigate further, but you may want to start with load_order and update.