Commit 207bdee1 authored by James Lopez's avatar James Lopez

Merge branch 'vs-send-email-to-ee-admin-user-count-threshold' into 'master'

Send email to EE admins on reaching active user count threshold

Closes #230608

See merge request gitlab-org/gitlab!42453
parents b6538e3a 868cb7a4
...@@ -529,6 +529,9 @@ Settings.cron_jobs['member_invitation_reminder_emails_worker']['cron'] ||= '0 0 ...@@ -529,6 +529,9 @@ Settings.cron_jobs['member_invitation_reminder_emails_worker']['cron'] ||= '0 0
Settings.cron_jobs['member_invitation_reminder_emails_worker']['job_class'] = 'MemberInvitationReminderEmailsWorker' Settings.cron_jobs['member_invitation_reminder_emails_worker']['job_class'] = 'MemberInvitationReminderEmailsWorker'
Gitlab.ee do Gitlab.ee do
Settings.cron_jobs['active_user_count_threshold_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['active_user_count_threshold_worker']['cron'] ||= '0 12 * * *'
Settings.cron_jobs['active_user_count_threshold_worker']['job_class'] = 'ActiveUserCountThresholdWorker'
Settings.cron_jobs['adjourned_group_deletion_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['adjourned_group_deletion_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['adjourned_group_deletion_worker']['cron'] ||= '0 3 * * *' Settings.cron_jobs['adjourned_group_deletion_worker']['cron'] ||= '0 3 * * *'
Settings.cron_jobs['adjourned_group_deletion_worker']['job_class'] = 'AdjournedGroupDeletionWorker' Settings.cron_jobs['adjourned_group_deletion_worker']['job_class'] = 'AdjournedGroupDeletionWorker'
......
...@@ -42,6 +42,6 @@ module LicenseMonitoringHelper ...@@ -42,6 +42,6 @@ module LicenseMonitoringHelper
end end
def remaining_user_count def remaining_user_count
strong_memoize(:remaining_user_count) { current_license.restricted_user_count } strong_memoize(:remaining_user_count) { current_license.remaining_user_count }
end end
end end
# frozen_string_literal: true
class LicenseMailer < ApplicationMailer
helper EmailsHelper
layout 'mailer'
def approaching_active_user_count_limit(recipients)
@license = License.current
return unless @license
mail(
bcc: recipients,
subject: "Your subscription is nearing its user limit"
)
end
end
# frozen_string_literal: true
class LicenseMailerPreview < ActionMailer::Preview
def approaching_active_user_count_limit
::LicenseMailer.approaching_active_user_count_limit(%w(admin@example.com))
end
end
...@@ -292,6 +292,15 @@ class License < ApplicationRecord ...@@ -292,6 +292,15 @@ class License < ApplicationRecord
decryptable_licenses.sort_by { |license| [license.starts_at, license.created_at, license.expires_at] }.reverse decryptable_licenses.sort_by { |license| [license.starts_at, license.created_at, license.expires_at] }.reverse
end end
def with_valid_license
current_license = License.current
return unless current_license
return if current_license.trial?
yield(current_license) if block_given?
end
private private
def load_future_dated def load_future_dated
......
- users_over_license_link = link_to("users over license", "https://docs.gitlab.com/ee/subscriptions/#users-over-license")
- self_managed_subscriptions_doc_link = link_to("self-managed subscriptions", "https://docs.gitlab.com/ee/subscriptions/self_managed/index.html")
- subscriptions_doc_link = link_to("our documentation", "https://docs.gitlab.com/ee/subscriptions")
%p
= _("Dear Administrator,")
%p
= html_escape(_("We would like to inform you that your subscription GitLab Enterprise Edition %{plan_name} is nearing its user limit. You have %{active_user_count} active users, which is almost at the user limit of %{maximum_user_count}.")) % { plan_name: @license.plan.titleize, active_user_count: @license.current_active_users_count, maximum_user_count: @license.restricted_user_count }
%p
= html_escape(_("If the number of active users exceeds the user limit, you will be charged for the number of %{users_over_license_link} at your next license reconciliation.")) % { users_over_license_link: users_over_license_link }
%p
= html_escape(_("For more information on how the number of active users is calculated, see the %{self_managed_subscriptions_doc_link} documentation.")) % { self_managed_subscriptions_doc_link: self_managed_subscriptions_doc_link }
%p
= html_escape(_("You can find more information about GitLab subscriptions in %{subscriptions_doc_link}.")) % { subscriptions_doc_link: subscriptions_doc_link }
%p
= _("Please reach out if you have any questions and we'll be happy to assist.")
%p
= _("Thank you for your business.")
%p
= _("GitLab Billing Team.")
# frozen_string_literal: true
class ActiveUserCountThresholdWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
# rubocop:disable Scalability/CronWorkerContext
# This worker does not perform work scoped to a context
include CronjobQueue
# rubocop:enable Scalability/CronWorkerContext
feature_category :provision
def perform
License.with_valid_license do |license|
break unless license.active_user_count_threshold_reached?
# rubocop:disable CodeReuse/ActiveRecord
recipients = User
.active
.admins
.pluck(:email)
.to_set
# rubocop:enable CodeReuse/ActiveRecord
recipients << license.licensee["Email"] if license.licensee["Email"]
LicenseMailer.approaching_active_user_count_limit(recipients.to_a)
end
end
end
...@@ -3,6 +3,14 @@ ...@@ -3,6 +3,14 @@
# #
# Do not edit it manually! # Do not edit it manually!
--- ---
- :name: cronjob:active_user_count_threshold
:feature_category: :provision
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent:
:tags: []
- :name: cronjob:adjourned_group_deletion - :name: cronjob:adjourned_group_deletion
:feature_category: :authentication_and_authorization :feature_category: :authentication_and_authorization
:has_external_dependencies: :has_external_dependencies:
......
...@@ -10,7 +10,7 @@ class HistoricalDataWorker # rubocop:disable Scalability/IdempotentWorker ...@@ -10,7 +10,7 @@ class HistoricalDataWorker # rubocop:disable Scalability/IdempotentWorker
feature_category :provision feature_category :provision
def perform def perform
return if License.current.nil? || License.current&.trial? return if License.current.nil? || License.current.trial?
HistoricalData.track! HistoricalData.track!
end end
......
---
title: Send email reminder when approaching active user limit
merge_request: 42453
author:
type: added
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe LicenseMailer do
include EmailSpec::Matchers
let(:recipients) { %w(admin@example.com another_admin@example.com) }
let_it_be(:license) { create_current_license({ plan: License::STARTER_PLAN, restrictions: { active_user_count: 21 } }) }
describe '#approaching_active_user_count_limit' do
let(:subject_text) { 'Your subscription is nearing its user limit' }
let(:subscription_name) { 'GitLab Enterprise Edition Starter' }
let(:active_user_count) { 20 }
subject { described_class.approaching_active_user_count_limit(recipients) }
before do
allow(license).to receive(:current_active_users_count).and_return(active_user_count)
allow(License).to receive(:current).and_return(license)
end
context 'when license is present' do
it { is_expected.to have_subject subject_text }
it { is_expected.to bcc_to recipients }
it { is_expected.to have_body_text "your subscription #{subscription_name}" }
it { is_expected.to have_body_text "You have #{active_user_count} active users" }
it { is_expected.to have_body_text "the user limit of #{license.restricted_user_count}" }
end
context 'when license is not present' do
it 'does not send email' do
expect { subject }.not_to change(ActionMailer::Base.deliveries, :count)
end
end
end
end
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
require "spec_helper" require "spec_helper"
RSpec.describe License do RSpec.describe License do
using RSpec::Parameterized::TableSyntax
let(:gl_license) { build(:gitlab_license) } let(:gl_license) { build(:gitlab_license) }
let(:license) { build(:license, data: gl_license.export) } let(:license) { build(:license, data: gl_license.export) }
...@@ -26,8 +28,6 @@ RSpec.describe License do ...@@ -26,8 +28,6 @@ RSpec.describe License do
end end
describe '#check_users_limit' do describe '#check_users_limit' do
using RSpec::Parameterized::TableSyntax
before do before do
create(:group_member, :guest) create(:group_member, :guest)
create(:group_member, :reporter) create(:group_member, :reporter)
...@@ -550,6 +550,39 @@ RSpec.describe License do ...@@ -550,6 +550,39 @@ RSpec.describe License do
it { is_expected.to be(false) } it { is_expected.to be(false) }
end end
end end
describe '.with_valid_license' do
context 'when license trial' do
before do
allow(license).to receive(:trial?).and_return(true)
allow(License).to receive(:current).and_return(license)
end
it 'does not yield block' do
expect { |b| License.with_valid_license(&b) }.not_to yield_control
end
end
context 'when license nil' do
before do
allow(License).to receive(:current).and_return(nil)
end
it 'does not yield block' do
expect { |b| License.with_valid_license(&b) }.not_to yield_control
end
end
context 'when license is valid' do
before do
allow(License).to receive(:current).and_return(license)
end
it 'yields block' do
expect { |b| License.with_valid_license(&b) }.to yield_with_args(license)
end
end
end
end end
describe "#md5" do describe "#md5" do
...@@ -742,8 +775,6 @@ RSpec.describe License do ...@@ -742,8 +775,6 @@ RSpec.describe License do
end end
describe '#maximum_user_count' do describe '#maximum_user_count' do
using RSpec::Parameterized::TableSyntax
subject { license.maximum_user_count } subject { license.maximum_user_count }
where(:current_active_users_count, :historical_max, :expected) do where(:current_active_users_count, :historical_max, :expected) do
...@@ -907,8 +938,6 @@ RSpec.describe License do ...@@ -907,8 +938,6 @@ RSpec.describe License do
end end
describe '#paid?' do describe '#paid?' do
using RSpec::Parameterized::TableSyntax
where(:plan, :paid_result) do where(:plan, :paid_result) do
License::STARTER_PLAN | true License::STARTER_PLAN | true
License::PREMIUM_PLAN | true License::PREMIUM_PLAN | true
...@@ -928,8 +957,6 @@ RSpec.describe License do ...@@ -928,8 +957,6 @@ RSpec.describe License do
end end
describe '#started?' do describe '#started?' do
using RSpec::Parameterized::TableSyntax
where(:starts_at, :result) do where(:starts_at, :result) do
Date.current - 1.month | true Date.current - 1.month | true
Date.current | true Date.current | true
...@@ -948,8 +975,6 @@ RSpec.describe License do ...@@ -948,8 +975,6 @@ RSpec.describe License do
end end
describe '#future_dated?' do describe '#future_dated?' do
using RSpec::Parameterized::TableSyntax
where(:starts_at, :result) do where(:starts_at, :result) do
Date.current - 1.month | false Date.current - 1.month | false
Date.current | false Date.current | false
...@@ -983,8 +1008,6 @@ RSpec.describe License do ...@@ -983,8 +1008,6 @@ RSpec.describe License do
end end
context 'for license with users' do context 'for license with users' do
using RSpec::Parameterized::TableSyntax
where(:restricted_user_count, :active_user_count, :percentage, :threshold_value) do where(:restricted_user_count, :active_user_count, :percentage, :threshold_value) do
3 | 2 | false | 1 3 | 2 | false | 1
20 | 18 | false | 2 20 | 18 | false | 2
...@@ -1006,8 +1029,6 @@ RSpec.describe License do ...@@ -1006,8 +1029,6 @@ RSpec.describe License do
end end
describe '#active_user_count_threshold_reached?' do describe '#active_user_count_threshold_reached?' do
using RSpec::Parameterized::TableSyntax
subject { license.active_user_count_threshold_reached? } subject { license.active_user_count_threshold_reached? }
where(:restricted_user_count, :current_active_users_count, :result) do where(:restricted_user_count, :current_active_users_count, :result) do
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ActiveUserCountThresholdWorker do
using RSpec::Parameterized::TableSyntax
subject { described_class.new }
let(:license) { build(:license) }
describe '#perform' do
where(:trial?, :threshold_reached?, :should_send_reminder?) do
false | false | false
false | true | true
true | false | false
true | true | false
end
with_them do
before do
allow(license).to receive(:trial?).and_return(trial?)
allow(license).to receive(:active_user_count_threshold_reached?).and_return(threshold_reached?)
allow(License).to receive(:current).and_return(license)
end
it do
if should_send_reminder?
expect(LicenseMailer).to receive(:approaching_active_user_count_limit)
else
expect(LicenseMailer).not_to receive(:approaching_active_user_count_limit)
end
subject.perform
end
end
context 'recipients' do
let_it_be(:admins) { create_list(:admin, 3) }
before do
allow(license).to receive(:trial?).and_return(false)
allow(license).to receive(:active_user_count_threshold_reached?).and_return(true)
allow(License).to receive(:current).and_return(license)
end
it 'sends reminder to admins only' do
admins_emails = admins.pluck(:email)
expect(LicenseMailer).to receive(:approaching_active_user_count_limit).with(array_including(*admins_emails))
subject.perform
end
it 'adds a licensee email to the recipients list' do
allow(license).to receive(:licensee).and_return({ 'Email' => admins.first.email })
licensee_email = license.licensee['Email']
expect(LicenseMailer).to receive(:approaching_active_user_count_limit).with(array_including([licensee_email]))
subject.perform
end
it 'sends reminder to unique emails' do
admins_emails = admins.pluck(:email)
allow(license.licensee).to receive('Email').and_return(admins.first.email)
expect(LicenseMailer).to receive(:approaching_active_user_count_limit).with(array_including(*admins_emails))
subject.perform
end
it 'sends reminder to active admins only' do
admins.first.deactivate!
active_admins_emails = admins.drop(1).pluck(:email)
expect(LicenseMailer).to receive(:approaching_active_user_count_limit).with(array_including(*active_admins_emails))
subject.perform
end
end
context 'when there is no license' do
it 'does not send a reminder' do
expect(LicenseMailer).not_to receive(:approaching_active_user_count_limit)
subject.perform
end
end
end
end
...@@ -8220,6 +8220,9 @@ msgstr "" ...@@ -8220,6 +8220,9 @@ msgstr ""
msgid "Days to merge" msgid "Days to merge"
msgstr "" msgstr ""
msgid "Dear Administrator,"
msgstr ""
msgid "Debug" msgid "Debug"
msgstr "" msgstr ""
...@@ -11398,6 +11401,9 @@ msgstr "" ...@@ -11398,6 +11401,9 @@ msgstr ""
msgid "For more info, read the documentation." msgid "For more info, read the documentation."
msgstr "" msgstr ""
msgid "For more information on how the number of active users is calculated, see the %{self_managed_subscriptions_doc_link} documentation."
msgstr ""
msgid "For more information, go to the " msgid "For more information, go to the "
msgstr "" msgstr ""
...@@ -11974,6 +11980,9 @@ msgstr "" ...@@ -11974,6 +11980,9 @@ msgstr ""
msgid "GitLab API" msgid "GitLab API"
msgstr "" msgstr ""
msgid "GitLab Billing Team."
msgstr ""
msgid "GitLab Group Runners can execute code for all the projects in this group." msgid "GitLab Group Runners can execute code for all the projects in this group."
msgstr "" msgstr ""
...@@ -13286,6 +13295,9 @@ msgstr "" ...@@ -13286,6 +13295,9 @@ msgstr ""
msgid "If enabled, access to projects will be validated on an external service using their classification label." msgid "If enabled, access to projects will be validated on an external service using their classification label."
msgstr "" msgstr ""
msgid "If the number of active users exceeds the user limit, you will be charged for the number of %{users_over_license_link} at your next license reconciliation."
msgstr ""
msgid "If there is no previous license or if the previous license has expired, some GitLab functionality will be blocked until a new, valid license is uploaded." msgid "If there is no previous license or if the previous license has expired, some GitLab functionality will be blocked until a new, valid license is uploaded."
msgstr "" msgstr ""
...@@ -19109,6 +19121,9 @@ msgstr "" ...@@ -19109,6 +19121,9 @@ msgstr ""
msgid "Please provide attributes to update" msgid "Please provide attributes to update"
msgstr "" msgstr ""
msgid "Please reach out if you have any questions and we'll be happy to assist."
msgstr ""
msgid "Please refer to %{docs_url}" msgid "Please refer to %{docs_url}"
msgstr "" msgstr ""
...@@ -25356,6 +25371,9 @@ msgstr "" ...@@ -25356,6 +25371,9 @@ msgstr ""
msgid "Thank you for signing up for your free trial! You will get additional instructions in your inbox shortly." msgid "Thank you for signing up for your free trial! You will get additional instructions in your inbox shortly."
msgstr "" msgstr ""
msgid "Thank you for your business."
msgstr ""
msgid "Thank you for your feedback!" msgid "Thank you for your feedback!"
msgstr "" msgstr ""
...@@ -28743,6 +28761,9 @@ msgstr "" ...@@ -28743,6 +28761,9 @@ msgstr ""
msgid "We will notify %{inviter} that you declined their invitation to join GitLab. You will stop receiving reminders." msgid "We will notify %{inviter} that you declined their invitation to join GitLab. You will stop receiving reminders."
msgstr "" msgstr ""
msgid "We would like to inform you that your subscription GitLab Enterprise Edition %{plan_name} is nearing its user limit. You have %{active_user_count} active users, which is almost at the user limit of %{maximum_user_count}."
msgstr ""
msgid "We've found no vulnerabilities" msgid "We've found no vulnerabilities"
msgstr "" msgstr ""
...@@ -29330,6 +29351,9 @@ msgstr "" ...@@ -29330,6 +29351,9 @@ msgstr ""
msgid "You can filter by 'days to merge' by clicking on the columns in the chart." msgid "You can filter by 'days to merge' by clicking on the columns in the chart."
msgstr "" msgstr ""
msgid "You can find more information about GitLab subscriptions in %{subscriptions_doc_link}."
msgstr ""
msgid "You can generate an access token scoped to this project for each application to use the GitLab API." msgid "You can generate an access token scoped to this project for each application to use the GitLab API."
msgstr "" msgstr ""
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment