Commit db461b03 authored by Vamsi Vempati's avatar Vamsi Vempati Committed by Paul Slaughter

Fixed incorrect minimum number of users on subscription purchase flow

- Consumed new billable members count GraphQL endpoint with plan
- Added label description for min number of users
- Added loading state for new API call
- Reset number of users to 1 if no group is selected

Changelog: fixed
EE: true
parent 8b714a5f
query getBillableMembersCount($fullPath: ID!, $requestedHostedPlan: String) {
group(fullPath: $fullPath) {
id
billableMembersCount(requestedHostedPlan: $requestedHostedPlan)
}
}
<script> <script>
import { GlAlert, GlFormGroup, GlFormSelect, GlFormInput, GlSprintf, GlLink } from '@gitlab/ui'; import {
GlAlert,
GlFormGroup,
GlFormSelect,
GlFormInput,
GlSprintf,
GlLink,
GlLoadingIcon,
} from '@gitlab/ui';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { mapState, mapGetters, mapActions } from 'vuex'; import { mapState, mapGetters, mapActions } from 'vuex';
import { QSR_RECONCILIATION_PATH, STEP_SUBSCRIPTION_DETAILS } from 'ee/subscriptions/constants'; import { QSR_RECONCILIATION_PATH, STEP_SUBSCRIPTION_DETAILS } from 'ee/subscriptions/constants';
...@@ -9,6 +17,7 @@ import { sprintf, s__, __ } from '~/locale'; ...@@ -9,6 +17,7 @@ import { sprintf, s__, __ } from '~/locale';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow'; import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
import { helpPagePath } from '~/helpers/help_page_helper'; import { helpPagePath } from '~/helpers/help_page_helper';
import getBillableMembersCountQuery from 'ee/subscriptions/graphql/queries/billable_members_count.query.graphql';
export default { export default {
components: { components: {
...@@ -18,12 +27,44 @@ export default { ...@@ -18,12 +27,44 @@ export default {
GlFormInput, GlFormInput,
GlSprintf, GlSprintf,
GlLink, GlLink,
GlLoadingIcon,
Step, Step,
}, },
directives: { directives: {
autofocusonshow, autofocusonshow,
}, },
mixins: [Tracking.mixin()], mixins: [Tracking.mixin()],
apollo: {
billableMembersMin: {
query: getBillableMembersCountQuery,
loadingKey: 'isLoading',
variables() {
return {
fullPath: this.selectedGroupFullPath,
requestedHostedPlan: this.selectedPlanDetails.code,
};
},
update(data) {
this.resetError();
return data.group.billableMembersCount || 1;
},
skip() {
return this.shouldSkipQuery;
},
error(error) {
this.handleError(error);
},
},
},
data() {
return {
billableMembersMin: 1,
errorMessage: '',
isLoading: 0,
showError: false,
};
},
computed: { computed: {
...mapState([ ...mapState([
'availablePlans', 'availablePlans',
...@@ -39,10 +80,14 @@ export default { ...@@ -39,10 +80,14 @@ export default {
'selectedPlanText', 'selectedPlanText',
'selectedPlanDetails', 'selectedPlanDetails',
'selectedGroupId', 'selectedGroupId',
'selectedGroupData',
'isGroupSelected', 'isGroupSelected',
'selectedGroupUsers',
'selectedGroupName', 'selectedGroupName',
'isSelectedGroupPresent',
]), ]),
hasError() {
return Boolean(this.errorMessage);
},
selectedPlanModel: { selectedPlanModel: {
get() { get() {
return this.selectedPlan; return this.selectedPlan;
...@@ -78,6 +123,23 @@ export default { ...@@ -78,6 +123,23 @@ export default {
selectedPlanTextLine() { selectedPlanTextLine() {
return sprintf(this.$options.i18n.selectedPlan, { selectedPlanText: this.selectedPlanText }); return sprintf(this.$options.i18n.selectedPlan, { selectedPlanText: this.selectedPlanText });
}, },
selectedGroupFullPath() {
return this.selectedGroupData?.fullPath;
},
shouldSkipQuery() {
return (
!this.selectedGroupFullPath || !this.hasSelectedPlan || this.shouldDisableNumberOfUsers
);
},
numberOfUsersLabelDescription() {
if (this.shouldSkipQuery || this.hasError) {
return null;
}
return sprintf(this.$options.i18n.numberOfUsersLabelDescription, {
minimumNumberOfUsers: this.billableMembersMin,
});
},
hasAtLeastOneUser() { hasAtLeastOneUser() {
return this.numberOfUsers > 0; return this.numberOfUsers > 0;
}, },
...@@ -94,7 +156,7 @@ export default { ...@@ -94,7 +156,7 @@ export default {
return true; return true;
}, },
isSelectedUsersEqualOrGreaterThanGroupUsers() { isSelectedUsersEqualOrGreaterThanGroupUsers() {
return this.numberOfUsers >= this.selectedGroupUsers; return this.numberOfUsers >= this.billableMembersMin;
}, },
isValid() { isValid() {
return ( return (
...@@ -132,6 +194,16 @@ export default { ...@@ -132,6 +194,16 @@ export default {
return this.isNewUser && !this.isSetupForCompany; return this.isNewUser && !this.isSetupForCompany;
}, },
}, },
watch: {
billableMembersMin(val) {
this.updateNumberOfUsers(val);
},
isSelectedGroupPresent(isSelectedGroupPresent) {
if (!isSelectedGroupPresent) {
this.billableMembersMin = 1;
}
},
},
methods: { methods: {
...mapActions([ ...mapActions([
'updateSelectedPlan', 'updateSelectedPlan',
...@@ -140,6 +212,17 @@ export default { ...@@ -140,6 +212,17 @@ export default {
'updateNumberOfUsers', 'updateNumberOfUsers',
'updateOrganizationName', 'updateOrganizationName',
]), ]),
hideError() {
this.showError = false;
},
handleError(error) {
this.errorMessage = error || __('An unexpected error occurred');
this.showError = true;
},
resetError() {
this.errorMessage = '';
this.showError = false;
},
trackStepTransition() { trackStepTransition() {
this.track('click_button', { this.track('click_button', {
label: 'update_plan_type', label: 'update_plan_type',
...@@ -167,6 +250,10 @@ export default { ...@@ -167,6 +250,10 @@ export default {
createNewGroupDescription: s__("Checkout|You'll create your new group after checkout"), createNewGroupDescription: s__("Checkout|You'll create your new group after checkout"),
organizationNameLabel: s__('Checkout|Name of company or organization using GitLab'), organizationNameLabel: s__('Checkout|Name of company or organization using GitLab'),
numberOfUsersLabel: s__('Checkout|Number of users'), numberOfUsersLabel: s__('Checkout|Number of users'),
numberOfUsersLabelDescription: s__(
'Checkout|This number must be %{minimumNumberOfUsers} (your seats in use) or more.',
),
loadingText: s__('Checkout|Calculating your subscription...'),
needMoreUsersLink: s__('Checkout|Need more users? Purchase GitLab for your %{company}.'), needMoreUsersLink: s__('Checkout|Need more users? Purchase GitLab for your %{company}.'),
companyOrTeam: s__('Checkout|company or team'), companyOrTeam: s__('Checkout|company or team'),
selectedPlan: s__('Checkout|%{selectedPlanText} plan'), selectedPlan: s__('Checkout|%{selectedPlanText} plan'),
...@@ -182,6 +269,23 @@ export default { ...@@ -182,6 +269,23 @@ export default {
</script> </script>
<template> <template>
<step <step
v-if="isLoading"
:step-id="$options.stepId"
:title="$options.i18n.stepTitle"
:is-valid="false"
>
<template #body>
<div
data-testid="subscription-loading-container"
class="gl-display-flex gl-h-200! gl-justify-content-center gl-align-items-center gl-flex-direction-column"
>
<gl-loading-icon v-if="true" size="md" />
<span>{{ $options.i18n.loadingText }}</span>
</div>
</template>
</step>
<step
v-else
:step-id="$options.stepId" :step-id="$options.stepId"
:title="$options.i18n.stepTitle" :title="$options.i18n.stepTitle"
:is-valid="isValid" :is-valid="isValid"
...@@ -190,6 +294,14 @@ export default { ...@@ -190,6 +294,14 @@ export default {
@stepEdit="trackStepEdit" @stepEdit="trackStepEdit"
> >
<template #body> <template #body>
<gl-alert
v-if="hasError && showError"
data-testid="error-message"
class="gl-mb-5"
variant="danger"
@dismiss="hideError"
>{{ errorMessage }}</gl-alert
>
<gl-form-group :label="$options.i18n.selectedPlanLabel" label-size="sm" class="mb-3"> <gl-form-group :label="$options.i18n.selectedPlanLabel" label-size="sm" class="mb-3">
<gl-form-select <gl-form-select
v-model="selectedPlanModel" v-model="selectedPlanModel"
...@@ -222,15 +334,18 @@ export default { ...@@ -222,15 +334,18 @@ export default {
</gl-form-group> </gl-form-group>
<div class="combined d-flex"> <div class="combined d-flex">
<gl-form-group <gl-form-group
data-testid="number-of-users-field"
:label="$options.i18n.numberOfUsersLabel" :label="$options.i18n.numberOfUsersLabel"
label-size="sm" label-size="sm"
class="number gl-mb-0" class="gl-mb-0"
:label-description="numberOfUsersLabelDescription"
> >
<gl-form-input <gl-form-input
ref="number-of-users" ref="number-of-users"
v-model.number="numberOfUsersModel" v-model.number="numberOfUsersModel"
class="number"
type="number" type="number"
:min="selectedGroupUsers" :min="billableMembersMin"
:disabled="shouldDisableNumberOfUsers" :disabled="shouldDisableNumberOfUsers"
data-qa-selector="number_of_users" data-qa-selector="number_of_users"
/> />
......
...@@ -11,14 +11,12 @@ import * as types from './mutation_types'; ...@@ -11,14 +11,12 @@ import * as types from './mutation_types';
export const updateSelectedPlan = ({ commit, getters }, selectedPlan) => { export const updateSelectedPlan = ({ commit, getters }, selectedPlan) => {
commit(types.UPDATE_SELECTED_PLAN, selectedPlan); commit(types.UPDATE_SELECTED_PLAN, selectedPlan);
commit(types.UPDATE_NUMBER_OF_USERS, getters.selectedGroupUsers);
trackCheckout(selectedPlan, getters.confirmOrderParams?.subscription?.quantity); trackCheckout(selectedPlan, getters.confirmOrderParams?.subscription?.quantity);
}; };
export const updateSelectedGroup = ({ commit, getters }, selectedGroup) => { export const updateSelectedGroup = ({ commit }, selectedGroup) => {
commit(types.UPDATE_SELECTED_GROUP, selectedGroup); commit(types.UPDATE_SELECTED_GROUP, selectedGroup);
commit(types.UPDATE_ORGANIZATION_NAME, null); commit(types.UPDATE_ORGANIZATION_NAME, null);
commit(types.UPDATE_NUMBER_OF_USERS, getters.selectedGroupUsers);
}; };
export const toggleIsSetupForCompany = ({ state, commit }) => { export const toggleIsSetupForCompany = ({ state, commit }) => {
......
...@@ -60,28 +60,18 @@ export const usersPresent = (state) => state.numberOfUsers > 0; ...@@ -60,28 +60,18 @@ export const usersPresent = (state) => state.numberOfUsers > 0;
export const isGroupSelected = (state) => export const isGroupSelected = (state) =>
state.selectedGroup !== null && state.selectedGroup !== NEW_GROUP; state.selectedGroup !== null && state.selectedGroup !== NEW_GROUP;
export const isSelectedGroupPresent = (state, getters) => { export const isSelectedGroupPresent = (state, getters) => Boolean(getters.selectedGroupData);
return (
getters.isGroupSelected && state.groupData.some((group) => group.value === state.selectedGroup)
);
};
export const selectedGroupUsers = (state, getters) => { export const selectedGroupData = (state, getters) => {
if (!getters.isGroupSelected) { if (!getters.isGroupSelected) {
return 1; return null;
} else if (getters.isSelectedGroupPresent && getters.isUltimatePlan) {
const selectedGroup = state.groupData.find((group) => group.value === state.selectedGroup);
return selectedGroup.numberOfUsers - selectedGroup.numberOfGuests;
} else if (getters.isSelectedGroupPresent) {
return state.groupData.find((group) => group.value === state.selectedGroup).numberOfUsers;
} }
return null; return state.groupData.find((group) => group.value === state.selectedGroup);
}; };
export const selectedGroupName = (state, getters) => { export const selectedGroupName = (state, getters) => {
if (!getters.isGroupSelected) return null; return getters.selectedGroupData?.text;
return state.groupData.find((group) => group.value === state.selectedGroup).text;
}; };
export const selectedGroupId = (state, getters) => export const selectedGroupId = (state, getters) =>
......
...@@ -14,8 +14,7 @@ const parseGroupData = (groupData) => ...@@ -14,8 +14,7 @@ const parseGroupData = (groupData) =>
JSON.parse(groupData).map((group) => ({ JSON.parse(groupData).map((group) => ({
value: group.id, value: group.id,
text: group.name, text: group.name,
numberOfUsers: group.users, fullPath: group.full_path,
numberOfGuests: group.guests,
})); }));
const determineSelectedPlan = (planId, plans) => { const determineSelectedPlan = (planId, plans) => {
...@@ -25,20 +24,6 @@ const determineSelectedPlan = (planId, plans) => { ...@@ -25,20 +24,6 @@ const determineSelectedPlan = (planId, plans) => {
return plans[0] && plans[0].value; return plans[0] && plans[0].value;
}; };
const determineNumberOfUsers = (groupId, groups) => {
if (!groupId || !groups) {
return 1;
}
const chosenGroup = groups.find((group) => group.value === groupId);
if (chosenGroup?.numberOfUsers > 1) {
return chosenGroup.numberOfUsers;
}
return 1;
};
export default ({ export default ({
availablePlans: plansData = '[]', availablePlans: plansData = '[]',
planId, planId,
...@@ -66,7 +51,7 @@ export default ({ ...@@ -66,7 +51,7 @@ export default ({
groupData: groups, groupData: groups,
selectedGroup: groupId, selectedGroup: groupId,
organizationName: null, organizationName: null,
numberOfUsers: determineNumberOfUsers(groupId, groups), numberOfUsers: 1,
country: null, country: null,
streetAddressLine1: null, streetAddressLine1: null,
streetAddressLine2: null, streetAddressLine2: null,
......
...@@ -66,7 +66,8 @@ module SubscriptionsHelper ...@@ -66,7 +66,8 @@ module SubscriptionsHelper
account_id: account_id, account_id: account_id,
name: namespace.name, name: namespace.name,
users: namespace.member_count, users: namespace.member_count,
guests: namespace.guest_count guests: namespace.guest_count,
full_path: namespace.full_path
} }
end end
end end
...@@ -39,11 +39,8 @@ describe('Subscriptions Actions', () => { ...@@ -39,11 +39,8 @@ describe('Subscriptions Actions', () => {
await testAction( await testAction(
actions.updateSelectedPlan, actions.updateSelectedPlan,
'planId', 'planId',
{ selectedGroupUsers: 4 }, {},
[ [{ type: 'UPDATE_SELECTED_PLAN', payload: 'planId' }],
{ type: 'UPDATE_SELECTED_PLAN', payload: 'planId' },
{ type: 'UPDATE_NUMBER_OF_USERS', payload: 4 },
],
[], [],
); );
}); });
...@@ -54,11 +51,10 @@ describe('Subscriptions Actions', () => { ...@@ -54,11 +51,10 @@ describe('Subscriptions Actions', () => {
await testAction( await testAction(
actions.updateSelectedGroup, actions.updateSelectedGroup,
'groupId', 'groupId',
{ selectedGroupUsers: 3 }, {},
[ [
{ type: 'UPDATE_SELECTED_GROUP', payload: 'groupId' }, { type: 'UPDATE_SELECTED_GROUP', payload: 'groupId' },
{ type: 'UPDATE_ORGANIZATION_NAME', payload: null }, { type: 'UPDATE_ORGANIZATION_NAME', payload: null },
{ type: 'UPDATE_NUMBER_OF_USERS', payload: 3 },
], ],
[], [],
); );
......
...@@ -151,90 +151,56 @@ describe('Subscriptions Getters', () => { ...@@ -151,90 +151,56 @@ describe('Subscriptions Getters', () => {
}); });
}); });
describe('selectedGroupUsers', () => { describe('isSelectedGroupPresent', () => {
it('returns 1 when no group is selected', () => { it('returns true when known group is selected', () => {
expect( expect(getters.isSelectedGroupPresent({}, { selectedGroupData: {} })).toBe(true);
getters.selectedGroupUsers(
{ groupData: [{ numberOfUsers: 3, value: 123 }], selectedGroup: 123 },
{ isGroupSelected: false },
),
).toBe(1);
});
it('returns `null` when a group is selected, but not present', () => {
expect(
getters.selectedGroupUsers(
{ groupData: [{ numberOfUsers: 3, value: 123 }], selectedGroup: 123 },
{ isGroupSelected: true, isSelectedGroupPresent: false },
),
).toBe(null);
});
it('returns the number of users of the selected group when a group is selected and plan is not ultimate', () => {
expect(
getters.selectedGroupUsers(
{ groupData: [{ numberOfUsers: 3, numberOfGuests: 1, value: 123 }], selectedGroup: 123 },
{ isGroupSelected: true, isSelectedGroupPresent: true, isUltimatePlan: false },
),
).toBe(3);
}); });
it('returns difference between the number of users and guests of the selected group if the selected plan is ultimate', () => { it('returns false when no group is selected', () => {
expect( expect(getters.isSelectedGroupPresent({}, { selectedGroupData: undefined })).toBe(false);
getters.selectedGroupUsers(
{ groupData: [{ numberOfUsers: 3, numberOfGuests: 1, value: 123 }], selectedGroup: 123 },
{ isGroupSelected: true, isSelectedGroupPresent: true, isUltimatePlan: true },
),
).toBe(2);
}); });
}); });
describe('isSelectedGroupPresent', () => { describe('selectedGroupData', () => {
it('returns false when group is not selected', () => { it('returns null when no group is selected', () => {
expect( expect(
getters.isSelectedGroupPresent( getters.selectedGroupData(
{ groupData: [{ numberOfUsers: 3, value: 123 }], selectedGroup: null }, {
groupData: [
{ text: 'Not selected group', value: 'not-selected-group' },
{ text: 'Selected group', value: 'selected-group' },
],
},
{ isGroupSelected: false }, { isGroupSelected: false },
), ),
).toBe(false); ).toBe(null);
});
it('returns false when group is selected, but not present', () => {
expect(
getters.isSelectedGroupPresent(
{ groupData: [{ numberOfUsers: 3, value: 123 }], selectedGroup: 321 },
{ isGroupSelected: true },
),
).toBe(false);
}); });
it('returns true when group is selected and is present', () => { it('returns the selected group when a group is selected', () => {
expect( expect(
getters.isSelectedGroupPresent( getters.selectedGroupData(
{ groupData: [{ numberOfUsers: 3, value: 123 }], selectedGroup: 123 }, {
selectedGroup: 'selected-group',
groupData: [
{ text: 'Not selected group', value: 'not-selected-group' },
{ text: 'Selected group', value: 'selected-group' },
],
},
{ isGroupSelected: true }, { isGroupSelected: true },
), ),
).toBe(true); ).toEqual({ text: 'Selected group', value: 'selected-group' });
}); });
}); });
describe('selectedGroupName', () => { describe('selectedGroupName', () => {
it('returns null when no group is selected', () => { it('returns null when no group is selected', () => {
expect( expect(getters.selectedGroupName({}, { selectedGroupData: undefined })).toBe(undefined);
getters.selectedGroupName(
{ groupData: [{ text: 'Selected group', value: 123 }], selectedGroup: 123 },
{ isGroupSelected: false },
),
).toBe(null);
}); });
it('returns the text attribute of the selected group when a group is selected', () => { it('returns the text attribute of the selected group when a group is selected', () => {
expect( expect(getters.selectedGroupName({}, { selectedGroupData: { text: 'Selected group' } })).toBe(
getters.selectedGroupName( 'Selected group',
{ groupData: [{ text: 'Selected group', value: 123 }], selectedGroup: 123 }, );
{ isGroupSelected: true },
),
).toBe('Selected group');
}); });
}); });
......
...@@ -10,8 +10,8 @@ describe('projectsSelector default state', () => { ...@@ -10,8 +10,8 @@ describe('projectsSelector default state', () => {
]; ];
const groupData = [ const groupData = [
{ id: 132, name: 'My first group', users: 3, guests: 1 }, { id: 132, name: 'My first group', full_path: 'my-first-group' },
{ id: 483, name: 'My second group', users: 12, guests: 0 }, { id: 483, name: 'My second group', full_path: 'my-second-group' },
]; ];
const initialData = { const initialData = {
...@@ -98,8 +98,8 @@ describe('projectsSelector default state', () => { ...@@ -98,8 +98,8 @@ describe('projectsSelector default state', () => {
describe('groupData', () => { describe('groupData', () => {
it('sets the groupData to the provided parsed groupData', () => { it('sets the groupData to the provided parsed groupData', () => {
expect(state.groupData).toEqual([ expect(state.groupData).toEqual([
{ value: 132, text: 'My first group', numberOfUsers: 3, numberOfGuests: 1 }, { value: 132, text: 'My first group', fullPath: 'my-first-group' },
{ value: 483, text: 'My second group', numberOfUsers: 12, numberOfGuests: 0 }, { value: 483, text: 'My second group', fullPath: 'my-second-group' },
]); ]);
}); });
......
...@@ -4,8 +4,13 @@ import stepListQuery from 'ee/vue_shared/purchase_flow/graphql/queries/step_list ...@@ -4,8 +4,13 @@ import stepListQuery from 'ee/vue_shared/purchase_flow/graphql/queries/step_list
import resolvers from 'ee/vue_shared/purchase_flow/graphql/resolvers'; import resolvers from 'ee/vue_shared/purchase_flow/graphql/resolvers';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
export function createMockApolloProvider(stepList, initialStepIndex = 0, additionalResolvers = {}) { export function createMockApolloProvider(
const mockApollo = createMockApollo([], merge({}, resolvers, additionalResolvers)); stepList,
initialStepIndex = 0,
additionalResolvers = {},
handlers = [],
) {
const mockApollo = createMockApollo(handlers, merge({}, resolvers, additionalResolvers));
mockApollo.clients.defaultClient.cache.writeQuery({ mockApollo.clients.defaultClient.cache.writeQuery({
query: stepListQuery, query: stepListQuery,
data: { stepList }, data: { stepList },
......
...@@ -51,7 +51,7 @@ RSpec.describe SubscriptionsHelper do ...@@ -51,7 +51,7 @@ RSpec.describe SubscriptionsHelper do
it { is_expected.to include(plan_id: 'bronze_id') } it { is_expected.to include(plan_id: 'bronze_id') }
it { is_expected.to include(namespace_id: group.id.to_s) } it { is_expected.to include(namespace_id: group.id.to_s) }
it { is_expected.to include(source: 'some_source') } it { is_expected.to include(source: 'some_source') }
it { is_expected.to include(group_data: %Q{[{"id":#{group.id},"account_id":null,"name":"My Namespace","users":2,"guests":1}]}) } it { is_expected.to include(group_data: %Q{[{"id":#{group.id},"account_id":null,"name":"My Namespace","users":2,"guests":1,"full_path":"my_namespace"}]}) }
it { is_expected.to include(trial: 'false') } it { is_expected.to include(trial: 'false') }
it { is_expected.to include(new_trial_registration_path: '/-/trial_registrations/new') } it { is_expected.to include(new_trial_registration_path: '/-/trial_registrations/new') }
...@@ -163,7 +163,7 @@ RSpec.describe SubscriptionsHelper do ...@@ -163,7 +163,7 @@ RSpec.describe SubscriptionsHelper do
it { is_expected.to include(namespace_id: group.id.to_s) } it { is_expected.to include(namespace_id: group.id.to_s) }
it { is_expected.to include(active_subscription: active_subscription) } it { is_expected.to include(active_subscription: active_subscription) }
it { is_expected.to include(source: 'some_source') } it { is_expected.to include(source: 'some_source') }
it { is_expected.to include(group_data: %Q{[{"id":#{group.id},"account_id":"#{account_id}","name":"My Namespace","users":1,"guests":0}]}) } it { is_expected.to include(group_data: %Q{[{"id":#{group.id},"account_id":"#{account_id}","name":"My Namespace","users":1,"guests":0,"full_path":"my_namespace"}]}) }
it { is_expected.to include(redirect_after_success: group_usage_quotas_path(group, anchor: anchor, purchased_product: purchased_product)) } it { is_expected.to include(redirect_after_success: group_usage_quotas_path(group, anchor: anchor, purchased_product: purchased_product)) }
end end
end end
...@@ -4122,6 +4122,9 @@ msgstr "" ...@@ -4122,6 +4122,9 @@ msgstr ""
msgid "An unauthenticated user" msgid "An unauthenticated user"
msgstr "" msgstr ""
msgid "An unexpected error occurred"
msgstr ""
msgid "An unexpected error occurred while checking the project environment." msgid "An unexpected error occurred while checking the project environment."
msgstr "" msgstr ""
...@@ -7106,6 +7109,9 @@ msgstr "" ...@@ -7106,6 +7109,9 @@ msgstr ""
msgid "Checkout|CI minutes" msgid "Checkout|CI minutes"
msgstr "" msgstr ""
msgid "Checkout|Calculating your subscription..."
msgstr ""
msgid "Checkout|Checkout" msgid "Checkout|Checkout"
msgstr "" msgstr ""
...@@ -7220,6 +7226,9 @@ msgstr "" ...@@ -7220,6 +7226,9 @@ msgstr ""
msgid "Checkout|Tax" msgid "Checkout|Tax"
msgstr "" msgstr ""
msgid "Checkout|This number must be %{minimumNumberOfUsers} (your seats in use) or more."
msgstr ""
msgid "Checkout|Total" msgid "Checkout|Total"
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