Commit 849b2a76 authored by Alex Kalderimis's avatar Alex Kalderimis

Merge branch 'jh-fetch_eligible_namespaces' into 'master'

Fetch eligibility for new purchase from customers app

See merge request gitlab-org/gitlab!53857
parents d1cd0f48 e266a52b
......@@ -4,6 +4,8 @@ class SubscriptionsController < ApplicationController
layout 'checkout'
skip_before_action :authenticate_user!, only: [:new, :buy_minutes]
before_action :load_eligible_groups, only: %i[new buy_minutes]
feature_category :purchase
content_security_policy do |p|
......@@ -44,15 +46,10 @@ class SubscriptionsController < ApplicationController
def create
current_user.update(setup_for_company: true) if params[:setup_for_company]
group = params[:selected_group] ? find_group : create_group
if params[:selected_group]
group = current_user.manageable_groups_eligible_for_subscription.find(params[:selected_group])
else
name = Namespace.clean_name(params[:setup_for_company] ? customer_params[:company] : current_user.name)
path = Namespace.clean_path(name)
group = Groups::CreateService.new(current_user, name: name, path: path).execute
return render json: group.errors.to_json unless group.persisted?
end
return not_found if group.nil?
return render json: group.errors.to_json unless group.persisted?
response = Subscriptions::CreateService.new(
current_user,
......@@ -85,6 +82,23 @@ class SubscriptionsController < ApplicationController
params.require(:subscription).permit(:plan_id, :payment_method_id, :quantity)
end
def find_group
selected_group = current_user.manageable_groups.top_most.find(params[:selected_group])
result = GitlabSubscriptions::FilterPurchaseEligibleNamespacesService
.new(user: current_user, namespaces: Array(selected_group))
.execute
result.success? ? result.payload.first : nil
end
def create_group
name = Namespace.clean_name(params[:setup_for_company] ? customer_params[:company] : current_user.name)
path = Namespace.clean_path(name)
Groups::CreateService.new(current_user, name: name, path: path).execute
end
def client
Gitlab::SubscriptionPortal::Client
end
......@@ -99,4 +113,16 @@ class SubscriptionsController < ApplicationController
store_location_for :user, request.fullpath
redirect_to new_user_registration_path(redirect_from: from)
end
def load_eligible_groups
return unless current_user
candidate_groups = current_user.manageable_groups.top_most.with_counts(archived: false)
result = GitlabSubscriptions::FilterPurchaseEligibleNamespacesService
.new(user: current_user, namespaces: candidate_groups)
.execute
@eligible_groups = result.success? ? result.payload : []
end
end
......@@ -3,7 +3,7 @@
module SubscriptionsHelper
include ::Gitlab::Utils::StrongMemoize
def subscription_data
def subscription_data(eligible_groups)
{
setup_for_company: (current_user.setup_for_company == true).to_s,
full_name: current_user.name,
......@@ -12,7 +12,7 @@ module SubscriptionsHelper
plan_id: params[:plan_id],
namespace_id: params[:namespace_id],
new_user: new_user?.to_s,
group_data: group_data.to_json
group_data: present_groups(eligible_groups).to_json
}
end
......@@ -64,8 +64,8 @@ module SubscriptionsHelper
end
end
def group_data
current_user.manageable_groups_eligible_for_subscription.with_counts(archived: false).map do |namespace|
def present_groups(groups)
groups.map do |namespace|
{
id: namespace.id,
name: namespace.name,
......
......@@ -47,10 +47,6 @@ module EE
.where(plans: { name: [nil, *::Plan.default_plans] })
end
scope :eligible_for_subscription, -> do
top_most.in_active_trial.or(top_most.in_default_plan)
end
scope :eligible_for_trial, -> do
left_joins(gitlab_subscription: :hosted_plan)
.where(
......
......@@ -255,10 +255,6 @@ module EE
.any?
end
def manageable_groups_eligible_for_subscription
manageable_groups.eligible_for_subscription.order(:name)
end
def manageable_groups_eligible_for_trial
manageable_groups.eligible_for_trial.order(:name)
end
......
# frozen_string_literal: true
module GitlabSubscriptions
class FilterPurchaseEligibleNamespacesService
include ::Gitlab::Utils::StrongMemoize
def initialize(user:, namespaces:)
@user = user
@namespaces = namespaces
end
def execute
return success([]) if namespaces.empty?
return missing_user_error if user.nil?
if response[:success]
eligible_ids = response[:data].map { |data| data['id'] }.to_set
data = namespaces.filter { |namespace| eligible_ids.include?(namespace.id) }
success(data)
else
error('Failed to fetch namespaces', response.dig(:data, :errors))
end
end
private
attr_reader :user, :namespaces
def success(payload)
ServiceResponse.success(payload: payload)
end
def error(message, payload = nil)
ServiceResponse.error(message: message, payload: payload)
end
def missing_user_error
message = 'User cannot be nil'
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(ArgumentError.new(message))
error(message)
end
def response
strong_memoize(:response) do
Gitlab::SubscriptionPortal::Client.filter_purchase_eligible_namespaces(user, namespaces)
end
end
end
end
- page_title _('Buy CI Minutes')
#js-buy-minutes{ data: subscription_data }
#js-buy-minutes{ data: subscription_data(@eligible_groups) }
- page_title _('Checkout')
#js-new-subscription{ data: subscription_data }
#js-new-subscription{ data: subscription_data(@eligible_groups) }
......@@ -88,8 +88,7 @@ module Gitlab
response = execute_graphql_query({ query: query, variables: { tags: plan_tags } })[:data]
if response['errors'].present?
exception = SubscriptionPortal::Client::ResponseError.new("Received an error from CustomerDot")
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(exception, query: query, response: response)
track_error(query, response)
return error
end
......@@ -121,6 +120,39 @@ module Gitlab
end
end
def filter_purchase_eligible_namespaces(user, namespaces)
query = <<~GQL
query FilterEligibleNamespaces($customerUid: Int!, $namespaces: [GitlabNamespaceInput!]!) {
namespaceEligibility(customerUid: $customerUid, namespaces: $namespaces, eligibleForPurchase: true) {
id
}
}
GQL
namespace_data = namespaces.map do |namespace|
{
id: namespace.id,
parentId: namespace.parent_id,
plan: namespace.actual_plan_name,
trial: !!namespace.trial?
}
end
response = http_post(
"graphql",
admin_headers,
{ query: query, variables: { customerUid: user.id, namespaces: namespace_data } }
)[:data]
if response['errors'].blank?
{ success: true, data: response.dig('data', 'namespaceEligibility') }
else
track_error(query, response)
error(response['errors'])
end
end
private
def execute_graphql_query(params)
......@@ -137,6 +169,14 @@ module Gitlab
EE::SUBSCRIPTIONS_GRAPHQL_URL
end
def track_error(query, response)
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(
SubscriptionPortal::Client::ResponseError.new("Received an error from CustomerDot"),
query: query,
response: response
)
end
def error(errors = nil)
{
success: false,
......
......@@ -20,7 +20,7 @@ RSpec.describe SubscriptionsController do
end
describe 'GET #new' do
subject { get :new, params: { plan_id: 'bronze_id' } }
subject(:get_new) { get :new, params: { plan_id: 'bronze_id' } }
it_behaves_like 'unauthenticated subscription request', 'checkout'
......@@ -31,11 +31,41 @@ RSpec.describe SubscriptionsController do
it { is_expected.to render_template 'layouts/checkout' }
it { is_expected.to render_template :new }
context 'when there are groups eligible for the subscription' do
let_it_be(:group) { create(:group) }
before do
group.add_owner(user)
allow_next_instance_of(GitlabSubscriptions::FilterPurchaseEligibleNamespacesService, user: user, namespaces: [group]) do |instance|
allow(instance).to receive(:execute).and_return(instance_double(ServiceResponse, success?: true, payload: [group]))
end
end
it 'assigns the eligible groups for the subscription' do
get_new
expect(assigns(:eligible_groups)).to eq [group]
end
end
context 'when there are no eligible groups for the subscription' do
it 'assigns eligible groups as an empty array' do
allow_next_instance_of(GitlabSubscriptions::FilterPurchaseEligibleNamespacesService, user: user, namespaces: []) do |instance|
allow(instance).to receive(:execute).and_return(instance_double(ServiceResponse, success?: true, payload: []))
end
get_new
expect(assigns(:eligible_groups)).to eq []
end
end
end
end
describe 'GET #buy_minutes' do
subject { get :buy_minutes, params: { plan_id: 'bronze_id' } }
subject(:buy_minutes) { get :buy_minutes, params: { plan_id: 'bronze_id' } }
it_behaves_like 'unauthenticated subscription request', 'buy_minutes'
......@@ -46,6 +76,36 @@ RSpec.describe SubscriptionsController do
it { is_expected.to render_template 'layouts/checkout' }
it { is_expected.to render_template :buy_minutes }
context 'when there are groups eligible for the subscription' do
let_it_be(:group) { create(:group) }
before do
group.add_owner(user)
allow_next_instance_of(GitlabSubscriptions::FilterPurchaseEligibleNamespacesService, user: user, namespaces: [group]) do |instance|
allow(instance).to receive(:execute).and_return(instance_double(ServiceResponse, success?: true, payload: [group]))
end
end
it 'assigns the eligible groups for the subscription' do
buy_minutes
expect(assigns(:eligible_groups)).to eq [group]
end
end
context 'when there are no eligible groups for the subscription' do
it 'assigns eligible groups as an empty array' do
allow_next_instance_of(GitlabSubscriptions::FilterPurchaseEligibleNamespacesService, user: user, namespaces: []) do |instance|
allow(instance).to receive(:execute).and_return(instance_double(ServiceResponse, success?: true, payload: []))
end
buy_minutes
expect(assigns(:eligible_groups)).to eq []
end
end
end
context 'with :new_route_ci_minutes_purchase disabled' do
......@@ -218,7 +278,6 @@ RSpec.describe SubscriptionsController do
end
context 'when selecting an existing group' do
let_it_be(:selected_group) { create(:group) }
let(:params) do
{
selected_group: selected_group.id,
......@@ -227,21 +286,63 @@ RSpec.describe SubscriptionsController do
}
end
before do
selected_group.add_owner(user)
end
context 'when the selected group is eligible for a new subscription' do
let_it_be(:selected_group) { create(:group) }
before do
selected_group.add_owner(user)
allow_next_instance_of(
GitlabSubscriptions::FilterPurchaseEligibleNamespacesService,
user: user,
namespaces: [selected_group]
) do |instance|
allow(instance)
.to receive(:execute)
.and_return(instance_double(ServiceResponse, success?: true, payload: [selected_group]))
end
end
it 'does not create a group' do
expect { subject }.to not_change { Group.count }
it 'does not create a group' do
expect { subject }.to not_change { Group.count }
end
it 'returns the selected group location in JSON format' do
subject
plan_id = params[:subscription][:plan_id]
quantity = params[:subscription][:quantity]
expect(response.body).to eq({ location: "/#{selected_group.path}?plan_id=#{plan_id}&purchased_quantity=#{quantity}" }.to_json)
end
end
it 'returns the selected group location in JSON format' do
subject
context 'when the selected group is ineligible for a new subscription' do
let_it_be(:selected_group) { create(:group) }
before do
selected_group.add_owner(user)
allow_next_instance_of(
GitlabSubscriptions::FilterPurchaseEligibleNamespacesService,
user: user,
namespaces: [selected_group]
) do |instance|
allow(instance)
.to receive(:execute)
.and_return(instance_double(ServiceResponse, success?: true, payload: []))
end
end
plan_id = params[:subscription][:plan_id]
quantity = params[:subscription][:quantity]
it 'does not create a group' do
expect { subject }.to not_change { Group.count }
end
expect(response.body).to eq({ location: "/#{selected_group.path}?plan_id=#{plan_id}&purchased_quantity=#{quantity}" }.to_json)
it 'returns a 404 not found' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when selected group is a sub group' do
......
......@@ -46,7 +46,7 @@ RSpec.describe SubscriptionsHelper do
group.add_owner(user)
end
subject { helper.subscription_data }
subject { helper.subscription_data([group]) }
it { is_expected.to include(setup_for_company: 'false') }
it { is_expected.to include(full_name: 'First Last') }
......
......@@ -244,4 +244,100 @@ RSpec.describe Gitlab::SubscriptionPortal::Clients::Graphql do
end
end
end
describe '#filter_purchase_eligible_namespaces' do
subject do
client.filter_purchase_eligible_namespaces(user, [user_namespace, group_namespace, subgroup])
end
let_it_be(:user) { create(:user) }
let_it_be(:user_namespace) { user.namespace }
let_it_be(:group_namespace) { create(:group) }
let_it_be(:subgroup) { create(:group, parent: group_namespace) }
let(:headers) do
{
"Accept" => "application/json",
"Content-Type" => "application/json",
"X-Admin-Email" => "gl_com_api@gitlab.com",
"X-Admin-Token" => "customer_admin_token"
}
end
let(:variables) do
{
customerUid: user.id,
namespaces: [
{ id: user_namespace.id, parentId: nil, plan: "default", trial: false },
{ id: group_namespace.id, parentId: nil, plan: "default", trial: false },
{ id: subgroup.id, parentId: group_namespace.id, plan: "default", trial: false }
]
}
end
let(:params) do
{
variables: variables,
query: <<~GQL
query FilterEligibleNamespaces($customerUid: Int!, $namespaces: [GitlabNamespaceInput!]!) {
namespaceEligibility(customerUid: $customerUid, namespaces: $namespaces, eligibleForPurchase: true) {
id
}
}
GQL
}
end
context 'when the response is successful' do
it 'returns the namespace data', :aggregate_failures do
response = {
data: {
'data' => {
'namespaceEligibility' => [{ 'id' => 1 }, { 'id' => 3 }]
}
}
}
expect(client).to receive(:http_post).with('graphql', headers, params).and_return(response)
expect(subject).to eq(success: true, data: [{ 'id' => 1 }, { 'id' => 3 }])
end
end
context 'when the response is unsuccessful' do
it 'returns the error message', :aggregate_failures do
response = {
data: {
"data" => {
"namespaceEligibility" => nil
},
"errors" => [
{
"message" => "You must be logged in to access this resource",
"locations" => [{ "line" => 2, "column" => 3 }],
"path" => ["namespaceEligibility"]
}
]
}
}
expect(Gitlab::ErrorTracking)
.to receive(:track_and_raise_for_dev_exception)
.with(
a_kind_of(Gitlab::SubscriptionPortal::Client::ResponseError),
query: params[:query], response: response[:data])
expect(client).to receive(:http_post).with('graphql', headers, params).and_return(response)
expect(subject).to eq(
success: false,
errors: [{
"locations" => [{ "column" => 3, "line" => 2 }],
"message" => "You must be logged in to access this resource",
"path" => ["namespaceEligibility"]
}]
)
end
end
end
end
......@@ -221,92 +221,6 @@ RSpec.describe Namespace do
end
end
describe '.eligible_for_subscription' do
let_it_be(:namespace) { create :namespace }
let_it_be(:group) { create :group }
let_it_be(:subgroup) { create(:group, parent: group) }
subject { described_class.eligible_for_subscription.ids }
context 'when there is no subscription' do
it { is_expected.to contain_exactly(group.id, namespace.id) }
end
context 'when there is a subscription' do
context 'with a plan that is eligible for a trial' do
where(plan: ::Plan::PLANS_ELIGIBLE_FOR_TRIAL)
with_them do
context 'and has not yet been trialed' do
before do
create :gitlab_subscription, plan, namespace: namespace
create :gitlab_subscription, plan, namespace: group
create :gitlab_subscription, plan, namespace: subgroup
end
it { is_expected.to contain_exactly(group.id, namespace.id) }
end
context 'but has already had a trial' do
before do
create :gitlab_subscription, plan, namespace: namespace
create :gitlab_subscription, plan, :expired_trial, namespace: group
create :gitlab_subscription, plan, :expired_trial, namespace: subgroup
end
it { is_expected.to contain_exactly(group.id, namespace.id) }
end
context 'but is currently being trialed' do
before do
create :gitlab_subscription, plan, namespace: namespace
create :gitlab_subscription, plan, :active_trial, namespace: group
create :gitlab_subscription, plan, :active_trial, namespace: subgroup
end
it { is_expected.to contain_exactly(group.id, namespace.id) }
end
end
end
context 'in active trial ultimate plan' do
using RSpec::Parameterized::TableSyntax
where(:plan_name) do
[
[::Plan::GOLD],
[::Plan::ULTIMATE]
]
end
with_them do
before do
create :gitlab_subscription, plan_name, :active_trial, namespace: namespace
create :gitlab_subscription, plan_name, :active_trial, namespace: group
create :gitlab_subscription, plan_name, :active_trial, namespace: subgroup
end
it { is_expected.to contain_exactly(group.id, namespace.id) }
end
end
context 'with a paid plan and not in trial' do
where(plan: ::Plan::PAID_HOSTED_PLANS)
with_them do
context 'and has not yet been trialed' do
before do
create :gitlab_subscription, plan, namespace: namespace
create :gitlab_subscription, plan, namespace: group
end
it { is_expected.to be_empty }
end
end
end
end
end
describe '.eligible_for_trial' do
let_it_be(:namespace) { create :namespace }
......
......@@ -1009,88 +1009,6 @@ RSpec.describe User do
end
end
describe '#manageable_groups_eligible_for_subscription' do
let_it_be(:user) { create(:user) }
let_it_be(:licensed_group) { create(:group_with_plan, plan: :bronze_plan) }
let_it_be(:free_group_z) { create(:group_with_plan, plan: :free_plan, name: 'AZ') }
let_it_be(:free_group_a) { create(:group_with_plan, plan: :free_plan, name: 'AA') }
let_it_be(:sub_group) { create(:group, name: 'SubGroup', parent: free_group_a) }
let_it_be(:trial_group) { create(:group_with_plan, plan: :ultimate_plan, trial_ends_on: Date.current + 1.day, name: 'AB') }
subject { user.manageable_groups_eligible_for_subscription }
context 'user with no groups' do
it { is_expected.to eq [] }
end
context 'owner of a licensed group' do
before do
licensed_group.add_owner(user)
end
it { is_expected.not_to include licensed_group }
end
context 'guest of a free group' do
before do
free_group_a.add_guest(user)
end
it { is_expected.not_to include free_group_a }
end
context 'developer of a free group' do
before do
free_group_a.add_developer(user)
end
it { is_expected.not_to include free_group_a }
end
context 'maintainer of a free group' do
before do
free_group_a.add_maintainer(user)
end
it { is_expected.to include free_group_a }
end
context 'owner of 2 free groups' do
before do
free_group_a.add_owner(user)
free_group_z.add_owner(user)
end
it { is_expected.to eq [free_group_a, free_group_z] }
it { is_expected.not_to include(sub_group) }
end
context 'developer of a trial group' do
before do
trial_group.add_developer(user)
end
it { is_expected.not_to include(trial_group) }
end
context 'owner of a trial group' do
before do
trial_group.add_owner(user)
end
it { is_expected.to include(trial_group) }
end
context 'maintainer of a trial group' do
before do
trial_group.add_maintainer(user)
end
it { is_expected.to include(trial_group) }
end
end
describe '#manageable_groups_eligible_for_trial' do
let_it_be(:user) { create :user }
let_it_be(:non_trialed_group_z) { create :group_with_plan, name: 'Zeta', plan: :free_plan }
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSubscriptions::FilterPurchaseEligibleNamespacesService do
describe '#execute' do
let_it_be(:user) { build(:user) }
context 'when no namespaces are supplied' do
it 'returns an empty array', :aggregate_failures do
result = described_class.new(user: user, namespaces: []).execute
expect(result).to be_success
expect(result.payload).to eq []
end
end
context 'when no user is supplied' do
subject(:service) { described_class.new(user: nil, namespaces: [build(:namespace)]) }
it 'logs and returns an error message', :aggregate_failures do
expect(Gitlab::ErrorTracking)
.to receive(:track_and_raise_for_dev_exception)
.with an_instance_of(ArgumentError)
result = service.execute
expect(result).to be_error
expect(result.message).to eq 'User cannot be nil'
expect(result.payload).to be_nil
end
end
context 'when the http request fails' do
subject(:service) { described_class.new(user: user, namespaces: [create(:namespace)]) }
before do
allow(Gitlab::SubscriptionPortal::Client).to receive(:filter_purchase_eligible_namespaces).and_return(
success: false, data: { errors: 'error' }
)
end
it 'returns an error message', :aggregate_failures do
result = service.execute
expect(result).to be_error
expect(result.message).to eq 'Failed to fetch namespaces'
expect(result.payload).to eq 'error'
end
end
context 'when all the namespaces are eligible' do
let(:namespace_1) { create(:namespace) }
let(:namespace_2) { create(:namespace) }
before do
allow(Gitlab::SubscriptionPortal::Client).to receive(:filter_purchase_eligible_namespaces).and_return(
success: true, data: [{ 'id' => namespace_1.id }, { 'id' => namespace_2.id }]
)
end
it 'does not filter any namespaces', :aggregate_failures do
namespaces = [namespace_1, namespace_2]
result = described_class.new(user: user, namespaces: namespaces).execute
expect(result).to be_success
expect(result.payload).to eq namespaces
end
end
context 'when the user has a namespace ineligible' do
let(:namespace_1) { create(:namespace) }
let(:namespace_2) { create(:namespace) }
before do
allow(Gitlab::SubscriptionPortal::Client).to receive(:filter_purchase_eligible_namespaces).and_return(
success: true, data: [{ 'id' => namespace_1.id }]
)
end
it 'is filtered from the results', :aggregate_failures do
namespaces = [namespace_1, namespace_2]
result = described_class.new(user: user, namespaces: namespaces).execute
expect(result).to be_success
expect(result.payload).to eq [namespace_1]
end
end
end
end
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