diff --git a/app/services/shop_products_service.rb b/app/services/shop_products_service.rb new file mode 100644 index 0000000000..85497356f7 --- /dev/null +++ b/app/services/shop_products_service.rb @@ -0,0 +1,70 @@ +# Returns a (paginatable) AR object for the products in stock for a given distributor and OC. +# The stock-checking includes on_demand and stock level overrides from variant_overrides. + +class ShopProductsService + def initialize(distributor, order_cycle) + @distributor = distributor + @order_cycle = order_cycle + end + + def relation + Spree::Product.where(id: distributed_products) + end + + private + + def distributed_products + @order_cycle. + variants_distributed_by(@distributor). + merge(stocked_variants_and_overrides). + select("DISTINCT spree_variants.product_id") + end + + def stocked_variants_and_overrides + Spree::Variant. + joins("LEFT OUTER JOIN variant_overrides ON variant_overrides.variant_id = spree_variants.id + AND variant_overrides.hub_id = #{@distributor.id}"). + joins(:stock_items). + where(query_stock_with_overrides) + end + + def query_stock_with_overrides + "( #{variant_not_overriden} AND ( #{variant_on_demand} OR #{variant_in_stock} ) ) + OR ( #{variant_overriden} AND ( #{override_on_demand} OR #{override_in_stock} ) ) + OR ( #{variant_overriden} AND ( #{override_on_demand_null} AND #{variant_on_demand} ) ) + OR ( #{variant_overriden} AND ( #{override_on_demand_null} + AND #{variant_not_on_demand} AND #{variant_in_stock} ) )" + end + + def variant_not_overriden + "variant_overrides.id IS NULL" + end + + def variant_overriden + "variant_overrides.id IS NOT NULL" + end + + def variant_in_stock + "spree_stock_items.count_on_hand > 0" + end + + def variant_on_demand + "spree_stock_items.backorderable IS TRUE" + end + + def variant_not_on_demand + "spree_stock_items.backorderable IS FALSE" + end + + def override_on_demand + "variant_overrides.on_demand IS TRUE" + end + + def override_in_stock + "variant_overrides.count_on_hand > 0" + end + + def override_on_demand_null + "variant_overrides.on_demand IS NULL" + end +end diff --git a/lib/open_food_network/products_renderer.rb b/lib/open_food_network/products_renderer.rb index e6e3fbe460..3de2177d3b 100644 --- a/lib/open_food_network/products_renderer.rb +++ b/lib/open_food_network/products_renderer.rb @@ -32,64 +32,11 @@ module OpenFoodNetwork def load_products return unless @order_cycle - Spree::Product.where(id: distributed_products). + ShopProductsService.new(@distributor, @order_cycle).relation. order(taxon_order). each { |product| scoper.scope(product) } end - def distributed_products - @order_cycle. - variants_distributed_by(@distributor). - merge(stocked_variants_with_overrides). - select("DISTINCT spree_variants.product_id") - end - - def stocked_variants_with_overrides - Spree::Variant. - joins("LEFT OUTER JOIN variant_overrides ON variant_overrides.variant_id = spree_variants.id AND variant_overrides.hub_id = #{@distributor.id}"). - joins(:stock_items). - where(query_stock_with_overrides) - end - - def query_stock_with_overrides - "( #{variant_not_overriden} AND ( #{variant_in_stock} OR #{variant_on_demand} ) ) - OR ( #{variant_overriden} AND ( #{override_on_demand} OR #{override_in_stock} ) ) - OR ( #{variant_overriden} AND ( #{override_on_demand_null} AND #{variant_on_demand} ) ) - OR ( #{variant_overriden} AND ( #{override_on_demand_null} AND #{variant_not_on_demand} AND #{variant_in_stock} ) )" - end - - def variant_not_overriden - "variant_overrides.id IS NULL" - end - - def variant_overriden - "variant_overrides.id IS NOT NULL" - end - - def variant_in_stock - "spree_stock_items.count_on_hand > 0" - end - - def variant_on_demand - "spree_stock_items.backorderable IS TRUE" - end - - def variant_not_on_demand - "spree_stock_items.backorderable IS FALSE" - end - - def override_on_demand - "variant_overrides.on_demand IS TRUE" - end - - def override_in_stock - "variant_overrides.count_on_hand > 0" - end - - def override_on_demand_null - "variant_overrides.on_demand IS NULL" - end - def scoper ScopeProductToHub.new(@distributor) end diff --git a/spec/services/shop_products_service_spec.rb b/spec/services/shop_products_service_spec.rb new file mode 100644 index 0000000000..70840418ad --- /dev/null +++ b/spec/services/shop_products_service_spec.rb @@ -0,0 +1,68 @@ +require 'spec_helper' + +describe ShopProductsService do + let(:distributor) { create(:distributor_enterprise) } + let(:product) { create(:product) } + let(:variant) { product.variants.first } + let(:order_cycle) do + create(:simple_order_cycle, distributors: [distributor], variants: [variant]) + end + + describe "product distributed by distributor in the OC" do + it "returns products" do + expect(described_class.new(distributor, order_cycle).relation).to eq([product]) + end + end + + describe "product distributed by distributor in another OC" do + let(:reference_variant) { create(:product).variants.first } + let(:order_cycle) do + create(:simple_order_cycle, distributors: [distributor], variants: [reference_variant]) + end + let(:another_order_cycle) do + create(:simple_order_cycle, distributors: [distributor], variants: [variant]) + end + + it "does not return product" do + expect(described_class.new(distributor, order_cycle).relation).to_not include product + end + end + + describe "product distributed by another distributor in the OC" do + let(:another_distributor) { create(:distributor_enterprise) } + let(:order_cycle) do + create(:simple_order_cycle, distributors: [another_distributor], variants: [variant]) + end + + it "does not return product" do + expect(described_class.new(distributor, order_cycle).relation).to_not include product + end + end + + describe "filtering products that are out of stock" do + context "with regular variants" do + it "returns product when variant is in stock" do + expect(described_class.new(distributor, order_cycle).relation).to include product + end + + it "does not return product when variant is out of stock" do + variant.update_attribute(:on_hand, 0) + expect(described_class.new(distributor, order_cycle).relation).to_not include product + end + end + + context "with variant overrides" do + let!(:override) { create(:variant_override, hub: distributor, variant: variant, count_on_hand: 0) } + + it "does not return product when an override is out of stock" do + expect(described_class.new(distributor, order_cycle).relation).to_not include product + end + + it "returns product when an override is in stock" do + variant.update_attribute(:on_hand, 0) + override.update_attribute(:count_on_hand, 10) + expect(described_class.new(distributor, order_cycle).relation).to include product + end + end + end +end