diff --git a/app/assets/javascripts/darkswarm/services/cart.js.coffee b/app/assets/javascripts/darkswarm/services/cart.js.coffee index b797390e73..849af562b8 100644 --- a/app/assets/javascripts/darkswarm/services/cart.js.coffee +++ b/app/assets/javascripts/darkswarm/services/cart.js.coffee @@ -41,7 +41,7 @@ Darkswarm.factory 'Cart', (CurrentOrder, Variants, $timeout, $http, $modal, $roo update: => @update_running = true - $http.post('/orders/populate', @data()).success (data, status)=> + $http.post('/cart/populate', @data()).success (data, status)=> @saved() @update_running = false diff --git a/app/controllers/cart_controller.rb b/app/controllers/cart_controller.rb new file mode 100644 index 0000000000..0aa1291eef --- /dev/null +++ b/app/controllers/cart_controller.rb @@ -0,0 +1,96 @@ +require 'spree/core/controller_helpers/order_decorator' + +class CartController < BaseController + before_filter :check_authorization + + def populate + # Without intervention, the Spree::Adjustment#update_adjustable callback is called many times + # during cart population, for both taxation and enterprise fees. This operation triggers a + # costly Spree::Order#update!, which only needs to be run once. We avoid this by disabling + # callbacks on Spree::Adjustment and then manually invoke Spree::Order#update! on success. + Spree::Adjustment.without_callbacks do + populator = Spree::OrderPopulator.new(current_order(true), current_currency) + + if populator.populate(params.slice(:products, :variants, :quantity), true) + fire_event('spree.cart.add') + fire_event('spree.order.contents_changed') + + current_order.cap_quantity_at_stock! + current_order.update! + + variant_ids = variant_ids_in(populator.variants_h) + + render json: { error: false, stock_levels: stock_levels(current_order, variant_ids) }, + status: 200 + + else + render json: { error: true }, status: 412 + end + end + populate_variant_attributes + end + + # Report the stock levels in the order for all variant ids requested + def stock_levels(order, variant_ids) + stock_levels = li_stock_levels(order) + + li_variant_ids = stock_levels.keys + (variant_ids - li_variant_ids).each do |variant_id| + stock_levels[variant_id] = { quantity: 0, max_quantity: 0, on_hand: Spree::Variant.find(variant_id).on_hand } + end + + stock_levels + end + + def variant_ids_in(variants_h) + variants_h.map { |v| v[:variant_id].to_i } + end + + private + + def check_authorization + session[:access_token] ||= params[:token] + order = Spree::Order.find_by_number(params[:id]) || current_order + + if order + authorize! :edit, order, session[:access_token] + else + authorize! :create, Spree::Order + end + end + + def populate_variant_attributes + order = current_order.reload + + if params.key? :variant_attributes + params[:variant_attributes].each do |variant_id, attributes| + order.set_variant_attributes(Spree::Variant.find(variant_id), attributes) + end + end + + if params.key? :quantity + params[:products].each do |_product_id, variant_id| + max_quantity = params[:max_quantity].to_i + order.set_variant_attributes(Spree::Variant.find(variant_id), + max_quantity: max_quantity) + end + end + end + + def li_stock_levels(order) + Hash[ + order.line_items.map do |li| + [li.variant.id, + { quantity: li.quantity, + max_quantity: li.max_quantity, + on_hand: wrap_json_infinity(li.variant.on_hand) }] + end + ] + end + + # Rails to_json encodes Float::INFINITY as Infinity, which is not valid JSON + # Return it as a large integer (max 32 bit signed int) + def wrap_json_infinity(number) + number == Float::INFINITY ? 2147483647 : number + end +end diff --git a/app/controllers/spree/orders_controller_decorator.rb b/app/controllers/spree/orders_controller_decorator.rb index e435b64522..a354852391 100644 --- a/app/controllers/spree/orders_controller_decorator.rb +++ b/app/controllers/spree/orders_controller_decorator.rb @@ -1,7 +1,6 @@ require 'spree/core/controller_helpers/order_decorator' Spree::OrdersController.class_eval do - after_filter :populate_variant_attributes, only: :populate before_filter :update_distribution, only: :update before_filter :filter_order_params, only: :update before_filter :enable_embedded_shopfront @@ -71,61 +70,6 @@ Spree::OrdersController.class_eval do end end - def populate - # Without intervention, the Spree::Adjustment#update_adjustable callback is called many times - # during cart population, for both taxation and enterprise fees. This operation triggers a - # costly Spree::Order#update!, which only needs to be run once. We avoid this by disabling - # callbacks on Spree::Adjustment and then manually invoke Spree::Order#update! on success. - - Spree::Adjustment.without_callbacks do - populator = Spree::OrderPopulator.new(current_order(true), current_currency) - - if populator.populate(params.slice(:products, :variants, :quantity), true) - fire_event('spree.cart.add') - fire_event('spree.order.contents_changed') - - current_order.cap_quantity_at_stock! - current_order.update! - - variant_ids = variant_ids_in(populator.variants_h) - - render json: {error: false, stock_levels: stock_levels(current_order, variant_ids)}, - status: 200 - - else - render json: {error: true}, status: 412 - end - end - end - - # Report the stock levels in the order for all variant ids requested - def stock_levels(order, variant_ids) - stock_levels = li_stock_levels(order) - - li_variant_ids = stock_levels.keys - (variant_ids - li_variant_ids).each do |variant_id| - stock_levels[variant_id] = {quantity: 0, max_quantity: 0, - on_hand: Spree::Variant.find(variant_id).on_hand} - end - - stock_levels - end - - def variant_ids_in(variants_h) - variants_h.map { |v| v[:variant_id].to_i } - end - - def li_stock_levels(order) - Hash[ - order.line_items.map do |li| - [li.variant.id, - {quantity: li.quantity, - max_quantity: li.max_quantity, - on_hand: wrap_json_infinity(li.variant.on_hand)}] - end - ] - end - def update_distribution @order = current_order(true) @@ -184,30 +128,6 @@ Spree::OrdersController.class_eval do private - def populate_variant_attributes - order = current_order.reload - - if params.key? :variant_attributes - params[:variant_attributes].each do |variant_id, attributes| - order.set_variant_attributes(Spree::Variant.find(variant_id), attributes) - end - end - - if params.key? :quantity - params[:products].each do |product_id, variant_id| - max_quantity = params[:max_quantity].to_i - order.set_variant_attributes(Spree::Variant.find(variant_id), - {:max_quantity => max_quantity}) - end - end - end - - # Rails to_json encodes Float::INFINITY as Infinity, which is not valid JSON - # Return it as a large integer (max 32 bit signed int) - def wrap_json_infinity(n) - n == Float::INFINITY ? 2147483647 : n - end - def order_to_update return @order_to_update if defined? @order_to_update return @order_to_update = current_order unless params[:id] diff --git a/config/routes.rb b/config/routes.rb index 37dbfc1e51..574140bd5b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -24,6 +24,14 @@ Openfoodnetwork::Application.routes.draw do get "/connect", to: redirect("https://openfoodnetwork.org/#{ENV['DEFAULT_COUNTRY_CODE'].andand.downcase}/connect/") get "/learn", to: redirect("https://openfoodnetwork.org/#{ENV['DEFAULT_COUNTRY_CODE'].andand.downcase}/learn/") + get "/cart", :to => "spree/orders#edit", :as => :cart + put "/cart", :to => "spree/orders#update", :as => :update_cart + put "/cart/empty", :to => 'spree/orders#empty', :as => :empty_cart + + resource :cart, controller: "cart" do + post :populate + end + resource :shop, controller: "shop" do get :products post :order_cycle diff --git a/spec/controllers/cart_controller_spec.rb b/spec/controllers/cart_controller_spec.rb new file mode 100644 index 0000000000..ff6e3bda9b --- /dev/null +++ b/spec/controllers/cart_controller_spec.rb @@ -0,0 +1,104 @@ +require 'spec_helper' + +describe CartController, type: :controller do + let(:order) { create(:order) } + + describe "returning stock levels in JSON on success" do + let(:product) { create(:simple_product) } + + it "returns stock levels as JSON" do + controller.stub(:variant_ids_in) { [123] } + controller.stub(:stock_levels) { 'my_stock_levels' } + Spree::OrderPopulator.stub(:new).and_return(populator = double()) + populator.stub(:populate) { true } + populator.stub(:variants_h) { {} } + + xhr :post, :populate, use_route: :spree, format: :json + + data = JSON.parse(response.body) + data['stock_levels'].should == 'my_stock_levels' + end + + describe "generating stock levels" do + let!(:order) { create(:order) } + let!(:li) { create(:line_item, order: order, variant: v, quantity: 2, max_quantity: 3) } + let!(:v) { create(:variant, count_on_hand: 4) } + let!(:v2) { create(:variant, count_on_hand: 2) } + + before do + order.reload + controller.stub(:current_order) { order } + end + + it "returns a hash with variant id, quantity, max_quantity and stock on hand" do + controller.stock_levels(order, [v.id]).should == + {v.id => {quantity: 2, max_quantity: 3, on_hand: 4}} + end + + it "includes all line items, even when the variant_id is not specified" do + controller.stock_levels(order, []).should == + {v.id => {quantity: 2, max_quantity: 3, on_hand: 4}} + end + + it "includes an empty quantity entry for variants that aren't in the order" do + controller.stock_levels(order, [v.id, v2.id]).should == + {v.id => {quantity: 2, max_quantity: 3, on_hand: 4}, + v2.id => {quantity: 0, max_quantity: 0, on_hand: 2}} + end + + describe "encoding Infinity" do + let!(:v) { create(:variant, on_demand: true, count_on_hand: 0) } + + it "encodes Infinity as a large, finite integer" do + controller.stock_levels(order, [v.id]).should == + {v.id => {quantity: 2, max_quantity: 3, on_hand: 2147483647}} + end + end + end + + it "extracts variant ids from the populator" do + variants_h = [{:variant_id=>"900", :quantity=>2, :max_quantity=>nil}, + {:variant_id=>"940", :quantity=>3, :max_quantity=>3}] + + controller.variant_ids_in(variants_h).should == [900, 940] + end + end + + context "adding a group buy product to the cart" do + it "sets a variant attribute for the max quantity" do + distributor_product = create(:distributor_enterprise) + p = create(:product, :distributors => [distributor_product], :group_buy => true) + + order = subject.current_order(true) + order.stub(:distributor) { distributor_product } + order.should_receive(:set_variant_attributes).with(p.master, {'max_quantity' => '3'}) + controller.stub(:current_order).and_return(order) + + expect do + spree_post :populate, :variants => {p.master.id => 1}, :variant_attributes => {p.master.id => {:max_quantity => 3}} + end.to change(Spree::LineItem, :count).by(1) + end + + it "returns HTTP success when successful" do + Spree::OrderPopulator.stub(:new).and_return(populator = double()) + populator.stub(:populate) { true } + populator.stub(:variants_h) { {} } + xhr :post, :populate, use_route: :spree, format: :json + response.status.should == 200 + end + + it "returns failure when unsuccessful" do + Spree::OrderPopulator.stub(:new).and_return(populator = double()) + populator.stub(:populate).and_return false + xhr :post, :populate, use_route: :spree, format: :json + response.status.should == 412 + end + + it "tells populator to overwrite" do + Spree::OrderPopulator.stub(:new).and_return(populator = double()) + populator.should_receive(:populate).with({}, true) + xhr :post, :populate, use_route: :spree, format: :json + end + end +end + diff --git a/spec/controllers/spree/orders_controller_spec.rb b/spec/controllers/spree/orders_controller_spec.rb index fbcd5ec107..86f232c48f 100644 --- a/spec/controllers/spree/orders_controller_spec.rb +++ b/spec/controllers/spree/orders_controller_spec.rb @@ -63,104 +63,6 @@ describe Spree::OrdersController, type: :controller do end end - describe "returning stock levels in JSON on success" do - let(:product) { create(:simple_product) } - - it "returns stock levels as JSON" do - controller.stub(:variant_ids_in) { [123] } - controller.stub(:stock_levels) { 'my_stock_levels' } - Spree::OrderPopulator.stub(:new).and_return(populator = double()) - populator.stub(:populate) { true } - populator.stub(:variants_h) { {} } - - xhr :post, :populate, use_route: :spree, format: :json - - data = JSON.parse(response.body) - data['stock_levels'].should == 'my_stock_levels' - end - - describe "generating stock levels" do - let!(:order) { create(:order) } - let!(:li) { create(:line_item, order: order, variant: v, quantity: 2, max_quantity: 3) } - let!(:v) { create(:variant, count_on_hand: 4) } - let!(:v2) { create(:variant, count_on_hand: 2) } - - before do - order.reload - controller.stub(:current_order) { order } - end - - it "returns a hash with variant id, quantity, max_quantity and stock on hand" do - controller.stock_levels(order, [v.id]).should == - {v.id => {quantity: 2, max_quantity: 3, on_hand: 4}} - end - - it "includes all line items, even when the variant_id is not specified" do - controller.stock_levels(order, []).should == - {v.id => {quantity: 2, max_quantity: 3, on_hand: 4}} - end - - it "includes an empty quantity entry for variants that aren't in the order" do - controller.stock_levels(order, [v.id, v2.id]).should == - {v.id => {quantity: 2, max_quantity: 3, on_hand: 4}, - v2.id => {quantity: 0, max_quantity: 0, on_hand: 2}} - end - - describe "encoding Infinity" do - let!(:v) { create(:variant, on_demand: true, count_on_hand: 0) } - - it "encodes Infinity as a large, finite integer" do - controller.stock_levels(order, [v.id]).should == - {v.id => {quantity: 2, max_quantity: 3, on_hand: 2147483647}} - end - end - end - - it "extracts variant ids from the populator" do - variants_h = [{:variant_id=>"900", :quantity=>2, :max_quantity=>nil}, - {:variant_id=>"940", :quantity=>3, :max_quantity=>3}] - - controller.variant_ids_in(variants_h).should == [900, 940] - end - end - - context "adding a group buy product to the cart" do - it "sets a variant attribute for the max quantity" do - distributor_product = create(:distributor_enterprise) - p = create(:product, :distributors => [distributor_product], :group_buy => true) - - order = subject.current_order(true) - order.stub(:distributor) { distributor_product } - order.should_receive(:set_variant_attributes).with(p.master, {'max_quantity' => '3'}) - controller.stub(:current_order).and_return(order) - - expect do - spree_post :populate, :variants => {p.master.id => 1}, :variant_attributes => {p.master.id => {:max_quantity => 3}} - end.to change(Spree::LineItem, :count).by(1) - end - - it "returns HTTP success when successful" do - Spree::OrderPopulator.stub(:new).and_return(populator = double()) - populator.stub(:populate) { true } - populator.stub(:variants_h) { {} } - xhr :post, :populate, use_route: :spree, format: :json - response.status.should == 200 - end - - it "returns failure when unsuccessful" do - Spree::OrderPopulator.stub(:new).and_return(populator = double()) - populator.stub(:populate).and_return false - xhr :post, :populate, use_route: :spree, format: :json - response.status.should == 412 - end - - it "tells populator to overwrite" do - Spree::OrderPopulator.stub(:new).and_return(populator = double()) - populator.should_receive(:populate).with({}, true) - xhr :post, :populate, use_route: :spree, format: :json - end - end - describe "removing line items from cart" do describe "when I pass params that includes a line item no longer in our cart" do it "should silently ignore the missing line item" do diff --git a/spec/javascripts/unit/darkswarm/services/cart_spec.js.coffee b/spec/javascripts/unit/darkswarm/services/cart_spec.js.coffee index 53ae64f437..131880ddc7 100644 --- a/spec/javascripts/unit/darkswarm/services/cart_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/services/cart_spec.js.coffee @@ -80,7 +80,7 @@ describe 'Cart service', -> data = {variants: {}} it "sets update_running during the update, and clears it on success", -> - $httpBackend.expectPOST("/orders/populate", data).respond 200, {} + $httpBackend.expectPOST("/cart/populate", data).respond 200, {} expect(Cart.update_running).toBe(false) Cart.update() expect(Cart.update_running).toBe(true) @@ -88,7 +88,7 @@ describe 'Cart service', -> expect(Cart.update_running).toBe(false) it "sets update_running during the update, and clears it on failure", -> - $httpBackend.expectPOST("/orders/populate", data).respond 404, {} + $httpBackend.expectPOST("/cart/populate", data).respond 404, {} expect(Cart.update_running).toBe(false) Cart.update() expect(Cart.update_running).toBe(true) @@ -97,7 +97,7 @@ describe 'Cart service', -> it "marks the form as saved on success", -> spyOn(Cart, 'saved') - $httpBackend.expectPOST("/orders/populate", data).respond 200, {} + $httpBackend.expectPOST("/cart/populate", data).respond 200, {} Cart.update() $httpBackend.flush() expect(Cart.saved).toHaveBeenCalled() @@ -106,7 +106,7 @@ describe 'Cart service', -> Cart.update_enqueued = true spyOn(Cart, 'saved') spyOn(Cart, 'popQueue') - $httpBackend.expectPOST("/orders/populate", data).respond 200, {} + $httpBackend.expectPOST("/cart/populate", data).respond 200, {} Cart.update() $httpBackend.flush() expect(Cart.popQueue).toHaveBeenCalled() @@ -115,14 +115,14 @@ describe 'Cart service', -> Cart.update_enqueued = false spyOn(Cart, 'saved') spyOn(Cart, 'popQueue') - $httpBackend.expectPOST("/orders/populate", data).respond 200, {} + $httpBackend.expectPOST("/cart/populate", data).respond 200, {} Cart.update() $httpBackend.flush() expect(Cart.popQueue).not.toHaveBeenCalled() it "retries the update on failure", -> spyOn(Cart, 'scheduleRetry') - $httpBackend.expectPOST("/orders/populate", data).respond 404, {} + $httpBackend.expectPOST("/cart/populate", data).respond 404, {} Cart.update() $httpBackend.flush() expect(Cart.scheduleRetry).toHaveBeenCalled()