Blog

April 08 2009

Simplified Checkout Form Using Nested Attributesby railsdog

Rails 2.3 has an excellent new feature which allows for the use of nested attributes. Prior to this we had a CheckoutPresenter and separate CheckoutController with a lot of confusing logic. We were using active_presenter to help simplify things but the nested forms allow us to simplify things even further.

In fact, we were able to consolidate all of the checkout logic back into a single checkout method of OrdersController and eliminate the non-restful hacks we were relying on with CheckoutController.

Behold the new checkout method:


def checkout
  build_object 
  load_object 
  load_data 
  # additional default values needed for checkout
  @order.bill_address ||= Address.new(:country => @default_country)
  @order.ship_address ||= Address.new(:country => @default_country)
  if @order.creditcards.empty?
    @order.creditcards.build(:month => Date.today.month, :year => Date.today.year)
  end
  @shipping_method = ShippingMethod.find_by_id(params[:method_id]) if params[:method_id]  
  @shipping_method ||= @order.shipping_methods.first    
  @order.shipments.build(:address => @order.ship_address, :shipping_method => @shipping_method)      
  if request.put?                           
    @order.creditcards.clear
    @order.attributes = params[:order]
    @order.creditcards[0].address = @order.bill_address if @order.creditcards.present?
    @order.user = current_user       
    @order.ip_address = request.env['REMOTE_ADDR']
    @order.update_totals
    begin
      # need to check valid b/c we dump the creditcard info while saving
      if @order.valid?                       
        if params[:final_answer].blank?
          @order.save
        else                                           
          @order.creditcards[0].authorize(@order.total)
          @order.complete
          # remove the order from the session
          session[:order_id] = nil 
        end
      else
        flash.now[:error] = t("unable_to_save_order")  
        render :action => "checkout" and return unless request.xhr?
      end       
    rescue Spree::GatewayError => ge
      flash.now[:error] = t("unable_to_authorize_credit_card") + ": #{ge.message}"
      render :action => "new" and return 
    end
    respond_to do |format|
      format.html {redirect_to order_url(@order, :checkout_complete => true) }
      format.js {render :json => { :order => {:order_total => @order.total, 
                                              :ship_amount => @order.ship_amount, 
                                              :tax_amount => @order.tax_amount},
                                   :available_methods => rate_hash}.to_json,
                        :layout => false}
    end
  end
end

This single method takes care of all of the steps of the checkout process, including the AJAX posts that occur while transitioning between steps. We’re also considering moving this checkout method into its own module so that you can just override this library in your extension instead of having a giant class_eval or requiring that you duplicate the OrdersController in your site extension.

This will also mean some minor but hopefully obvious changes to your existing sites if you have modified the checkout process. The mostly likely change you will need to make is to move the location of any views you have overridden so that they are in the app/views/orders folder instead of app/views/checkout.