Skip to content
代码片段 群组 项目
提交 480f20eb 编辑于 作者: Josianne Hyson's avatar Josianne Hyson 提交者: Douglas Barbosa Alexandre
浏览文件

Create service to calculate the seat count usage

We want to be able to display an alert to the user when they are nearing
their seat count for their subscription.

Create a service that will determine if a use should be shown this alert
based on a differing set of thresholds for their company size.

Issue: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/79563
EE: true
上级 6e1546a6
无相关合并请求
...@@ -74,6 +74,10 @@ def calculate_seats_owed ...@@ -74,6 +74,10 @@ def calculate_seats_owed
[0, max_seats_used - seats].max [0, max_seats_used - seats].max
end end
def seats_remaining
[0, seats - max_seats_used.to_i].max
end
# Refresh seat related attribute (without persisting them) # Refresh seat related attribute (without persisting them)
def refresh_seat_attributes! def refresh_seat_attributes!
self.seats_in_use = calculate_seats_in_use self.seats_in_use = calculate_seats_in_use
......
# frozen_string_literal: true
#
module GitlabSubscriptions
module Reconciliations
class CalculateSeatCountDataService
include Gitlab::Utils::StrongMemoize
# For smaller groups we want to alert when they have a set quantity of seats remaining.
# For larger groups we want to alert them when they have a percentage of seats remaining.
SEAT_COUNT_THRESHOLD_LIMITS = [
{ range: (0..15), percentage: false, value: 1 },
{ range: (16..25), percentage: false, value: 2 },
{ range: (26..99), percentage: true, value: 10 },
{ range: (100..999), percentage: true, value: 8 },
{ range: (1000..nil), percentage: true, value: 5 }
].freeze
attr_reader :namespace, :user
delegate :max_seats_used, :max_seats_used_changed_at, :seats, :seats_remaining, to: :current_subscription
def initialize(namespace:, user:)
@namespace = namespace
@user = user
end
def execute
return unless owner_of_paid_group? && seat_count_threshold_reached?
return if user_dismissed_alert?
return unless alert_user_overage?
{
namespace: namespace,
remaining_seat_count: seats_remaining,
seats_in_use: max_seats_used,
total_seat_count: seats
}
end
private
def owner_of_paid_group?
(::Gitlab::CurrentSettings.should_check_namespace_plan? &&
namespace.group_namespace? &&
user.can?(:admin_group, namespace) &&
current_subscription).present?
end
def adapted_remaining_user_count
return seats_remaining.fdiv(seats) * 100 if current_seat_count_threshold[:percentage]
seats_remaining
end
def alert_user_overage?
CheckSeatUsageAlertsEligibilityService.new(namespace: namespace).execute
end
def current_subscription
namespace.gitlab_subscription
end
def current_seat_count_threshold
strong_memoize(:current_seat_count_threshold) do
SEAT_COUNT_THRESHOLD_LIMITS.find do |threshold|
threshold[:range].cover?(seats)
end
end
end
def seat_count_threshold_reached?
max_seats_used &&
max_seats_used < seats &&
current_seat_count_threshold[:value] >= adapted_remaining_user_count
end
def user_dismissed_alert?
user.dismissed_callout_for_group?(
feature_name: Users::GroupCalloutsHelper::APPROACHING_SEAT_COUNT_THRESHOLD,
group: namespace,
ignore_dismissal_earlier_than: max_seats_used_changed_at
)
end
end
end
end
# frozen_string_literal: true
# Service to determine if the given namespace meets the criteria to see
# alert usage overage alerts when they cross the usage thresholds.
# This service only determines eligibility from the CustomersDot application.
#
# If there is a problem querying CustomersDot, it assumes the status is false
#
# returns true, false
module GitlabSubscriptions
module Reconciliations
class CheckSeatUsageAlertsEligibilityService
def initialize(namespace:)
@namespace = namespace
end
def execute
return false unless namespace.gitlab_subscription.present?
eligible_for_seat_usage_alerts
end
private
attr_reader :namespace
def client
Gitlab::SubscriptionPortal::Client
end
def eligible_for_seat_usage_alerts_request
response = client.subscription_seat_usage_alerts_eligibility(namespace.id)
return false unless response[:success]
response[:eligible_for_seat_usage_alerts]
end
def cache
Rails.cache
end
def cache_key
"subscription:eligible_for_seat_usage_alerts:namespace:#{namespace.gitlab_subscription.cache_key}"
end
def eligible_for_seat_usage_alerts
cache.fetch(cache_key, skip_nil: true, expires_in: 1.day) { eligible_for_seat_usage_alerts_request } || false
end
end
end
end
...@@ -86,6 +86,30 @@ def plan_upgrade_offer(namespace_id) ...@@ -86,6 +86,30 @@ def plan_upgrade_offer(namespace_id)
end end
end end
def subscription_seat_usage_alerts_eligibility(namespace_id)
return error('Must provide a namespace ID') unless namespace_id
query = <<~GQL
query($namespaceId: ID!) {
subscription(namespaceId: $namespaceId) {
eligibleForSeatUsageAlerts
}
}
GQL
response = execute_graphql_query({ query: query, variables: { namespaceId: namespace_id } })
if response[:success]
{
success: true,
eligible_for_seat_usage_alerts: response.dig(:data, 'data', 'subscription',
'eligibleForSeatUsageAlerts')
}
else
error(response.dig(:data, :errors))
end
end
def subscription_last_term(namespace_id) def subscription_last_term(namespace_id)
return error('Must provide a namespace ID') unless namespace_id return error('Must provide a namespace ID') unless namespace_id
......
...@@ -204,6 +204,69 @@ ...@@ -204,6 +204,69 @@
end end
end end
describe '#subscription_seat_usage_alerts_eligibility' do
let(:query) do
<<~GQL
query($namespaceId: ID!) {
subscription(namespaceId: $namespaceId) {
eligibleForSeatUsageAlerts
}
}
GQL
end
it 'returns success when a valid license key is specified' do
expected_args = {
query: query,
variables: {
namespaceId: 'namespace-id'
}
}
expected_response = {
success: true,
data: {
"data" => {
"subscription" => {
"eligibleForSeatUsageAlerts" => true
}
}
}
}
expect(client).to receive(:execute_graphql_query).with(expected_args).and_return(expected_response)
result = client.subscription_seat_usage_alerts_eligibility('namespace-id')
expect(result).to eq({ success: true, eligible_for_seat_usage_alerts: true })
end
it 'returns failure when license_key is invalid' do
error = "some error"
expect(client).to receive(:execute_graphql_query).and_return(
{
success: false,
data: {
errors: error
}
}
)
result = client.subscription_seat_usage_alerts_eligibility('failing-namespace-id')
expect(result).to eq({ success: false, errors: error })
end
context 'with no namespace_id' do
it 'returns failure' do
expect(client).not_to receive(:execute_graphql_query)
expect(client.subscription_seat_usage_alerts_eligibility(nil))
.to eq({ success: false, errors: 'Must provide a namespace ID' })
end
end
end
describe '#get_plans' do describe '#get_plans' do
subject { client.get_plans(tags: ['test-plan-id']) } subject { client.get_plans(tags: ['test-plan-id']) }
......
...@@ -169,6 +169,40 @@ ...@@ -169,6 +169,40 @@
end end
end end
describe '#seats_remaining' do
context 'when there are more seats used than available in the subscription' do
it 'returns zero' do
subscription = build(:gitlab_subscription, seats: 10, max_seats_used: 15)
expect(subscription.seats_remaining).to eq 0
end
end
context 'when seats used equals seats in subscription' do
it 'returns zero' do
subscription = build(:gitlab_subscription, seats: 10, max_seats_used: 10)
expect(subscription.seats_remaining).to eq 0
end
end
context 'when there are seats left in the subscription' do
it 'returns the seat count remaining from the max seats used' do
subscription = build(:gitlab_subscription, seats: 10, max_seats_used: 5)
expect(subscription.seats_remaining).to eq 5
end
end
context 'when max seat data has not yet been generated for the subscription' do
it 'returns the seat count of the subscription' do
subscription = build(:gitlab_subscription, seats: 10, max_seats_used: nil)
expect(subscription.seats_remaining).to eq 10
end
end
end
describe '#refresh_seat_attributes!' do describe '#refresh_seat_attributes!' do
subject(:gitlab_subscription) { create(:gitlab_subscription, seats: 3, max_seats_used: 2) } subject(:gitlab_subscription) { create(:gitlab_subscription, seats: 3, max_seats_used: 2) }
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSubscriptions::Reconciliations::CalculateSeatCountDataService, :saas do
using RSpec::Parameterized::TableSyntax
describe '#execute' do
let_it_be(:user) { create(:user) }
let(:should_check_namespace_plan) { true }
before do
allow_next_instance_of(GitlabSubscriptions::Reconciliations::CheckSeatUsageAlertsEligibilityService) do |service|
expect(service).to receive(:execute).and_return(alert_user_overage)
end
allow(::Gitlab::CurrentSettings).to receive(:should_check_namespace_plan?).and_return(should_check_namespace_plan)
end
subject(:execute_service) { described_class.new(namespace: root_ancestor, user: user).execute }
context 'with no subscription' do
let(:root_ancestor) { create(:group) }
let(:alert_user_overage) { true }
before do
root_ancestor.add_owner(user)
end
it { is_expected.to be nil }
end
context 'when conditions are not met' do
let(:max_seats_used) { 9 }
before do
create(
:gitlab_subscription,
namespace: root_ancestor,
plan_code: Plan::ULTIMATE,
seats: 10,
max_seats_used: max_seats_used
)
end
context 'when it is not SaaS' do
let(:alert_user_overage) { true }
let(:root_ancestor) { create(:group) }
let(:should_check_namespace_plan) { false }
before do
root_ancestor.add_owner(user)
end
it { is_expected.to be nil }
end
context 'when namespace is not a group' do
let(:alert_user_overage) { true }
let(:root_ancestor) { create(:namespace, :with_namespace_settings) }
it { is_expected.to be nil }
end
context 'when the alert was dismissed' do
let(:alert_user_overage) { true }
let(:root_ancestor) { create(:group) }
before do
allow(user).to receive(:dismissed_callout_for_group?).and_return(true)
root_ancestor.add_owner(user)
end
it { is_expected.to be nil }
end
context 'when the user does not have admin rights to the group' do
let(:alert_user_overage) { true }
let(:root_ancestor) { create(:group) }
before do
root_ancestor.add_developer(user)
end
it { is_expected.to be nil }
end
context 'when the subscription is not eligible for usage alerts' do
let(:alert_user_overage) { false }
let(:root_ancestor) { create(:group) }
before do
root_ancestor.add_owner(user)
end
it { is_expected.to be nil }
end
context 'when max seats used are more than the subscription seats' do
let(:alert_user_overage) { true }
let(:max_seats_used) { 11 }
let(:root_ancestor) { create(:group) }
before do
root_ancestor.add_owner(user)
end
it { is_expected.to be nil }
end
end
context 'with threshold limits' do
let_it_be(:alert_user_overage) { true }
let_it_be(:root_ancestor) { create(:group) }
before do
create(
:gitlab_subscription,
namespace: root_ancestor,
plan_code: Plan::ULTIMATE,
seats: seats,
max_seats_used: max_seats_used
)
root_ancestor.add_owner(user)
end
context 'when limits are not met' do
where(:seats, :max_seats_used) do
15 | 13
24 | 20
35 | 29
100 | 90
1000 | 949
end
with_them do
it { is_expected.to be nil }
end
end
context 'when limits are met' do
where(:seats, :max_seats_used) do
15 | 14
24 | 22
35 | 32
100 | 93
1000 | 950
end
with_them do
it {
is_expected.to eq({
namespace: root_ancestor,
remaining_seat_count: [seats - max_seats_used, 0].max,
seats_in_use: max_seats_used,
total_seat_count: seats
})
}
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSubscriptions::Reconciliations::CheckSeatUsageAlertsEligibilityService,
:use_clean_rails_memory_store_caching, :saas do
using RSpec::Parameterized::TableSyntax
describe '#execute' do
let_it_be(:namespace) { create(:namespace_with_plan) }
let(:cache_key) do
"subscription:eligible_for_seat_usage_alerts:namespace:#{namespace.gitlab_subscription.cache_key}"
end
subject(:execute_service) { described_class.new(namespace: namespace).execute }
where(:eligible_for_seat_usage_alerts, :expected_response) do
true | true
false | false
end
with_them do
let(:response) { { success: true, eligible_for_seat_usage_alerts: eligible_for_seat_usage_alerts } }
before do
allow(Gitlab::SubscriptionPortal::Client)
.to receive(:subscription_seat_usage_alerts_eligibility)
.and_return(response)
end
it 'returns the correct value' do
expect(execute_service).to eq expected_response
end
it 'caches the query response' do
expect(Rails.cache).to receive(:fetch).with(cache_key, skip_nil: true, expires_in: 1.day).and_call_original
execute_service
end
end
context 'with an unsuccessful CustomersDot query' do
it 'assumes no future renewal' do
allow(Gitlab::SubscriptionPortal::Client).to receive(:subscription_seat_usage_alerts_eligibility).and_return({
success: false
})
expect(execute_service).to be false
end
end
context 'when called with a group' do
let(:namespace) { create(:group_with_plan) }
it 'uses the namespace id' do
expect(Gitlab::SubscriptionPortal::Client)
.to receive(:subscription_seat_usage_alerts_eligibility)
.with(namespace.id)
.and_return({})
execute_service
end
end
context 'when the namespace has no plan' do
let(:namespace) { build(:group) }
it { is_expected.to be false }
end
end
end
0% 加载中 .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册