diff --git a/ee/app/controllers/gitlab_subscriptions/trials/duo_pro_controller.rb b/ee/app/controllers/gitlab_subscriptions/trials/duo_pro_controller.rb index e02a25f001746a446ea9a6712173d4354938413a..b0aa0b6c46a0f13c31b4136263cf992a367e8675 100644 --- a/ee/app/controllers/gitlab_subscriptions/trials/duo_pro_controller.rb +++ b/ee/app/controllers/gitlab_subscriptions/trials/duo_pro_controller.rb @@ -61,7 +61,7 @@ def create private def eligible_namespaces - @eligible_namespaces = Users::DuoProTrialEligibleNamespacesFinder.new(current_user).execute + @eligible_namespaces = Users::AddOnTrialEligibleNamespacesFinder.new(current_user, add_on: :duo_pro).execute end strong_memoize_attr :eligible_namespaces diff --git a/ee/app/finders/users/add_on_trial_eligible_namespaces_finder.rb b/ee/app/finders/users/add_on_trial_eligible_namespaces_finder.rb new file mode 100644 index 0000000000000000000000000000000000000000..34098eb95dbcdc54ddc8cf57744cfe6769d0c763 --- /dev/null +++ b/ee/app/finders/users/add_on_trial_eligible_namespaces_finder.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module Users + class AddOnTrialEligibleNamespacesFinder + def initialize(user, add_on:) + @user = user + @add_on = add_on + end + + def execute + return Namespace.none unless add_on_exists? + + items = user.owned_groups.ordered_by_name + by_add_on(items) + end + + private + + attr_reader :user, :add_on + + def by_add_on(items) + case add_on + when :duo_pro + items.not_in_default_plan.not_duo_pro_or_no_add_on + when :duo_enterprise + items.in_specific_plans(GitlabSubscriptions::DuoEnterprise::ELIGIBLE_PLANS).not_duo_enterprise_or_no_add_on + end + end + + def add_on_exists? + case add_on + when :duo_pro + GitlabSubscriptions::AddOn.code_suggestions.exists? + when :duo_enterprise + GitlabSubscriptions::AddOn.duo_enterprise.exists? + else + raise ArgumentError, "Unknown add_on: #{add_on}" + end + end + end +end diff --git a/ee/app/finders/users/duo_pro_trial_eligible_namespaces_finder.rb b/ee/app/finders/users/duo_pro_trial_eligible_namespaces_finder.rb deleted file mode 100644 index f11fb1fe3d6e4851512ac142043bb58ae5f97a21..0000000000000000000000000000000000000000 --- a/ee/app/finders/users/duo_pro_trial_eligible_namespaces_finder.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -module Users - class DuoProTrialEligibleNamespacesFinder - def initialize(user) - @user = user - end - - def execute - return Namespace.none if GitlabSubscriptions::AddOn.code_suggestions.none? - - user.owned_groups.not_in_default_plan.not_duo_pro_or_no_add_on.ordered_by_name - end - - private - - attr_reader :user - end -end diff --git a/ee/app/models/ee/namespace.rb b/ee/app/models/ee/namespace.rb index 7ab7b64bbdc5dc3c70600efe344a4841557f83f6..5c4d152e144d776084018bcb8bbbe9c9715352ab 100644 --- a/ee/app/models/ee/namespace.rb +++ b/ee/app/models/ee/namespace.rb @@ -85,6 +85,12 @@ module Namespace .allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/419988") end + scope :in_specific_plans, ->(plan_names) do + left_joins(gitlab_subscription: :hosted_plan) + .where(plans: { name: plan_names }) + .allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/419988") + end + scope :not_duo_pro_or_no_add_on, -> do # We return any namespace that does not have a duo pro add on. # We get all namespaces that do not have an add on from the left_joins and the @@ -97,6 +103,18 @@ module Namespace ).or(GitlabSubscriptions::AddOnPurchase.arel_table[:subscription_add_on_id].eq(nil))) end + scope :not_duo_enterprise_or_no_add_on, -> do + # We return any namespace that does not have a duo pro add on. + # We get all namespaces that do not have an add on from the left_joins and the + # or nil condition preserves the unmatched data what would be removed due to the not eq + # condition without it. + left_joins(:subscription_add_on_purchases) + .where( + GitlabSubscriptions::AddOnPurchase.arel_table[:subscription_add_on_id].not_eq( + GitlabSubscriptions::AddOn.duo_enterprise.pick(:id) + ).or(GitlabSubscriptions::AddOnPurchase.arel_table[:subscription_add_on_id].eq(nil))) + end + scope :eligible_for_trial, -> do left_joins(gitlab_subscription: :hosted_plan) .where( diff --git a/ee/app/models/gitlab_subscriptions/duo_enterprise.rb b/ee/app/models/gitlab_subscriptions/duo_enterprise.rb new file mode 100644 index 0000000000000000000000000000000000000000..fcb2b30f24a8172ddb8ff05552579d4613d725d4 --- /dev/null +++ b/ee/app/models/gitlab_subscriptions/duo_enterprise.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module GitlabSubscriptions + module DuoEnterprise + ELIGIBLE_PLANS = [::Plan::ULTIMATE, ::Plan::ULTIMATE_TRIAL].freeze + end +end diff --git a/ee/app/services/gitlab_subscriptions/trials/create_duo_pro_service.rb b/ee/app/services/gitlab_subscriptions/trials/create_duo_pro_service.rb index e008cb3a012f1bfc61aad1857a7cb879b687193a..2c73578712569b21f07287a75590c054ff85c15f 100644 --- a/ee/app/services/gitlab_subscriptions/trials/create_duo_pro_service.rb +++ b/ee/app/services/gitlab_subscriptions/trials/create_duo_pro_service.rb @@ -54,7 +54,7 @@ def apply_trial_service_class end def namespaces_eligible_for_trial - Users::DuoProTrialEligibleNamespacesFinder.new(user).execute + Users::AddOnTrialEligibleNamespacesFinder.new(user, add_on: :duo_pro).execute end def trial_user_params diff --git a/ee/spec/finders/users/add_on_trial_eligible_namespaces_finder_spec.rb b/ee/spec/finders/users/add_on_trial_eligible_namespaces_finder_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..4e78ad77b55d8646a7aac4e5772f473d0f94617f --- /dev/null +++ b/ee/spec/finders/users/add_on_trial_eligible_namespaces_finder_spec.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Users::AddOnTrialEligibleNamespacesFinder, feature_category: :subscription_management do + describe '#execute', :saas do + let_it_be(:user) { create :user } + let_it_be(:namespace_with_paid_plan) { create(:group_with_plan, name: 'Zed', plan: :ultimate_plan) } + let_it_be(:namespace_with_duo) { create(:group_with_plan, plan: :ultimate_plan) } + let_it_be(:namespace_with_other_addon) { create(:group_with_plan, name: 'Alpha', plan: :ultimate_plan) } + let_it_be(:namespace_with_middle_name) { create(:group_with_plan, name: 'Beta', plan: :ultimate_plan) } + let_it_be(:namespace_with_premium_plan) { create(:group_with_plan, name: 'Gama', plan: :premium_plan) } + let_it_be(:namespace_with_free_plan) { create(:group_with_plan, plan: :free_plan) } + + before_all do + create(:gitlab_subscription_add_on_purchase, :product_analytics, namespace: namespace_with_other_addon) + end + + context 'for duo_pro' do + before_all do + create(:gitlab_subscription_add_on_purchase, :gitlab_duo_pro, namespace: namespace_with_duo) + end + + subject(:execute) { described_class.new(user, add_on: :duo_pro).execute } + + context 'when the add-on does not exist in the system' do + it { is_expected.to eq [] } + end + + context 'when the add-on exists in the system' do + context 'when user does not own groups' do + it { is_expected.to eq [] } + end + + context 'when user owns groups' do + before_all do + namespace_with_paid_plan.add_owner(user) + namespace_with_duo.add_owner(user) + namespace_with_premium_plan.add_owner(user) + namespace_with_free_plan.add_owner(user) + namespace_with_other_addon.add_owner(user) + namespace_with_middle_name.add_owner(user) + end + + specify do + result = [ + namespace_with_other_addon, + namespace_with_middle_name, + namespace_with_premium_plan, + namespace_with_paid_plan + ] + + is_expected.to eq result + end + end + end + end + + context 'for duo_enterprise' do + before_all do + create(:gitlab_subscription_add_on_purchase, :duo_enterprise, namespace: namespace_with_duo) + end + + subject(:execute) { described_class.new(user, add_on: :duo_enterprise).execute } + + context 'when the add-on does not exist in the system' do + it { is_expected.to eq [] } + end + + context 'when the add-on exists in the system' do + context 'when user does not own groups' do + it { is_expected.to eq [] } + end + + context 'when user owns groups' do + before_all do + namespace_with_paid_plan.add_owner(user) + namespace_with_duo.add_owner(user) + namespace_with_premium_plan.add_owner(user) + namespace_with_free_plan.add_owner(user) + namespace_with_other_addon.add_owner(user) + namespace_with_middle_name.add_owner(user) + end + + it { is_expected.to eq [namespace_with_other_addon, namespace_with_middle_name, namespace_with_paid_plan] } + end + end + end + + context 'with invalid add_on' do + subject(:execute) { described_class.new(user, add_on: :invalid_add_on).execute } + + it 'raises an error' do + expect { execute }.to raise_error(ArgumentError) + end + end + end +end diff --git a/ee/spec/finders/users/duo_pro_trial_eligible_namespaces_finder_spec.rb b/ee/spec/finders/users/duo_pro_trial_eligible_namespaces_finder_spec.rb deleted file mode 100644 index a3c9c75ba0992cc51e2023df1f9a077157e6d20f..0000000000000000000000000000000000000000 --- a/ee/spec/finders/users/duo_pro_trial_eligible_namespaces_finder_spec.rb +++ /dev/null @@ -1,43 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Users::DuoProTrialEligibleNamespacesFinder, feature_category: :subscription_management do - describe '#execute', :saas do - let_it_be(:user) { create :user } - let_it_be(:namespace_with_paid_plan) { create(:group_with_plan, name: 'Zed', plan: :ultimate_plan) } - let_it_be(:namespace_with_duo_pro) { create(:group_with_plan, plan: :ultimate_plan) } - let_it_be(:namespace_with_other_addon) { create(:group_with_plan, name: 'Alpha', plan: :ultimate_plan) } - let_it_be(:namespace_with_middle_name) { create(:group_with_plan, name: 'Beta', plan: :ultimate_plan) } - let_it_be(:namespace_with_free_plan) { create(:group_with_plan, plan: :free_plan) } - - before_all do - create(:gitlab_subscription_add_on_purchase, :gitlab_duo_pro, namespace: namespace_with_duo_pro) - create(:gitlab_subscription_add_on_purchase, :product_analytics, namespace: namespace_with_other_addon) - end - - subject(:execute) { described_class.new(user).execute } - - context 'when the add-on does not exist in the system' do - it { is_expected.to eq [] } - end - - context 'when the add-on exists in the system' do - context 'when user does not own groups' do - it { is_expected.to eq [] } - end - - context 'when user owns groups' do - before_all do - namespace_with_paid_plan.add_owner(user) - namespace_with_duo_pro.add_owner(user) - namespace_with_free_plan.add_owner(user) - namespace_with_other_addon.add_owner(user) - namespace_with_middle_name.add_owner(user) - end - - it { is_expected.to eq [namespace_with_other_addon, namespace_with_middle_name, namespace_with_paid_plan] } - end - end - end -end diff --git a/ee/spec/models/ee/namespace_spec.rb b/ee/spec/models/ee/namespace_spec.rb index e0821402e7683c194ea47331e99b8f20bd08837d..ff1eab37937df5fdced63ed508263cd162d8d758 100644 --- a/ee/spec/models/ee/namespace_spec.rb +++ b/ee/spec/models/ee/namespace_spec.rb @@ -385,6 +385,44 @@ end end + describe '.in_specific_plans', :saas do + let_it_be(:free_namespace) { create(:namespace_with_plan, plan: :free_plan) } + let_it_be(:premium_namespace) { create(:namespace_with_plan, plan: :premium_plan) } + let_it_be(:ultimate_namespace) { create(:namespace_with_plan, plan: :ultimate_plan) } + + it 'returns namespaces with the specified plan names' do + result = described_class.in_specific_plans(%w[ultimate premium]) + + expect(result).to include(ultimate_namespace, premium_namespace) + expect(result).not_to include(free_namespace) + end + + it 'returns an empty relation when no matching plans are found' do + result = described_class.in_specific_plans('gold') + + expect(result).to be_empty + end + + it 'returns all namespaces when all plan names are specified' do + result = described_class.in_specific_plans(%w[free ultimate premium]) + + expect(result).to include(free_namespace, ultimate_namespace, premium_namespace) + end + + it 'does not return namespaces without subscriptions' do + namespace_without_subscription = create(:namespace) + result = described_class.in_specific_plans(%w[free ultimate premium]) + + expect(result).not_to include(namespace_without_subscription) + end + + it 'allows chaining with other scopes' do + result = described_class.id_in(ultimate_namespace).in_specific_plans(['ultimate']) + + expect(result).to contain_exactly(ultimate_namespace) + end + end + describe '.not_duo_pro_or_no_add_on', :saas do let_it_be(:namespace_with_paid_plan) { create(:group_with_plan, plan: :ultimate_plan) } let_it_be(:namespace_with_duo_pro) { create(:group_with_plan, plan: :ultimate_plan) } @@ -402,6 +440,23 @@ end end + describe '.not_duo_enterprise_or_no_add_on', :saas do + let_it_be(:namespace_with_paid_plan) { create(:group_with_plan, plan: :ultimate_plan) } + let_it_be(:namespace_with_duo) { create(:group_with_plan, plan: :ultimate_plan) } + let_it_be(:namespace_with_other_addon) { create(:group_with_plan, plan: :ultimate_plan) } + let_it_be(:regular_namespace) { create(:group) } + + before_all do + create(:gitlab_subscription_add_on_purchase, :duo_enterprise, namespace: namespace_with_duo) + create(:gitlab_subscription_add_on_purchase, :product_analytics, namespace: namespace_with_other_addon) + end + + it 'includes correct namespaces' do + expect(described_class.not_duo_enterprise_or_no_add_on) + .to match_array([namespace_with_paid_plan, namespace_with_other_addon, regular_namespace]) + end + end + describe '.eligible_for_trial', :saas do let_it_be(:namespace) { create :namespace }