Add subscription expired blocked state for Duo Agent Platform

What does this MR do and why?

Render a "subscription has been cancelled" empty state inside the main Duo chat panel when a user's paid Duo Agent Platform subscription (Premium/Ultimate) has expired. The empty state offers a Repurchase Agent Platform CTA (billing-privileged users) and a Learn more link.

This MR supersedes !232198. That MR implemented the same feature on the legacy ai_panel_empty_state.vue path; stakeholders asked for new empty states to migrate onto the newer DuoAgenticChatBlockedStateView slot pattern introduced in !229299 (merged). See this comment for the architectural direction.

Screenshots

.com

dap-subscription-expired-dotcom

SM

dap-subscription-expired-sm

References

Test plan

  • Automated: bundle exec rspec ee/spec/components/duo_chat_panel/ — green
  • Automated: bundle exec rspec ee/spec/models/gitlab_subscription_spec.rb -e paid_and_expired? — green
  • Automated: yarn jest ee/spec/frontend/ai/duo_agentic_chat/components/subscription_expired_empty_state_spec.js ee/spec/frontend/ai/duo_agentic_chat/components/duo_agentic_chat_blocked_state_view_spec.js ee/spec/frontend/ai/init_duo_panel_spec.js — green
  • Manual (.com SaaS) — see details below
  • Manual (Self-managed) — see details below
Manual verification — .com (SaaS)

1. GDK SaaS sim mode. Edit ~/gdk/env.runit:

export GITLAB_SIMULATE_SAAS=1
export CUSTOMER_PORTAL_URL=http://localhost:5000
export GITLAB_LICENSE_MODE=test
export CLOUD_CONNECTOR_SELF_SIGN_TOKENS=1

gdk restart rails-web rails-background-jobs. First run only: bundle exec rails db:migrate.

2. Seed group + expired paid sub.

GITLAB_SIMULATE_SAAS=1 bundle exec rails runner '
user = User.find_by!(username: "root")
org  = Organizations::Organization.default_organization
group = Group.find_or_create_by!(path: "dap-subexp-dotcom") do |g|
  g.name = "DAP SubExp DotCom"
  g.organization = org
  g.owner = user
end
group.add_owner(user) unless group.owners.include?(user)
NamespaceSetting.new(namespace: group).save(validate: false) unless group.namespace_settings
premium = Plan.find_by(name: "premium") || Plan.find_or_create_by!(name: "premium") { |p| p.title = "Premium" }
sub = GitlabSubscription.find_or_initialize_by(namespace: group)
sub.assign_attributes(hosted_plan: premium, seats: 10, start_date: 1.year.ago.to_date, end_date: 1.day.ago.to_date, trial: false)
sub.save!(validate: false)
puts group.gitlab_subscription.paid_and_expired?  # => true
'

3. Non-admin owner carla.

GITLAB_SIMULATE_SAAS=1 bundle exec rails runner '
group = Group.find_by(path: "dap-subexp-dotcom")
owner = User.find_by(username: "carla") || User.create!(
  username: "carla", email: "carla@example.com", name: "Carla",
  password: "Password1!", password_confirmation: "Password1!",
  organization: Organizations::Organization.default_organization,
  confirmed_at: Time.current
)
group.add_owner(owner) unless group.owners.include?(owner)
'

4. Confirm orchestrator routes ChatComponent + emits data attr.

GITLAB_SIMULATE_SAAS=1 bundle exec rails runner '
group = Group.find_by(path: "dap-subexp-dotcom")
owner = User.find_by!(username: "carla")
puts DuoChatPanel::Component.new(user: owner, project: nil, group: group).send(:component_instance).class.name
# => DuoChatPanel::ChatComponent
chat = DuoChatPanel::ChatComponent.new(user: owner, project: nil, group: group)
puts chat.send(:subscription_status_data, group, nil).inspect
# => {..., :subscription_expired=>"true"}
'

5. Browser. Sign in carla / Password1!/groups/dap-subexp-dotcom → open Duo panel right rail.

See:

  • Headline: "Your GitLab Duo Agent Platform subscription has been cancelled" ✓
  • Body cancelled copy ✓
  • "Learn more" visible ✓
  • "Repurchase Agent Platform" hidden — carla no edit_billing on .com namespace ✓
Manual verification — Self-managed

1. GDK SM mode. Edit ~/gdk/env.runit:

export GITLAB_SIMULATE_SAAS=0
export GITLAB_LICENSE_MODE=test
export CLOUD_CONNECTOR_SELF_SIGN_TOKENS=1

gdk restart rails-web rails-background-jobs.

2. Mint + load expired Ultimate license. Sign inside GitLab rails with CDot test signing key (sidesteps CDot bundle if broken local). Critical: set block_changes_at else License#notify_users? crash every page render with comparison of Date with nil failed.

bundle exec rails runner '
key_path = File.expand_path("~/customers-gitlab-com/safe/test_license_encryption_key")
Gitlab::License.encryption_key = OpenSSL::PKey::RSA.new(File.read(key_path))
license = Gitlab::License.new
license.licensee = { "Name" => "Subscription Expired Test" }
license.starts_at = Date.today - 30
license.expires_at = Date.yesterday
license.notify_admins_at = Date.today - 7
license.notify_users_at = Date.today - 7
license.block_changes_at = Date.today + 7
license.restrictions = { active_user_count: 100, plan: "ultimate", trial: false }
blob = license.export
License.where.not(id: nil).delete_all
License.reset_current
License.new(data: blob, cloud: false, last_synced_at: Time.current).save!(validate: false)
License.reset_current
cur = License.current
puts "trial?=#{cur.trial?} expired?=#{cur.expired?} plan=#{cur.plan}"
# => trial?=false expired?=true plan=ultimate
'

3. Confirm orchestrator + data.

bundle exec rails runner '
user = User.find_by!(username: "root")
group = Group.first
chat = DuoChatPanel::ChatComponent.new(user: user, project: nil, group: group)
puts "saas?=#{Gitlab::Saas.feature_available?(:gitlab_com_subscriptions)}"  # => false
puts DuoChatPanel::Component.new(user: user, project: nil, group: group).send(:component_instance).class.name
# => DuoChatPanel::ChatComponent
puts chat.send(:subscription_expired?, group, nil)  # => true
'

4. Browser. Sign in admin → any group → open Duo panel right rail.

See:

  • Headline + cancelled body ✓
  • "Repurchase Agent Platform" visible if admin pass Ability.allowed?(:admin_all_resources) (admin + admin_mode) ✓
  • CTA href = /admin/gitlab_credits_dashboard — in-app credits page, not customers portal (differs from !232198 destination, worth product confirm)
  • Page banner "Your Ultimate subscription has expired" = existing GitLab license-expiry UX, not this MR
Edited by Anas Shahid

Merge request reports

Loading