From b69ef71c62f9474a3fc912d81f0503f72e1f290e Mon Sep 17 00:00:00 2001 From: Qingyu Zhao <qzhao@gitlab.com> Date: Wed, 6 Dec 2023 15:18:07 +0000 Subject: [PATCH] Handle reset for ultimate-trial-paid-customer plan GitlabSubscription before_update callback `reset_seat_statistics` needs to handle ultimate-trial-paid-customer plan differently. During the ultimate-trial-paid-customer period, we want to continue the premium plan's seat statistics and QSR, as if the namespace is still under the premium plan. Thus we should not reset_seat_statistics when starts(and expires) ultimate-trial-paid-customer plan. There are several scenarios: - premium=>ultimate-trial-paid-customer, do not reset_seat_statistics - ultimate-trial-paid-customer=>premium(not renewed), do not reset_seat_statistics, - ultimate-trial-paid-customer=>premium(renewed), reset_seat_statistics - ultimate-trial-paid-customer=>ultimate, reset_seat_statistics Changelog: changed MR: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/137243 EE: true --- ee/app/models/gitlab_subscription.rb | 41 ++++++++++ ee/spec/models/gitlab_subscription_spec.rb | 94 +++++++++++++++++++--- 2 files changed, 124 insertions(+), 11 deletions(-) diff --git a/ee/app/models/gitlab_subscription.rb b/ee/app/models/gitlab_subscription.rb index 1d05ff4bfca1..4c16ffdfba77 100644 --- a/ee/app/models/gitlab_subscription.rb +++ b/ee/app/models/gitlab_subscription.rb @@ -173,6 +173,7 @@ def index_namespace # If the subscription changes, we reset max_seats_used and seats_owed # if they're out of date, so that we don't carry them over from the previous term/subscription. + # One exception is the plan switch between `premium` and `ultimate_trial_paid_customer`. def reset_seat_statistics return unless reset_seat_statistics? @@ -200,6 +201,7 @@ def new_term? end def reset_seat_statistics? + return reset_involves_ultimate_trial_paid_customer_plan? if involves_ultimate_trial_paid_customer_plan? return false unless has_a_paid_hosted_plan? return true if new_term? return true if trial_changed? && !trial @@ -207,6 +209,45 @@ def reset_seat_statistics? max_seats_used_changed_at.present? && max_seats_used_changed_at.to_date < start_date end + def ultimate_trial_paid_customer_plan_id + strong_memoize(:ultimate_trial_paid_customer_plan_id) do + ::Plan.find_by(name: ::Plan::ULTIMATE_TRIAL_PAID_CUSTOMER)&.id + end + end + + def premium_plan_id + strong_memoize(:premium_plan_id) do + ::Plan.find_by(name: ::Plan::PREMIUM)&.id + end + end + + def involves_ultimate_trial_paid_customer_plan? + return false unless ultimate_trial_paid_customer_plan_id + + ultimate_trial_paid_customer_plan_id.in?([hosted_plan_id, hosted_plan_id_was]) + end + + def reset_involves_ultimate_trial_paid_customer_plan? + # Do not reset if plan unchanged on ultimate_trial_paid_customer_plan + return false unless hosted_plan_id_changed? + + # Do not reset if switching to ultimate_trial_paid_customer_plan + return false if hosted_plan_id == ultimate_trial_paid_customer_plan_id + + # Now hosted_plan_id_was must be ultimate_trial_paid_customer_plan_id + return false if hosted_plan_id == premium_plan_id && premium_plan_not_renewed? + + # Either the premium plan was renewed, or it is upgrading to ultimate plan + true + end + + def premium_plan_not_renewed? + previous_premium_gs = GitlabSubscriptionHistory + .where(gitlab_subscription_id: id, hosted_plan_id: premium_plan_id).order(:id).last + + previous_premium_gs&.start_date == start_date && previous_premium_gs&.end_date == end_date + end + def tracked_attributes_changed? changed.intersection(GitlabSubscriptionHistory::TRACKED_ATTRIBUTES).any? end diff --git a/ee/spec/models/gitlab_subscription_spec.rb b/ee/spec/models/gitlab_subscription_spec.rb index 6ae7c8468e33..e35cf0c005a6 100644 --- a/ee/spec/models/gitlab_subscription_spec.rb +++ b/ee/spec/models/gitlab_subscription_spec.rb @@ -5,7 +5,7 @@ RSpec.describe GitlabSubscription, :saas, feature_category: :subscription_management do using RSpec::Parameterized::TableSyntax - %i[free_plan bronze_plan premium_plan ultimate_plan].each do |plan| # rubocop:disable RSpec/UselessDynamicDefinition -- `plan` used in `let_it_be` + ['free', *Plan::PAID_HOSTED_PLANS].map { |name| "#{name}_plan" }.each do |plan| # rubocop:disable RSpec/UselessDynamicDefinition -- `plan` used in `let_it_be` let_it_be(plan) { create(plan) } # rubocop:disable Rails/SaveBang end @@ -231,7 +231,7 @@ end context 'with a trial plan' do - let(:subscription_attrs) { { hosted_plan: bronze_plan, trial: true } } + let(:subscription_attrs) { { hosted_plan: ultimate_trial_plan, trial: true } } include_examples 'always returns a total of 0' end @@ -337,23 +337,25 @@ subject { gitlab_subscription.seats_in_use } context 'with a paid hosted plan' do - let(:hosted_plan) { ultimate_plan } + Plan::PAID_HOSTED_PLANS.each do |plan_name| # rubocop:disable RSpec/UselessDynamicDefinition -- `plan_name` used in `let` + let(:hosted_plan) { send("#{plan_name}_plan") } - it 'returns the previously calculated seats in use' do - expect(subject).to eq(5) - end + it "returns the previously calculated seats in use when plan is #{plan_name}" do + expect(subject).to eq(5) + end - context 'when seats in use is 0' do - let(:seats_in_use) { 0 } + context 'when seats in use is 0' do + let(:seats_in_use) { 0 } - it 'returns 0 too' do - expect(subject).to eq(0) + it "returns 0 too when plan is #{plan_name}" do + expect(subject).to eq(0) + end end end end context 'with a trial plan' do - let(:hosted_plan) { ultimate_plan } + let(:hosted_plan) { ultimate_trial_plan } let(:trial) { true } it 'returns the current seats in use' do @@ -806,6 +808,76 @@ it_behaves_like 'does not reset seat statistics' end end + + context 'when starts ultimate_trial_paid_customer plan' do + before do + gitlab_subscription.update!(hosted_plan: premium_plan) + end + + it 'does not reset seat statistics' do + expect do + gitlab_subscription.update!(hosted_plan: ultimate_trial_paid_customer_plan) + end.to not_change(gitlab_subscription, :max_seats_used) + .and not_change(gitlab_subscription, :max_seats_used_changed_at) + .and not_change(gitlab_subscription, :seats_owed) + end + end + + context 'when updates max_seats_used_changed_at during ultimate_trial_paid_customer plan' do + before do + gitlab_subscription.update!(hosted_plan: ultimate_trial_paid_customer_plan) + end + + it 'does not reset seat statistics' do + expect do + gitlab_subscription.update!(max_seats_used_changed_at: Date.current) + end.to not_change(gitlab_subscription, :max_seats_used) + .and not_change(gitlab_subscription, :seats_owed) + end + end + + context 'when switches back to premium plan from ultimate_trial_paid_customer plan' do + before do + gitlab_subscription.update!(hosted_plan: premium_plan) + gitlab_subscription.update!(hosted_plan: ultimate_trial_paid_customer_plan) + end + + it 'does not reset seat statistics' do + expect do + gitlab_subscription.update!(hosted_plan: premium_plan) + end.to not_change(gitlab_subscription, :max_seats_used) + .and not_change(gitlab_subscription, :max_seats_used_changed_at) + .and not_change(gitlab_subscription, :seats_owed) + end + + context 'when the premium_plan was renewed during the ultimate_trial_paid_customer trial' do + before do + gitlab_subscription.update!(start_date: Date.today + 2.years, end_date: Date.today + 3.years) + end + + it 'resets seat statistics' do + expect do + gitlab_subscription.update!(hosted_plan: premium_plan) + end.to change(gitlab_subscription, :max_seats_used).from(42).to(1) + .and change(gitlab_subscription, :seats_owed).from(29).to(0) + .and change(gitlab_subscription, :seats_in_use).from(20).to(1) + end + end + end + + context 'when upgrade to ultimate plan from ultimate_trial_paid_customer plan' do + before do + gitlab_subscription.update!(hosted_plan: ultimate_trial_paid_customer_plan) + end + + it 'does not reset seat statistics' do + expect do + gitlab_subscription.update!(hosted_plan: ultimate_plan) + end.to change(gitlab_subscription, :max_seats_used).from(42).to(1) + .and change(gitlab_subscription, :seats_owed).from(29).to(0) + .and change(gitlab_subscription, :seats_in_use).from(20).to(1) + end + end end context 'after_destroy_commit' do -- GitLab