diff --git a/app/services/variant_tag_rules_filterer.rb b/app/services/variant_tag_rules_filterer.rb new file mode 100644 index 0000000000..d9cf2582c0 --- /dev/null +++ b/app/services/variant_tag_rules_filterer.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +# Takes a Spree::Variant AR object and filters results based on applicable tag rules. +# Tag rules exists in the context of enterprise, customer, and variants. +# Returns a Spree::Variant AR object. + +# The filtering is somewhat not intuitive when they are conflicting rules in play: +# * When a variant is hidden by a default rule, It will apply the "show rule" if any +# * When there is no default rule, it will apply the "show rule" over any "hide rule" +# +class VariantTagRulesFilterer + def initialize(distributor:, customer:, variants_relation: ) + @distributor = distributor + @customer = customer + @variants_relation = variants_relation + end + + def call + return variants_relation unless distributor_rules.any? + + filter(variants_relation) + end + + private + + attr_accessor :distributor, :customer, :variants_relation + + def distributor_rules + @distributor_rules ||= TagRule::FilterVariants.for(distributor).all + end + + def filter(variants_relation) + return variants_relation unless variants_to_hide.any? + + variants_relation.where(query_with_tag_rules) + end + + def query_with_tag_rules + "#{variant_not_hidden_by_rule} OR #{variant_shown_by_rule}" + end + + def variant_not_hidden_by_rule + return "FALSE" unless variants_to_hide.any? + + "spree_variants.id NOT IN (#{variants_to_hide.join(',')})" + end + + def variant_shown_by_rule + return "FALSE" unless variants_to_show.any? + + "spree_variants.id IN (#{variants_to_show.join(',')})" + end + + def variants_to_hide + @variants_to_hide ||= Spree::Variant.where(supplier: distributor) + .tagged_with(default_rule_tags + hide_rule_tags, any: true) + .pluck(:id) + end + + def variants_to_show + @variants_to_show ||= Spree::Variant.where(supplier: distributor) + .tagged_with(show_rule_tags, any: true) + .pluck(:id) + 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 default_rules + # These rules hide a variant with tag X and apply to all customers + distributor_rules.select(&:is_default?) + end + + def non_default_rules + # These rules show or hide a variant with tag X for customer with tag Y + distributor_rules.reject(&:is_default?) + 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 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 customer_tag_list + customer&.tag_list || [] + end +end diff --git a/spec/services/variant_tag_rules_filterer_spec.rb b/spec/services/variant_tag_rules_filterer_spec.rb new file mode 100644 index 0000000000..ee74416509 --- /dev/null +++ b/spec/services/variant_tag_rules_filterer_spec.rb @@ -0,0 +1,119 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe VariantTagRulesFilterer do + subject(:filterer) { described_class.new(distributor:, customer:, variants_relation:) } + + let(:distributor) { create(:distributor_enterprise) } + let(:product) { create(:product) } + let!(:variant_hidden_by_default) { create(:variant, product:, supplier: distributor) } + let!(:variant_hidden_by_rule) { create(:variant, product:, supplier: distributor) } + let!(:variant_shown_by_rule) { create(:variant, product:, supplier: distributor) } + let!(:variant_hidden_for_another_customer) { create(:variant, product:, supplier: distributor) } + let(:customer) { create(:customer, enterprise: distributor) } + let(:variants_relation) { Spree::Variant.where(supplier: distributor) } + + describe "#call" do + let!(:hide_rule) { + create(:filter_variants_tag_rule, + enterprise: distributor, + preferred_variant_tags: "hide_these_variants", + preferred_customer_tags: "hide_from_these_customers", + preferred_matched_variants_visibility: "hidden" ) + } + let!(:show_rule) { + create(:filter_variants_tag_rule, + enterprise: distributor, + preferred_variant_tags: "show_these_variants", + preferred_customer_tags: "show_for_these_customers", + preferred_matched_variants_visibility: "visible" ) + } + let!(:non_applicable_rule) { + create(:filter_variants_tag_rule, + enterprise: distributor, + preferred_variant_tags: "hide_these_other_variants", + preferred_customer_tags: "hide_from_other_customers", + preferred_matched_variants_visibility: "hidden" ) + } + + context "when the distributor has no rules" do + it "returns the relation unchanged" do + expect(filterer.call).to eq variants_relation + end + end + + context "with hide rule" do + it "hides the variant matching the rule" do + customer.update_attribute(:tag_list, hide_rule.preferred_customer_tags) + variant_hidden_by_rule.update_attribute(:tag_list, hide_rule.preferred_variant_tags) + + expect(filterer.call).not_to include(variant_hidden_by_rule) + end + + context "with mutiple conflicting rules" do + it "applies the show rule" do + # Customer has show rule tag and hide rule tag + customer.update_attribute( + :tag_list, [hide_rule.preferred_customer_tags, show_rule.preferred_customer_tags] + ) + # Variant has show rule tag and hide rule tag + variant_hidden_by_rule.update_attribute( + :tag_list, [hide_rule.preferred_variant_tags, show_rule.preferred_variant_tags,] + ) + expect(filterer.call).to include(variant_hidden_by_rule) + end + end + end + + context "with variant hidden by default" do + let(:default_hide_rule) { + create(:filter_variants_tag_rule, + enterprise: distributor, + is_default: true, + preferred_variant_tags: "hide_these_variants_from_everyone", + preferred_matched_variants_visibility: "hidden") + } + + before do + variant_hidden_by_default.update_attribute( + :tag_list, default_hide_rule.preferred_variant_tags + ) + end + + it "excludes variant hidden by default" do + expect(filterer.call).not_to include(variant_hidden_by_default) + end + + context "with variant rule overriding default rule" do + it "includes variant hidden by default" do + customer.update_attribute(:tag_list, show_rule.preferred_customer_tags) + # Variant has default rule tag and show rule tag + variant_hidden_by_default.update_attribute( + :tag_list, [default_hide_rule.preferred_variant_tags, show_rule.preferred_variant_tags] + ) + + expect(filterer.call).to include(variant_hidden_by_default) + end + + context "with mutiple conflicting rules applying to same variant" do + it "applies the show rule" do + # customer has show rule and hide rule tag + customer.update_attribute( + :tag_list, [show_rule.preferred_customer_tags, hide_rule.preferred_customer_tags] + ) + + # Variant has default rule tag and show rule tag and hide rule tag + variant_hidden_by_default.update_attribute( + :tag_list, + [default_hide_rule.preferred_variant_tags, show_rule.preferred_variant_tags, + hide_rule.preferred_variant_tags] + ) + + expect(filterer.call).to include(variant_hidden_by_default) + end + end + end + end + end +end