diff --git a/app/models/spree/stock/quantifier.rb b/app/models/spree/stock/quantifier.rb index 8c437dc580..21f1750ca0 100644 --- a/app/models/spree/stock/quantifier.rb +++ b/app/models/spree/stock/quantifier.rb @@ -11,6 +11,10 @@ module Spree end def total_on_hand + # Associated stock_items no longer exist if the variant has been soft-deleted. A variant + # may still be in an active cart after it's deleted, so this will mark it as out of stock. + return 0 if @variant.deleted? + stock_items.sum(&:count_on_hand) end diff --git a/app/services/cart_service.rb b/app/services/cart_service.rb index 5818f8f91f..9df85cfe45 100644 --- a/app/services/cart_service.rb +++ b/app/services/cart_service.rb @@ -29,11 +29,15 @@ class CartService variants_data.each do |variant_data| loaded_variant = loaded_variants[variant_data[:variant_id].to_i] + + if loaded_variant.deleted? + remove_deleted_variant(loaded_variant) + next + end + next unless varies_from_cart(variant_data, loaded_variant) - attempt_cart_add( - loaded_variant, variant_data[:quantity], variant_data[:max_quantity] - ) + attempt_cart_add(loaded_variant, variant_data[:quantity], variant_data[:max_quantity]) end end @@ -41,12 +45,16 @@ class CartService @indexed_variants ||= begin variant_ids_in_data = variants_data.map{ |v| v[:variant_id] } - Spree::Variant.where(id: variant_ids_in_data). + Spree::Variant.with_deleted.where(id: variant_ids_in_data). includes(:default_price, :stock_items, :product). index_by(&:id) end end + def remove_deleted_variant(variant) + line_item_for_variant(variant).andand.destroy + end + def attempt_cart_add(variant, quantity, max_quantity = nil) quantity = quantity.to_i max_quantity = max_quantity.to_i if max_quantity diff --git a/app/services/variants_stock_levels.rb b/app/services/variants_stock_levels.rb index 0d778b9a0d..a72abc8ffb 100644 --- a/app/services/variants_stock_levels.rb +++ b/app/services/variants_stock_levels.rb @@ -8,7 +8,7 @@ class VariantsStockLevels variant_stock_levels = variant_stock_levels(order.line_items.includes(variant: :stock_items)) order_variant_ids = variant_stock_levels.keys - missing_variants = Spree::Variant.includes(:stock_items). + missing_variants = Spree::Variant.with_deleted.includes(:stock_items). where(id: (requested_variant_ids - order_variant_ids)) missing_variants.each do |missing_variant| diff --git a/spec/features/consumer/shopping/shopping_spec.rb b/spec/features/consumer/shopping/shopping_spec.rb index 432f750f17..429f4ba3d3 100644 --- a/spec/features/consumer/shopping/shopping_spec.rb +++ b/spec/features/consumer/shopping/shopping_spec.rb @@ -429,6 +429,34 @@ feature "As a consumer I want to shop with a distributor", js: true do end end end + + context "when a variant is soft-deleted" do + describe "adding the soft-deleted variant to the cart" do + it "handles it as if the variant has gone out of stock" do + variant.delete + + fill_in "variants[#{variant.id}]", with: '1' + + expect_out_of_stock_behavior + end + end + + context "when the soft-deleted variant has an associated override" do + describe "adding the soft-deleted variant to the cart" do + let!(:variant_override) { + create(:variant_override, variant: variant, hub: distributor, count_on_hand: 100) + } + + it "handles it as if the variant has gone out of stock" do + variant.delete + + fill_in "variants[#{variant.id}]", with: '1' + + expect_out_of_stock_behavior + end + end + end + end end context "when no order cycles are available" do @@ -543,4 +571,24 @@ feature "As a consumer I want to shop with a distributor", js: true do # waiting period before submitting the data... sleep 0.6 end + + def expect_out_of_stock_behavior + wait_for_debounce + wait_until { !cart_dirty } + + # Shows an "out of stock" modal, with helpful user feedback + within(".out-of-stock-modal") do + expect(page).to have_content I18n.t('js.out_of_stock.out_of_stock_text') + end + + # Removes the item from the client-side cart and marks the variant as unavailable + expect(page).to have_field "variants[#{variant.id}]", with: '0', disabled: true + expect(page).to have_selector "#variant-#{variant.id}.out-of-stock" + expect(page).to have_selector "#variants_#{variant.id}[ofn-on-hand='0']" + expect(page).to have_selector "#variants_#{variant.id}[disabled='disabled']" + + # We need to wait again for the cart to finish updating in Angular or the test can fail + # as the session cannot be reset properly (after the test) while it's still loading + wait_until { !cart_dirty } + end end diff --git a/spec/models/order_cycle_spec.rb b/spec/models/order_cycle_spec.rb index 42fc875911..22cfeb2b70 100644 --- a/spec/models/order_cycle_spec.rb +++ b/spec/models/order_cycle_spec.rb @@ -227,6 +227,14 @@ describe OrderCycle do expect(oc.variants_distributed_by(d2)).not_to include p1_v_hidden, p1_v_deleted expect(oc.variants_distributed_by(d1)).to include p2_v end + + context "with soft-deleted variants" do + it "does not consider soft-deleted variants to be currently distributed in the oc" do + p2_v.delete + + expect(oc.variants_distributed_by(d1)).to_not include p2_v + end + end end context "when hub prefers product selection from inventory only" do diff --git a/spec/models/spree/stock/quantifier_spec.rb b/spec/models/spree/stock/quantifier_spec.rb new file mode 100644 index 0000000000..2a06f74344 --- /dev/null +++ b/spec/models/spree/stock/quantifier_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'spec_helper' + +module Spree + module Stock + describe Quantifier do + let(:quantifier) { Spree::Stock::Quantifier.new(variant) } + let(:variant) { create(:variant, on_hand: 99) } + + describe "#total_on_hand" do + context "with a soft-deleted variant" do + before do + variant.delete + end + + it "returns zero stock for the variant" do + expect(quantifier.total_on_hand).to eq 0 + end + end + end + end + end +end diff --git a/spec/models/variant_override_spec.rb b/spec/models/variant_override_spec.rb index 4c5e5e71af..22b93a06d4 100644 --- a/spec/models/variant_override_spec.rb +++ b/spec/models/variant_override_spec.rb @@ -32,6 +32,11 @@ describe VariantOverride do expect(VariantOverride.indexed(hub1)).to eq( variant => vo1 ) expect(VariantOverride.indexed(hub2)).to eq( variant => vo2 ) end + + it "does not include overrides for soft-deleted variants" do + variant.delete + expect(VariantOverride.indexed(hub1)).to eq( nil => vo1 ) + end end end diff --git a/spec/services/cart_service_spec.rb b/spec/services/cart_service_spec.rb index 0baa3abe26..397014e08a 100644 --- a/spec/services/cart_service_spec.rb +++ b/spec/services/cart_service_spec.rb @@ -15,14 +15,18 @@ describe CartService do 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(:order_cycle) { create(:simple_order_cycle, distributors: [distributor], + variants: [variant]) } let(:cart_service) { CartService.new(order) } - let(:v) { create(:variant) } + let(:variant) { create(:variant) } - describe "populate" do + 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) + cart_service.populate( + { variants: { variant.id.to_s => { quantity: '1', max_quantity: '2' } } }, + true + ) + li = order.find_line_item_by_variant(variant) expect(li).to be expect(li.quantity).to eq(1) expect(li.max_quantity).to eq(2) @@ -30,10 +34,13 @@ describe CartService do end it "updates a variant's quantity, max quantity and final_weight_volume" do - order.add_variant v, 1, 2 + order.add_variant variant, 1, 2 - cart_service.populate({ variants: { v.id.to_s => { quantity: '2', max_quantity: '3' } } }, true) - li = order.find_line_item_by_variant(v) + cart_service.populate( + { variants: { variant.id.to_s => { quantity: '2', max_quantity: '3' } } }, + true + ) + li = order.find_line_item_by_variant(variant) expect(li).to be expect(li.quantity).to eq(2) expect(li.max_quantity).to eq(3) @@ -41,13 +48,43 @@ describe CartService do end it "removes a variant" do - order.add_variant v, 1, 2 + order.add_variant variant, 1, 2 cart_service.populate({ variants: {} }, true) order.line_items(:reload) - li = order.find_line_item_by_variant(v) + li = order.find_line_item_by_variant(variant) expect(li).not_to be end + + context "when a variant has been soft-deleted" do + let(:relevant_line_item) { order.reload.find_line_item_by_variant(variant) } + + describe "when the soft-deleted variant is not in the cart yet" do + it "does not add the deleted variant to the cart" do + variant.delete + + cart_service.populate({ variants: { variant.id.to_s => { quantity: '2' } } }, true) + + expect(relevant_line_item).to be_nil + expect(cart_service.errors.count).to be 0 + end + end + + describe "when the soft-deleted variant is already in the cart" do + let!(:existing_line_item) { + create(:line_item, variant: variant, quantity: 2, order: order) + } + + it "removes the line_item from the cart" do + variant.delete + + cart_service.populate({ variants: { variant.id.to_s => { quantity: '3' } } }, true) + + expect(Spree::LineItem.where(id: relevant_line_item).first).to be_nil + expect(cart_service.errors.count).to be 0 + end + end + end end end