3

I'm working on a Spree e-commerce store built on Ruby on Rails and want a custom action where a user can mark their order as complete straight from the checkout page without going through delivery etc. I've overridden all the checkout steps but cannot get the 'Checkout' button to complete the order by sending the order to a custom action in the Orders controller.

I'd like to think I've ticked off all the boxes: created a patch action in routes.rb and checked rake routes to make sure the route exists. But it's still telling me there is no route.

The cart page won't even load before I submit anything, with the following error. I've spent all day trying to fix this so any ideas would be great....

The error:

No route matches {:action=>"complete", :controller=>"spree/orders", :method=>:patch}

Routes.rb:

resources :orders do
   member do
     patch 'complete', to: 'orders#complete'
   end
 end

Rake routes:

        Prefix Verb   URI Pattern                    Controller#Action
       spree        /                              Spree::Core::Engine
complete_order PATCH  /orders/:id/complete(.:format) orders#complete
        orders GET    /orders(.:format)              orders#index
               POST   /orders(.:format)              orders#create
     new_order GET    /orders/new(.:format)          orders#new
    edit_order GET    /orders/:id/edit(.:format)     orders#edit
         order GET    /orders/:id(.:format)          orders#show
               PATCH  /orders/:id(.:format)          orders#update
               PUT    /orders/:id(.:format)          orders#update
               DELETE /orders/:id(.:format)          orders#destroy

HTML:

<%= form_for :order, url: {action: 'complete', method: :patch} do |f| %>
  <% f.submit %>
<% end %>

I haven't created the controller yet but it would be:

def complete
  # mark order as complete
  # redirect to confirmation page
end

Would really appreciate any help. Thanks

EDIT: Here is the updated view (app/views/orders/edit.html.erb):

<% @body_id = 'cart' %>
<div data-hook="cart_container">
  <h1><%= Spree.t(:shopping_cart) %></h1>

  <% if @order.line_items.empty? %>
    <div data-hook="empty_cart">
      <div class="alert alert-info"><%= Spree.t(:your_cart_is_empty) %></div>
      <p><%= link_to Spree.t(:continue_shopping), products_path, class: 'btn btn-default' %></p>
    </div>
  <% else %>
    <div data-hook="outside_cart_form">
      <%= form_for @order, url: update_cart_path, html: { id: 'update-cart' } do |order_form| %>
        <div data-hook="inside_cart_form">

          <div data-hook="cart_items" class="table-responsive">
            <%= render partial: 'form', locals: { order_form: order_form } %>
          </div>

        </div>
      <% end %>
    </div>

    <div id="empty-cart" class="col-md-6" data-hook>
      <%= form_tag empty_cart_path, method: :put do %>
        <p id="clear_cart_link" data-hook>
          <%= submit_tag Spree.t(:empty_cart), class: 'btn btn-default' %>
          <%= Spree.t(:or) %>
          <%= link_to Spree.t(:continue_shopping), products_path, class: 'continue' %>
        </p>
      <% end %>
    </div>

    <div id="complete-order">
      complete order here - submit to custom controller
      <%= @order.id  %>
      <%= form_for @order, url: complete_order_path(@order) do |f| %>
        <% f.submit %>
      <% end %>


    </div>

  <% end %>
</div>

Here is the whole controller:

module Spree
  class OrdersController < Spree::StoreController
    before_action :check_authorization
    rescue_from ActiveRecord::RecordNotFound, :with => :render_404
    helper 'spree/products', 'spree/orders'

    respond_to :html

    before_action :assign_order_with_lock, only: :update
    skip_before_action :verify_authenticity_token, only: [:populate]

    def show
      @order = Order.includes(line_items: [variant: [:option_values, :images, :product]], bill_address: :state, ship_address: :state).find_by_number!(params[:id])
    end

    def complete
      @order = current_order
    end

    def update
      if @order.contents.update_cart(order_params)
        respond_with(@order) do |format|
          format.html do
            if params.has_key?(:checkout)
              @order.next if @order.cart?
              redirect_to checkout_state_path(@order.checkout_steps.first)
            else
              redirect_to cart_path
            end
          end
        end
      else
        respond_with(@order)
      end
    end

    # Shows the current incomplete order from the session
    def edit
      @order = current_order || Order.incomplete.
                                  includes(line_items: [variant: [:images, :option_values, :product]]).
                                  find_or_initialize_by(guest_token: cookies.signed[:guest_token])
      associate_user
    end

    # Adds a new item to the order (creating a new order if none already exists)
    def populate
      order    = current_order(create_order_if_necessary: true)
      variant  = Spree::Variant.find(params[:variant_id])
      quantity = params[:quantity].to_i
      options  = params[:options] || {}

      # 2,147,483,647 is crazy. See issue #2695.
      if quantity.between?(1, 2_147_483_647)
        begin
          order.contents.add(variant, quantity, options)
        rescue ActiveRecord::RecordInvalid => e
          error = e.record.errors.full_messages.join(", ")
        end
      else
        error = Spree.t(:please_enter_reasonable_quantity)
      end

      if error
        flash[:error] = error
        redirect_back_or_default(spree.root_path)
      else
        respond_with(order) do |format|
          format.html { redirect_to cart_path }
        end
      end
    end

    def empty
      if @order = current_order
        @order.empty!
      end

      redirect_to spree.cart_path
    end

    def accurate_title
      if @order && @order.completed?
        Spree.t(:order_number, :number => @order.number)
      else
        Spree.t(:shopping_cart)
      end
    end

    def check_authorization
      order = Spree::Order.find_by_number(params[:id]) || current_order

      if order
        authorize! :edit, order, cookies.signed[:guest_token]
      else
        authorize! :create, Spree::Order
      end
    end

    private

      def order_params
        if params[:order]
          params[:order].permit(*permitted_order_attributes)
        else
          {}
        end
      end

      def assign_order_with_lock
        @order = current_order(lock: true)
        unless @order
          flash[:error] = Spree.t(:order_not_found)
          redirect_to root_path and return
        end
      end
  end
end

EDIT

It has become apparent since I posted this question that you do in fact need to declare your routes in a special way, despite rake routes showing them as correct.

In routes.rb, add this:

Spree::Core::Engine.routes.draw do
   # add your custom  routes here, e.g.
   get '/terms-and-conditions' => 'home#terms', as: :terms
end

This will then allow you to use the <%= link_to("Terms", terms_path) %> helper.

See Adding Routes to Rails' Spree E-Commerce for more details. I wish the documentation was better on this as it is mentioned NOWHERE apart from on SO as far as I can tell.

Community
  • 1
  • 1
Tebbers
  • 484
  • 7
  • 12

1 Answers1

2

This happens because you are not passing an object to the form. So there's no id parameter in the route, and the router fails to make a match.

Your route is defined as a member action, which means it expects an id parameter. You are passing a symbol instead.

<%= form_for :order <-- problem

The clue is in the error message:

No route matches {:action=>"complete", :controller=>"spree/orders", :method=>:patch}

Notice how there's no id parameter in the hash in the error message?

To solve this, provide an object to the form. For example:

<%= form_for @order, url: complete_order_path(@order) do |f| %>

Where @order instance variable is set in the controller.

On a side note, you can define your routes like so:

resources :orders do
  member do
    patch :complete
  end
  # or, since it's only one route...
  patch :complete, on: :member
end

Notice you can use symbols, and you don't have to specify the controller because it's inferred from the resource name.

Finally, you don't need to tell the form that the method should be patch. Rails infers this from the object passed in, in this case @order. If it's new, the method will be POST, otherwise it will be PATCH.

Mohamad
  • 34,731
  • 32
  • 140
  • 219
  • Thanks very much for the reply, much appreciated. I tested out a few form_for combinations yesterday with none of them working. I tried your code and got the below error message: `undefined method `complete_order_path'` I should also mention that this form_for code is in the app/views/orders/edit.html.erb file if that makes a difference? – Tebbers Jan 28 '16 at 08:33
  • @Tebbers I'm almost 100% sure my answer is correct. You need to pass an object to your form, and to your path (because you are using a custom URL). Of course `complete_order_path` exists. It's in your `rake routes` output. But if you call it without an object, the route will not be recognised, and you get the undefined error you pasted, because it can't match (notice the `:id` in the path in `rake routes`). Show me how you are passing the `@order` object to the form and to the path: `complete_order_path(@order)` -- where are you declaring `@order`? – Mohamad Jan 28 '16 at 10:32
  • The @order instance variable already exists in the edit.html.erb view - I can test this by printing out `@order.id` on the page which correctly shows the order ID. So the order object is valid on this page - I just need to pass it to the custom action `complete`.I've edited my original question with the whole controller and view. Many thanks again for your help Mohamad. – Tebbers Jan 28 '16 at 11:04
  • @Tebbers hmmm... I'm really not sure, then. But usually, when you get an error about a path method being undefined, and it is, it's because the wrong arguments are being pass to it. On a side note, `<% f.submit ...` shouldn't that be `<%= f.submit ...`? – Mohamad Jan 28 '16 at 11:09
  • Yes, thanks, I've changed `f.submit` now, although it didnt solve the problem. I'm just wondering if this is something to do with Spree rather than a Rails issue. I might make a new controller and see if it can submit there - the controllers are in app/views/controllers/spree/xxx_controller.rb if that makes a difference? – Tebbers Jan 28 '16 at 11:24
  • Maybe your routes must be prefixed with `spree_`? `spree_something_path`? – Mohamad Jan 28 '16 at 12:01
  • If the routing was taking place correctly you would get an undefined method in the controller error... something else – Mohamad Jan 28 '16 at 12:02
  • I will look into this more and update when I find out! The Spree documentation is severely lacking in places but perhaps I need to do more reading. Thanks very much again for your input Mohamad. – Tebbers Jan 28 '16 at 13:49
  • You are welcome. Do let me know what it ends up being. – Mohamad Jan 28 '16 at 14:38
  • Hi @Mohamad, I solved it just by changing the checkout flow. Instead of the 'Checkout' button progressing the order, I routed it to a custom action in the Orders controller. From: `if params.has_key?(:checkout)` `@order.next if @order.cart?` `redirect_to checkout_state_path(@order.checkout_steps.first)` To: `if params.has_key?(:checkout)` `complete_order(@order)` `def complete_order(order)` `order.state = "complete"` `end` Thanks again for your input, pleased I could find a simple fix in the end. – Tebbers Feb 02 '16 at 13:52
  • It's also become apparent that you need to declare your routes in a special way using `Spree::Core::Engine.routes.draw do` in `routes.rb`. I've added this to my question, but I solved this another way. I wish the documentation was better on this. – Tebbers Feb 02 '16 at 15:48