Hide Ultimate trial link from Ultimate tier owners

What does this MR do and why?

Owners of top-level namespaces on a community program plan (Open Source, Education, Non-Profit — all stored as the opensource plan) see "Start an Ultimate trial" in the profile dropdown, even though the namespace is already on an Ultimate-tier plan. A customer reported this.

The dropdown item is built by GlobalMediator in ee/app/presenters/gitlab_subscriptions/upgrade_presenter.rb, for the case where there is no group or project in context:

  1. can_upgrade_subscription? returns false. It relies on User#owned_free_or_trial_groups_with_limit, which uses the free_or_trial namespace scope. That scope only matches the free plan or an active trial, so an opensource namespace does not match.
  2. can_start_trial? then returns true. It calls GitlabSubscriptions::Trials.no_eligible_namespaces_for_user?, which is true when the user owns no namespace a trial could be applied to. Namespaces::TrialEligibleFinder filters on Plan::PLANS_ELIGIBLE_FOR_TRIAL (default, free, premium), which excludes opensource.
  3. With the first branch skipped and the second one true, the mediator falls through to the trial link.

The fix adds a check in can_start_trial?: it also requires that the user does not own any Ultimate-tier namespace, using the existing User#owns_paid_namespace? helper with the existing Plan::ULTIMATE_TIER_PLANS constant (gold, ultimate, opensource, ultimate_trial, ultimate_trial_paid_customer). Using the tier constant instead of hardcoding opensource means any future plan added to the Ultimate tier is covered automatically, and it also fixes the same symptom for owners of paid Ultimate and Gold namespaces, who hit the identical fall-through today.

What this MR deliberately does not change: Namespaces::TrialEligibleFinder and Plan::PLANS_ELIGIBLE_FOR_TRIAL. That finder is shared with the real trial-start flow (GitlabSubscriptions::TrialsController, GitlabSubscriptions::Trials::UltimateCreateService, ApplyTrialService). It already correctly excludes opensource, because a namespace already on the Ultimate tier should not be trial-eligible. Adding opensource there would make those namespaces genuinely eligible to start a trial, which is wrong.

No frontend change is needed. app/assets/javascripts/super_sidebar/components/user_menu.vue renders the item from data.upgrade_link with no client-side eligibility logic, so the presenter is the single source of truth. When the presenter returns an empty hash, the dropdown item is not rendered at all.

Performance: the new check is one database query, inside the existing 10 minute Rails cache entry for can_start_trial, and it only runs for users who reach the fallback branch at all. It is evaluated first, so owners of an Ultimate-tier namespace short-circuit before Namespaces::TrialEligibleFinder runs its own query. The reverse order would instead save that query for owners of a Premium or Bronze namespace. Neither order dominates, and for users who own no groups both queries run either way.

Behavior after this change:

  • Owner of an opensource namespace: no dropdown item.
  • Owner of a free namespace: unchanged, still "Upgrade subscription" pointing at the group billing page.
  • Owner of both a free and an opensource namespace: unchanged, still "Upgrade subscription" for the free group, because that branch is evaluated first.
  • User who owns no groups: unchanged, still "Start an Ultimate trial".
  • Owner of a namespace whose Ultimate trial ended today or earlier: no dropdown item, where before there was a trial link. in_active_trial requires trial_ends_on to be tomorrow or later, so those namespaces miss the first branch, and their plan name is still in Plan::ULTIMATE_TIER_PLANS. Suppressing the link looks right, since that namespace has already had its trial, but calling it out as a deliberate side effect.

Tests: ee/spec/presenters/gitlab_subscriptions/upgrade_presenter_spec.rb gains a case for an owner of an Ultimate-tier namespace (no dropdown item) and one for an owner of both a free and an Ultimate-tier namespace (still "Upgrade subscription"). ee/spec/models/ee/user_spec.rb covers owns_paid_namespace? scoped to Plan::ULTIMATE_TIER_PLANS: it matches a namespace on the opensource plan and does not match bronze or free namespaces. The plan-name coverage lives in the model spec because RSpec/FactoryBot/AvoidCreate applies to presenter specs.

Feature flag: none. The change is a single conditional in a presenter, is trivially revertable, and gating it would add more risk surface than it removes.

References

Screenshots or screen recordings

Before After
Screenshot attached to the issue (#628891): profile dropdown shows "Start an Ultimate trial" for a community program namespace owner Dropdown item is absent

How to set up and validate locally

  1. Simulate a SaaS instance: set GITLAB_SIMULATE_SAAS=1 in the GDK environment and restart Rails.

  2. In the Rails console, put a group you own on the opensource plan:

    user = User.find_by_username('root')
    group = Group.find_by_full_path('your-group')
    plan = Plan.find_or_create_by!(name: 'opensource') { |p| p.title = 'Open Source' }
    group.gitlab_subscription&.destroy!
    GitlabSubscription.create!(namespace: group, hosted_plan: plan, seats: 10, start_date: Date.current, end_date: 1.year.from_now)
  3. Clear the cached dropdown state for that user, because both branches are cached for 10 minutes:

    Rails.cache.delete(['users', user.id, 'can_start_trial'])
    Rails.cache.delete(['users', user.id, 'owned_groups_url'])
  4. Sign in as that user and open the profile dropdown in the top right. On master it shows "Start an Ultimate trial". On this branch there is no subscription item.

  5. Optionally confirm the free-plan path is unaffected: repeat with plan name free and confirm the dropdown shows "Upgrade subscription".

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

Merge request reports

Loading
Loading