diff --git a/app/services/order_cycle_distributed_products.rb b/app/services/order_cycle_distributed_products.rb index d2c9d3c4ca..3f8d3ddc9b 100644 --- a/app/services/order_cycle_distributed_products.rb +++ b/app/services/order_cycle_distributed_products.rb @@ -2,9 +2,10 @@ # The stock-checking includes on_demand and stock level overrides from variant_overrides. class OrderCycleDistributedProducts - def initialize(distributor, order_cycle) + def initialize(distributor, order_cycle, customer) @distributor = distributor @order_cycle = order_cycle + @customer = customer end def products_relation @@ -12,26 +13,97 @@ class OrderCycleDistributedProducts end def variants_relation - @order_cycle. - variants_distributed_by(@distributor). + order_cycle. + variants_distributed_by(distributor). merge(stocked_variants_and_overrides) end private + attr_reader :distributor, :order_cycle, :customer + def stocked_products - @order_cycle. - variants_distributed_by(@distributor). + 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. + stocked_variants = Spree::Variant. joins("LEFT OUTER JOIN variant_overrides ON variant_overrides.variant_id = spree_variants.id - AND variant_overrides.hub_id = #{@distributor.id}"). + AND variant_overrides.hub_id = #{distributor.id}"). joins(:stock_items). where(query_stock_with_overrides) + + if distributor_rules.any? + stocked_variants = apply_tag_rules(stocked_variants) + end + + stocked_variants + end + + def apply_tag_rules(stocked_variants) + stocked_variants.where(query_with_tag_rules) + end + + def distributor_rules + @distributor_rules ||= TagRule::FilterProducts.prioritised.for(distributor) + end + + def customer_tag_list + customer.andand.tag_list || [] + end + + def default_rule_tags + default_rules.map(&:preferred_variant_tags) + end + + def hide_rule_tags + hide_rules.map(&:preferred_variant_tags) + end + + def show_rule_tags + show_rules.map(&:preferred_variant_tags) + end + + def overrides_to_hide + @overrides_to_hide = VariantOverride.where(hub_id: distributor.id). + tagged_with(default_rule_tags + hide_rule_tags, any: true). + pluck(:id) + end + + def overrides_to_show + @overrides_to_show = VariantOverride.where(hub_id: distributor.id). + tagged_with(show_rule_tags, any: true). + pluck(:id) + end + + def customer_applicable_rules + # Rules which apply specifically to the current customer + @customer_applicable_rules ||= non_default_rules.select{ |rule| customer_tagged?(rule) } + end + + def default_rules + # These rules hide a variant_override with tag X + distributor_rules.select(&:is_default?) + end + + def non_default_rules + # These rules show or hide a variant_override with tag X for customer with tag Y + distributor_rules.reject(&:is_default?) + end + + def hide_rules + @hide_rules ||= customer_applicable_rules.select{ |rule| rule.preferred_matched_variants_visibility == 'hidden'} + end + + def show_rules + customer_applicable_rules - hide_rules + end + + def customer_tagged?(rule) + customer_tag_list.include? rule.preferred_customer_tags end def query_stock_with_overrides @@ -42,6 +114,22 @@ class OrderCycleDistributedProducts AND #{variant_not_on_demand} AND #{variant_in_stock} ) )" end + def query_with_tag_rules + "#{variant_not_overriden} OR ( #{variant_overriden} + AND ( #{override_not_hidden_by_rule} + OR #{override_shown_by_rule} ) )" + end + + def override_not_hidden_by_rule + return "FALSE" unless overrides_to_hide.any? + "variant_overrides.id NOT IN (#{overrides_to_hide.join(',')})" + end + + def override_shown_by_rule + return "FALSE" unless overrides_to_show.any? + "variant_overrides.id IN (#{overrides_to_show.join(',')})" + end + def variant_not_overriden "variant_overrides.id IS NULL" end diff --git a/lib/open_food_network/products_renderer.rb b/lib/open_food_network/products_renderer.rb index 32c4b90adb..bafbe27a59 100644 --- a/lib/open_food_network/products_renderer.rb +++ b/lib/open_food_network/products_renderer.rb @@ -4,9 +4,10 @@ module OpenFoodNetwork class ProductsRenderer class NoProducts < RuntimeError; end - def initialize(distributor, order_cycle, params = {}) + def initialize(distributor, order_cycle, customer, params = {}) @distributor = distributor @order_cycle = order_cycle + @customer = customer @params = params end @@ -28,7 +29,7 @@ module OpenFoodNetwork private - attr_reader :order_cycle, :distributor, :params + attr_reader :order_cycle, :distributor, :customer, :params def products return unless order_cycle @@ -44,7 +45,7 @@ module OpenFoodNetwork end def distributed_products - OrderCycleDistributedProducts.new(distributor, order_cycle) + OrderCycleDistributedProducts.new(distributor, order_cycle, customer) end def taxon_order diff --git a/spec/lib/open_food_network/products_renderer_spec.rb b/spec/lib/open_food_network/products_renderer_spec.rb index 252f26f517..dbc6d4b535 100644 --- a/spec/lib/open_food_network/products_renderer_spec.rb +++ b/spec/lib/open_food_network/products_renderer_spec.rb @@ -6,7 +6,8 @@ module OpenFoodNetwork let(:distributor) { create(:distributor_enterprise) } let(:order_cycle) { create(:simple_order_cycle, distributors: [distributor]) } let(:exchange) { order_cycle.exchanges.to_enterprises(distributor).outgoing.first } - let(:pr) { ProductsRenderer.new(distributor, order_cycle) } + let(:customer) { create(:customer) } + let(:pr) { ProductsRenderer.new(distributor, order_cycle, customer) } describe "sorting" do let(:t1) { create(:taxon) } @@ -89,7 +90,7 @@ module OpenFoodNetwork let!(:v2) { create(:variant, product: p, unit_value: 5) } # Not in exchange let!(:v3) { create(:variant, product: p, unit_value: 7, inventory_items: [create(:inventory_item, enterprise: hub, visible: true)]) } let!(:v4) { create(:variant, product: p, unit_value: 9, inventory_items: [create(:inventory_item, enterprise: hub, visible: false)]) } - let(:pr) { ProductsRenderer.new(hub, oc) } + let(:pr) { ProductsRenderer.new(hub, oc, customer) } let(:variants) { pr.send(:variants_for_shop_by_id) } it "scopes variants to distribution" do diff --git a/spec/services/order_cycle_distributed_products_spec.rb b/spec/services/order_cycle_distributed_products_spec.rb index b02baf4974..5356340c5d 100644 --- a/spec/services/order_cycle_distributed_products_spec.rb +++ b/spec/services/order_cycle_distributed_products_spec.rb @@ -5,13 +5,14 @@ describe OrderCycleDistributedProducts do let(:distributor) { create(:distributor_enterprise) } let(:product) { create(:product) } let(:variant) { product.variants.first } + let(:customer) { create(:customer) } 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).products_relation).to eq([product]) + expect(described_class.new(distributor, order_cycle, customer).products_relation).to eq([product]) end end @@ -25,7 +26,7 @@ describe OrderCycleDistributedProducts do end it "does not return product" do - expect(described_class.new(distributor, order_cycle).products_relation).to_not include product + expect(described_class.new(distributor, order_cycle, customer).products_relation).to_not include product end end @@ -36,19 +37,19 @@ describe OrderCycleDistributedProducts do end it "does not return product" do - expect(described_class.new(distributor, order_cycle).products_relation).to_not include product + expect(described_class.new(distributor, order_cycle, customer).products_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).products_relation).to include product + expect(described_class.new(distributor, order_cycle, customer).products_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).products_relation).to_not include product + expect(described_class.new(distributor, order_cycle, customer).products_relation).to_not include product end end @@ -56,13 +57,13 @@ describe OrderCycleDistributedProducts 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).products_relation).to_not include product + expect(described_class.new(distributor, order_cycle, customer).products_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).products_relation).to include product + expect(described_class.new(distributor, order_cycle, customer).products_relation).to include product end end end @@ -71,12 +72,13 @@ describe OrderCycleDistributedProducts do describe "#variants_relation" do let(:distributor) { create(:distributor_enterprise) } let(:oc) { create(:simple_order_cycle, distributors: [distributor], variants: [v1, v3]) } + let(:customer) { create(:customer) } let(:product) { create(:simple_product) } let!(:v1) { create(:variant, product: product) } let!(:v2) { create(:variant, product: product) } let!(:v3) { create(:variant, product: product) } let!(:vo) { create(:variant_override, hub: distributor, variant_id: v3.id, count_on_hand: 0) } - let(:variants) { described_class.new(distributor, oc).variants_relation } + let(:variants) { described_class.new(distributor, oc, customer).variants_relation } it "returns variants in the oc" do expect(variants).to include v1