diff --git a/app/controllers/cart_controller.rb b/app/controllers/cart_controller.rb index 0aa1291eef..65bb2402c2 100644 --- a/app/controllers/cart_controller.rb +++ b/app/controllers/cart_controller.rb @@ -9,16 +9,16 @@ class CartController < BaseController # 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) + cart_service = CartService.new(current_order(true), current_currency) - if populator.populate(params.slice(:products, :variants, :quantity), true) + if cart_service.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) + variant_ids = variant_ids_in(cart_service.variants_h) render json: { error: false, stock_levels: stock_levels(current_order, variant_ids) }, status: 200 diff --git a/app/models/spree/order_populator_decorator.rb b/app/services/cart_service.rb similarity index 94% rename from app/models/spree/order_populator_decorator.rb rename to app/services/cart_service.rb index 314e786c89..6e5e823520 100644 --- a/app/models/spree/order_populator_decorator.rb +++ b/app/services/cart_service.rb @@ -1,7 +1,15 @@ require 'open_food_network/scope_variant_to_hub' -Spree::OrderPopulator.class_eval do +class CartService + attr_accessor :order, :currency attr_reader :variants_h + attr_reader :errors + + def initialize(order, currency) + @order = order + @currency = currency + @errors = ActiveModel::Errors.new(self) + end def populate(from_hash, overwrite = false) @distributor, @order_cycle = distributor_and_order_cycle @@ -32,27 +40,6 @@ Spree::OrderPopulator.class_eval do valid? end - def read_variants(data) - @variants_h = read_products_hash(data) + - read_variants_hash(data) - end - - def read_products_hash(data) - (data[:products] || []).map do |product_id, variant_id| - {variant_id: variant_id, quantity: data[:quantity]} - end - end - - def read_variants_hash(data) - (data[:variants] || []).map do |variant_id, quantity| - if quantity.is_a?(Hash) - {variant_id: variant_id, quantity: quantity[:quantity], max_quantity: quantity[:max_quantity]} - else - {variant_id: variant_id, quantity: quantity} - end - end - end - def attempt_cart_add(variant_id, quantity, max_quantity = nil) quantity = quantity.to_i max_quantity = max_quantity.to_i if max_quantity @@ -82,14 +69,38 @@ Spree::OrderPopulator.class_eval do [quantity_to_add, max_quantity_to_add] end + private + + def read_variants(data) + @variants_h = read_products_hash(data) + + read_variants_hash(data) + end + + def read_products_hash(data) + (data[:products] || []).map do |product_id, variant_id| + {variant_id: variant_id, quantity: data[:quantity]} + end + end + + def read_variants_hash(data) + (data[:variants] || []).map do |variant_id, quantity| + if quantity.is_a?(Hash) + {variant_id: variant_id, quantity: quantity[:quantity], max_quantity: quantity[:max_quantity]} + else + {variant_id: variant_id, quantity: quantity} + end + end + end + + def valid? + errors.empty? + end + def cart_remove(variant_id) variant = Spree::Variant.find(variant_id) @order.remove_variant(variant) end - - private - def distributor_and_order_cycle [@order.distributor, @order.order_cycle] end diff --git a/spec/controllers/cart_controller_spec.rb b/spec/controllers/cart_controller_spec.rb index ff6e3bda9b..4345b91a52 100644 --- a/spec/controllers/cart_controller_spec.rb +++ b/spec/controllers/cart_controller_spec.rb @@ -9,9 +9,9 @@ describe CartController, type: :controller do 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) { {} } + CartService.stub(:new).and_return(cart_service = double()) + cart_service.stub(:populate) { true } + cart_service.stub(:variants_h) { {} } xhr :post, :populate, use_route: :spree, format: :json @@ -56,7 +56,7 @@ describe CartController, type: :controller do end end - it "extracts variant ids from the populator" do + it "extracts variant ids from the cart service" do variants_h = [{:variant_id=>"900", :quantity=>2, :max_quantity=>nil}, {:variant_id=>"940", :quantity=>3, :max_quantity=>3}] @@ -80,23 +80,23 @@ describe CartController, type: :controller do end it "returns HTTP success when successful" do - Spree::OrderPopulator.stub(:new).and_return(populator = double()) - populator.stub(:populate) { true } - populator.stub(:variants_h) { {} } + CartService.stub(:new).and_return(cart_service = double()) + cart_service.stub(:populate) { true } + cart_service.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 + CartService.stub(:new).and_return(cart_service = double()) + cart_service.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) + it "tells cart_service to overwrite" do + CartService.stub(:new).and_return(cart_service = double()) + cart_service.should_receive(:populate).with({}, true) xhr :post, :populate, use_route: :spree, format: :json end end diff --git a/spec/models/product_distribution_spec.rb b/spec/models/product_distribution_spec.rb index 9577ea326f..bf53b0b721 100644 --- a/spec/models/product_distribution_spec.rb +++ b/spec/models/product_distribution_spec.rb @@ -36,8 +36,8 @@ describe ProductDistribution do # When I add the product to the order, an adjustment should be made expect do - op = Spree::OrderPopulator.new order, 'AU' - op.populate products: {product.id => product.master.id}, quantity: 1, distributor_id: distributor.id + cart_service = CartService.new order, 'AU' + cart_service.populate products: {product.id => product.master.id}, quantity: 1, distributor_id: distributor.id # Normally the controller would fire this event when the order's contents are changed fire_order_contents_changed_event(order.user, order) diff --git a/spec/models/spree/order_populator_spec.rb b/spec/models/spree/order_populator_spec.rb deleted file mode 100644 index c0430ba69a..0000000000 --- a/spec/models/spree/order_populator_spec.rb +++ /dev/null @@ -1,312 +0,0 @@ -require 'spec_helper' - -module Spree - describe OrderPopulator do - let(:order) { double(:order, id: 123) } - let(:currency) { double(:currency) } - let(:params) { {} } - let(:distributor) { double(:distributor) } - let(:order_cycle) { double(:order_cycle) } - let(:op) { OrderPopulator.new(order, currency) } - - context "end-to-end" do - let(:order) { create(:order, distributor: distributor, order_cycle: order_cycle) } - let(:distributor) { create(:distributor_enterprise) } - let(:order_cycle) { create(:simple_order_cycle, distributors: [distributor], variants: [v]) } - let(:op) { OrderPopulator.new(order, nil) } - let(:v) { create(:variant) } - - describe "populate" do - it "adds a variant" do - op.populate({variants: {v.id.to_s => {quantity: '1', max_quantity: '2'}}}, true) - li = order.find_line_item_by_variant(v) - li.should be - li.quantity.should == 1 - li.max_quantity.should == 2 - li.final_weight_volume.should == 1.0 - end - - it "updates a variant's quantity, max quantity and final_weight_volume" do - order.add_variant v, 1, 2 - - op.populate({variants: {v.id.to_s => {quantity: '2', max_quantity: '3'}}}, true) - li = order.find_line_item_by_variant(v) - li.should be - li.quantity.should == 2 - li.max_quantity.should == 3 - li.final_weight_volume.should == 2.0 - end - - it "removes a variant" do - order.add_variant v, 1, 2 - - op.populate({variants: {}}, true) - order.line_items(:reload) - li = order.find_line_item_by_variant(v) - li.should_not be - end - end - end - - describe "populate" do - before do - op.should_receive(:distributor_and_order_cycle). - and_return([distributor, order_cycle]) - end - - it "checks that distribution can supply all products in the cart" do - op.should_receive(:distribution_can_supply_products_in_cart). - with(distributor, order_cycle).and_return(false) - - op.populate(params).should be false - op.errors.to_a.should == ["That distributor or order cycle can't supply all the products in your cart. Please choose another."] - end - - it "locks the order" do - op.stub(:distribution_can_supply_products_in_cart).and_return(true) - order.should_receive(:with_lock) - op.populate(params, true) - end - - it "attempts cart add with max_quantity" do - op.stub(:distribution_can_supply_products_in_cart).and_return true - params = {variants: {"1" => {quantity: 1, max_quantity: 2}}} - order.stub(:with_lock).and_yield - op.stub(:varies_from_cart) { true } - op.stub(:variants_removed) { [] } - op.should_receive(:attempt_cart_add).with("1", 1, 2).and_return true - op.populate(params, true) - end - end - - describe "varies_from_cart" do - let(:variant) { double(:variant, id: 123) } - - it "returns true when item is not in cart and a quantity is specified" do - op.should_receive(:line_item_for_variant_id).with(variant.id).and_return(nil) - op.send(:varies_from_cart, {variant_id: variant.id, quantity: '2'}).should be true - end - - it "returns true when item is not in cart and a max_quantity is specified" do - op.should_receive(:line_item_for_variant_id).with(variant.id).and_return(nil) - op.send(:varies_from_cart, {variant_id: variant.id, quantity: '0', max_quantity: '2'}).should be true - end - - it "returns false when item is not in cart and no quantity or max_quantity are specified" do - op.should_receive(:line_item_for_variant_id).with(variant.id).and_return(nil) - op.send(:varies_from_cart, {variant_id: variant.id, quantity: '0'}).should be false - end - - it "returns true when quantity varies" do - li = double(:line_item, quantity: 1, max_quantity: nil) - op.stub(:line_item_for_variant_id) { li } - - op.send(:varies_from_cart, {variant_id: variant.id, quantity: '2'}).should be true - end - - it "returns true when max_quantity varies" do - li = double(:line_item, quantity: 1, max_quantity: nil) - op.stub(:line_item_for_variant_id) { li } - - op.send(:varies_from_cart, {variant_id: variant.id, quantity: '1', max_quantity: '3'}).should be true - end - - it "returns false when max_quantity varies only in nil vs 0" do - li = double(:line_item, quantity: 1, max_quantity: nil) - op.stub(:line_item_for_variant_id) { li } - - op.send(:varies_from_cart, {variant_id: variant.id, quantity: '1'}).should be false - end - - it "returns false when both are specified and neither varies" do - li = double(:line_item, quantity: 1, max_quantity: 2) - op.stub(:line_item_for_variant_id) { li } - - op.send(:varies_from_cart, {variant_id: variant.id, quantity: '1', max_quantity: '2'}).should be false - end - end - - describe "variants_removed" do - it "returns the variant ids when one is in the cart but not in those given" do - op.stub(:variant_ids_in_cart) { [123] } - op.send(:variants_removed, []).should == [123] - end - - it "returns nothing when all items in the cart are provided" do - op.stub(:variant_ids_in_cart) { [123] } - op.send(:variants_removed, [{variant_id: '123'}]).should == [] - end - - it "returns nothing when items are added to cart" do - op.stub(:variant_ids_in_cart) { [123] } - op.send(:variants_removed, [{variant_id: '123'}, {variant_id: '456'}]).should == [] - end - - it "does not return duplicates" do - op.stub(:variant_ids_in_cart) { [123, 123] } - op.send(:variants_removed, []).should == [123] - end - end - - describe "attempt_cart_add" do - let(:variant) { double(:variant, on_hand: 250) } - let(:quantity) { 123 } - - before do - Spree::Variant.stub(:find).and_return(variant) - VariantOverride.stub(:for).and_return(nil) - end - - it "performs additional validations" do - op.should_receive(:check_order_cycle_provided_for).with(variant).and_return(true) - op.should_receive(:check_variant_available_under_distribution).with(variant). - and_return(true) - order.should_receive(:add_variant).with(variant, quantity, nil, currency) - - op.attempt_cart_add(333, quantity.to_s) - end - - it "filters quantities through #quantities_to_add" do - op.should_receive(:quantities_to_add).with(variant, 123, 123). - and_return([5, 5]) - - op.stub(:check_order_cycle_provided_for) { true } - op.stub(:check_variant_available_under_distribution) { true } - - order.should_receive(:add_variant).with(variant, 5, 5, currency) - - op.attempt_cart_add(333, quantity.to_s, quantity.to_s) - end - - it "removes variants which have become out of stock" do - op.should_receive(:quantities_to_add).with(variant, 123, 123). - and_return([0, 0]) - - op.stub(:check_order_cycle_provided_for) { true } - op.stub(:check_variant_available_under_distribution) { true } - - order.should_receive(:remove_variant).with(variant) - order.should_receive(:add_variant).never - - op.attempt_cart_add(333, quantity.to_s, quantity.to_s) - end - end - - describe "quantities_to_add" do - let(:v) { double(:variant, on_hand: 10) } - - context "when backorders are not allowed" do - context "when max_quantity is not provided" do - it "returns full amount when available" do - op.quantities_to_add(v, 5, nil).should == [5, nil] - end - - it "returns a limited amount when not entirely available" do - op.quantities_to_add(v, 15, nil).should == [10, nil] - end - end - - context "when max_quantity is provided" do - it "returns full amount when available" do - op.quantities_to_add(v, 5, 6).should == [5, 6] - end - - it "also returns the full amount when not entirely available" do - op.quantities_to_add(v, 15, 16).should == [10, 16] - end - end - end - - context "when backorders are allowed" do - before do - Spree::Config.allow_backorders = true - end - - it "does not limit quantity" do - op.quantities_to_add(v, 15, nil).should == [15, nil] - end - - it "does not limit max_quantity" do - op.quantities_to_add(v, 15, 16).should == [15, 16] - end - end - end - - describe "validations" do - describe "determining if distributor can supply products in cart" do - it "delegates to DistributionChangeValidator" do - dcv = double(:dcv) - dcv.should_receive(:can_change_to_distribution?).with(distributor, order_cycle).and_return(true) - DistributionChangeValidator.should_receive(:new).with(order).and_return(dcv) - op.send(:distribution_can_supply_products_in_cart, distributor, order_cycle).should be true - end - end - - describe "checking order cycle is provided for a variant, OR is not needed" do - let(:variant) { double(:variant) } - - it "returns false and errors when order cycle is not provided and is required" do - op.stub(:order_cycle_required_for).and_return true - op.send(:check_order_cycle_provided_for, variant).should be false - op.errors.to_a.should == ["Please choose an order cycle for this order."] - end - it "returns true when order cycle is provided" do - op.stub(:order_cycle_required_for).and_return true - op.instance_variable_set :@order_cycle, double(:order_cycle) - op.send(:check_order_cycle_provided_for, variant).should be true - end - it "returns true when order cycle is not required" do - op.stub(:order_cycle_required_for).and_return false - op.send(:check_order_cycle_provided_for, variant).should be true - end - end - - describe "checking variant is available under the distributor" do - let(:product) { double(:product) } - let(:variant) { double(:variant, product: product) } - - it "delegates to DistributionChangeValidator, returning true when available" do - dcv = double(:dcv) - dcv.should_receive(:variants_available_for_distribution).with(123, 234).and_return([variant]) - DistributionChangeValidator.should_receive(:new).with(order).and_return(dcv) - op.instance_eval { @distributor = 123; @order_cycle = 234 } - op.send(:check_variant_available_under_distribution, variant).should be true - op.errors.should be_empty - end - - it "delegates to DistributionChangeValidator, returning false and erroring otherwise" do - dcv = double(:dcv) - dcv.should_receive(:variants_available_for_distribution).with(123, 234).and_return([]) - DistributionChangeValidator.should_receive(:new).with(order).and_return(dcv) - op.instance_eval { @distributor = 123; @order_cycle = 234 } - op.send(:check_variant_available_under_distribution, variant).should be false - op.errors.to_a.should == ["That product is not available from the chosen distributor or order cycle."] - end - end - end - - - describe "support" do - describe "checking if order cycle is required for a variant" do - it "requires an order cycle when the product has no product distributions" do - product = double(:product, product_distributions: []) - variant = double(:variant, product: product) - op.send(:order_cycle_required_for, variant).should be true - end - - it "does not require an order cycle when the product has product distributions" do - product = double(:product, product_distributions: [1]) - variant = double(:variant, product: product) - op.send(:order_cycle_required_for, variant).should be false - end - end - - it "provides the distributor and order cycle for the order" do - order.should_receive(:distributor).and_return(distributor) - order.should_receive(:order_cycle).and_return(order_cycle) - op.send(:distributor_and_order_cycle).should == [distributor, - order_cycle] - end - end - end -end diff --git a/spec/services/cart_service_spec.rb b/spec/services/cart_service_spec.rb new file mode 100644 index 0000000000..caa8afe2a5 --- /dev/null +++ b/spec/services/cart_service_spec.rb @@ -0,0 +1,310 @@ +require 'spec_helper' + +describe CartService do + let(:order) { double(:order, id: 123) } + let(:currency) { double(:currency) } + let(:params) { {} } + let(:distributor) { double(:distributor) } + let(:order_cycle) { double(:order_cycle) } + let(:cart_service) { CartService.new(order, currency) } + + context "end-to-end" do + let(:order) { create(:order, distributor: distributor, order_cycle: order_cycle) } + let(:distributor) { create(:distributor_enterprise) } + let(:order_cycle) { create(:simple_order_cycle, distributors: [distributor], variants: [v]) } + let(:cart_service) { CartService.new(order, nil) } + let(:v) { create(:variant) } + + describe "populate" do + it "adds a variant" do + cart_service.populate({variants: {v.id.to_s => {quantity: '1', max_quantity: '2'}}}, true) + li = order.find_line_item_by_variant(v) + li.should be + li.quantity.should == 1 + li.max_quantity.should == 2 + li.final_weight_volume.should == 1.0 + end + + it "updates a variant's quantity, max quantity and final_weight_volume" do + order.add_variant v, 1, 2 + + cart_service.populate({variants: {v.id.to_s => {quantity: '2', max_quantity: '3'}}}, true) + li = order.find_line_item_by_variant(v) + li.should be + li.quantity.should == 2 + li.max_quantity.should == 3 + li.final_weight_volume.should == 2.0 + end + + it "removes a variant" do + order.add_variant v, 1, 2 + + cart_service.populate({variants: {}}, true) + order.line_items(:reload) + li = order.find_line_item_by_variant(v) + li.should_not be + end + end + end + + describe "populate" do + before do + cart_service.should_receive(:distributor_and_order_cycle). + and_return([distributor, order_cycle]) + end + + it "checks that distribution can supply all products in the cart" do + cart_service.should_receive(:distribution_can_supply_products_in_cart). + with(distributor, order_cycle).and_return(false) + + cart_service.populate(params).should be false + cart_service.errors.to_a.should == ["That distributor or order cycle can't supply all the products in your cart. Please choose another."] + end + + it "locks the order" do + cart_service.stub(:distribution_can_supply_products_in_cart).and_return(true) + order.should_receive(:with_lock) + cart_service.populate(params, true) + end + + it "attempts cart add with max_quantity" do + cart_service.stub(:distribution_can_supply_products_in_cart).and_return true + params = {variants: {"1" => {quantity: 1, max_quantity: 2}}} + order.stub(:with_lock).and_yield + cart_service.stub(:varies_from_cart) { true } + cart_service.stub(:variants_removed) { [] } + cart_service.should_receive(:attempt_cart_add).with("1", 1, 2).and_return true + cart_service.populate(params, true) + end + end + + describe "varies_from_cart" do + let(:variant) { double(:variant, id: 123) } + + it "returns true when item is not in cart and a quantity is specified" do + cart_service.should_receive(:line_item_for_variant_id).with(variant.id).and_return(nil) + cart_service.send(:varies_from_cart, {variant_id: variant.id, quantity: '2'}).should be true + end + + it "returns true when item is not in cart and a max_quantity is specified" do + cart_service.should_receive(:line_item_for_variant_id).with(variant.id).and_return(nil) + cart_service.send(:varies_from_cart, {variant_id: variant.id, quantity: '0', max_quantity: '2'}).should be true + end + + it "returns false when item is not in cart and no quantity or max_quantity are specified" do + cart_service.should_receive(:line_item_for_variant_id).with(variant.id).and_return(nil) + cart_service.send(:varies_from_cart, {variant_id: variant.id, quantity: '0'}).should be false + end + + it "returns true when quantity varies" do + li = double(:line_item, quantity: 1, max_quantity: nil) + cart_service.stub(:line_item_for_variant_id) { li } + + cart_service.send(:varies_from_cart, {variant_id: variant.id, quantity: '2'}).should be true + end + + it "returns true when max_quantity varies" do + li = double(:line_item, quantity: 1, max_quantity: nil) + cart_service.stub(:line_item_for_variant_id) { li } + + cart_service.send(:varies_from_cart, {variant_id: variant.id, quantity: '1', max_quantity: '3'}).should be true + end + + it "returns false when max_quantity varies only in nil vs 0" do + li = double(:line_item, quantity: 1, max_quantity: nil) + cart_service.stub(:line_item_for_variant_id) { li } + + cart_service.send(:varies_from_cart, {variant_id: variant.id, quantity: '1'}).should be false + end + + it "returns false when both are specified and neither varies" do + li = double(:line_item, quantity: 1, max_quantity: 2) + cart_service.stub(:line_item_for_variant_id) { li } + + cart_service.send(:varies_from_cart, {variant_id: variant.id, quantity: '1', max_quantity: '2'}).should be false + end + end + + describe "variants_removed" do + it "returns the variant ids when one is in the cart but not in those given" do + cart_service.stub(:variant_ids_in_cart) { [123] } + cart_service.send(:variants_removed, []).should == [123] + end + + it "returns nothing when all items in the cart are provided" do + cart_service.stub(:variant_ids_in_cart) { [123] } + cart_service.send(:variants_removed, [{variant_id: '123'}]).should == [] + end + + it "returns nothing when items are added to cart" do + cart_service.stub(:variant_ids_in_cart) { [123] } + cart_service.send(:variants_removed, [{variant_id: '123'}, {variant_id: '456'}]).should == [] + end + + it "does not return duplicates" do + cart_service.stub(:variant_ids_in_cart) { [123, 123] } + cart_service.send(:variants_removed, []).should == [123] + end + end + + describe "attempt_cart_add" do + let(:variant) { double(:variant, on_hand: 250) } + let(:quantity) { 123 } + + before do + Spree::Variant.stub(:find).and_return(variant) + VariantOverride.stub(:for).and_return(nil) + end + + it "performs additional validations" do + cart_service.should_receive(:check_order_cycle_provided_for).with(variant).and_return(true) + cart_service.should_receive(:check_variant_available_under_distribution).with(variant). + and_return(true) + order.should_receive(:add_variant).with(variant, quantity, nil, currency) + + cart_service.attempt_cart_add(333, quantity.to_s) + end + + it "filters quantities through #quantities_to_add" do + cart_service.should_receive(:quantities_to_add).with(variant, 123, 123). + and_return([5, 5]) + + cart_service.stub(:check_order_cycle_provided_for) { true } + cart_service.stub(:check_variant_available_under_distribution) { true } + + order.should_receive(:add_variant).with(variant, 5, 5, currency) + + cart_service.attempt_cart_add(333, quantity.to_s, quantity.to_s) + end + + it "removes variants which have become out of stock" do + cart_service.should_receive(:quantities_to_add).with(variant, 123, 123). + and_return([0, 0]) + + cart_service.stub(:check_order_cycle_provided_for) { true } + cart_service.stub(:check_variant_available_under_distribution) { true } + + order.should_receive(:remove_variant).with(variant) + order.should_receive(:add_variant).never + + cart_service.attempt_cart_add(333, quantity.to_s, quantity.to_s) + end + end + + describe "quantities_to_add" do + let(:v) { double(:variant, on_hand: 10) } + + context "when backorders are not allowed" do + context "when max_quantity is not provided" do + it "returns full amount when available" do + cart_service.quantities_to_add(v, 5, nil).should == [5, nil] + end + + it "returns a limited amount when not entirely available" do + cart_service.quantities_to_add(v, 15, nil).should == [10, nil] + end + end + + context "when max_quantity is provided" do + it "returns full amount when available" do + cart_service.quantities_to_add(v, 5, 6).should == [5, 6] + end + + it "also returns the full amount when not entirely available" do + cart_service.quantities_to_add(v, 15, 16).should == [10, 16] + end + end + end + + context "when backorders are allowed" do + before do + Spree::Config.allow_backorders = true + end + + it "does not limit quantity" do + cart_service.quantities_to_add(v, 15, nil).should == [15, nil] + end + + it "does not limit max_quantity" do + cart_service.quantities_to_add(v, 15, 16).should == [15, 16] + end + end + end + + describe "validations" do + describe "determining if distributor can supply products in cart" do + it "delegates to DistributionChangeValidator" do + dcv = double(:dcv) + dcv.should_receive(:can_change_to_distribution?).with(distributor, order_cycle).and_return(true) + DistributionChangeValidator.should_receive(:new).with(order).and_return(dcv) + cart_service.send(:distribution_can_supply_products_in_cart, distributor, order_cycle).should be true + end + end + + describe "checking order cycle is provided for a variant, OR is not needed" do + let(:variant) { double(:variant) } + + it "returns false and errors when order cycle is not provided and is required" do + cart_service.stub(:order_cycle_required_for).and_return true + cart_service.send(:check_order_cycle_provided_for, variant).should be false + cart_service.errors.to_a.should == ["Please choose an order cycle for this order."] + end + it "returns true when order cycle is provided" do + cart_service.stub(:order_cycle_required_for).and_return true + cart_service.instance_variable_set :@order_cycle, double(:order_cycle) + cart_service.send(:check_order_cycle_provided_for, variant).should be true + end + it "returns true when order cycle is not required" do + cart_service.stub(:order_cycle_required_for).and_return false + cart_service.send(:check_order_cycle_provided_for, variant).should be true + end + end + + describe "checking variant is available under the distributor" do + let(:product) { double(:product) } + let(:variant) { double(:variant, product: product) } + + it "delegates to DistributionChangeValidator, returning true when available" do + dcv = double(:dcv) + dcv.should_receive(:variants_available_for_distribution).with(123, 234).and_return([variant]) + DistributionChangeValidator.should_receive(:new).with(order).and_return(dcv) + cart_service.instance_eval { @distributor = 123; @order_cycle = 234 } + cart_service.send(:check_variant_available_under_distribution, variant).should be true + cart_service.errors.should be_empty + end + + it "delegates to DistributionChangeValidator, returning false and erroring otherwise" do + dcv = double(:dcv) + dcv.should_receive(:variants_available_for_distribution).with(123, 234).and_return([]) + DistributionChangeValidator.should_receive(:new).with(order).and_return(dcv) + cart_service.instance_eval { @distributor = 123; @order_cycle = 234 } + cart_service.send(:check_variant_available_under_distribution, variant).should be false + cart_service.errors.to_a.should == ["That product is not available from the chosen distributor or order cycle."] + end + end + end + + + describe "support" do + describe "checking if order cycle is required for a variant" do + it "requires an order cycle when the product has no product distributions" do + product = double(:product, product_distributions: []) + variant = double(:variant, product: product) + cart_service.send(:order_cycle_required_for, variant).should be true + end + + it "does not require an order cycle when the product has product distributions" do + product = double(:product, product_distributions: [1]) + variant = double(:variant, product: product) + cart_service.send(:order_cycle_required_for, variant).should be false + end + end + + it "provides the distributor and order cycle for the order" do + order.should_receive(:distributor).and_return(distributor) + order.should_receive(:order_cycle).and_return(order_cycle) + cart_service.send(:distributor_and_order_cycle).should == [distributor, + order_cycle] + end + end +end diff --git a/spec/support/request/shop_workflow.rb b/spec/support/request/shop_workflow.rb index 3fc7ac53a6..5f3882da26 100644 --- a/spec/support/request/shop_workflow.rb +++ b/spec/support/request/shop_workflow.rb @@ -17,8 +17,8 @@ module ShopWorkflow end def add_product_to_cart(order, product, quantity: 1) - populator = Spree::OrderPopulator.new(order, order.currency) - populator.populate(variants: {product.variants.first.id => quantity}) + cart_service = CartService.new(order, order.currency) + cart_service.populate(variants: {product.variants.first.id => quantity}) # Recalculate fee totals order.update_distribution_charge!