diff --git a/services/web/app/src/Features/Subscription/Errors.js b/services/web/app/src/Features/Subscription/Errors.js index 609ec15a73..9ebb08c6db 100644 --- a/services/web/app/src/Features/Subscription/Errors.js +++ b/services/web/app/src/Features/Subscription/Errors.js @@ -26,8 +26,6 @@ class SubtotalLimitExceededError extends OError {} class HasPastDueInvoiceError extends OError {} -class HasNoAdditionalLicenseWhenManuallyCollectedError extends OError {} - class PaymentActionRequiredError extends OError { constructor(info) { super('Payment action required', info) @@ -45,5 +43,4 @@ module.exports = { InactiveError, SubtotalLimitExceededError, HasPastDueInvoiceError, - HasNoAdditionalLicenseWhenManuallyCollectedError, } diff --git a/services/web/app/src/Features/Subscription/SubscriptionGroupController.mjs b/services/web/app/src/Features/Subscription/SubscriptionGroupController.mjs index 7aa596836d..792850a1c1 100644 --- a/services/web/app/src/Features/Subscription/SubscriptionGroupController.mjs +++ b/services/web/app/src/Features/Subscription/SubscriptionGroupController.mjs @@ -8,6 +8,7 @@ import SessionManager from '../Authentication/SessionManager.js' import UserAuditLogHandler from '../User/UserAuditLogHandler.js' import { expressify } from '@overleaf/promise-utils' import Modules from '../../infrastructure/Modules.js' +import SplitTestHandler from '../SplitTests/SplitTestHandler.js' import UserGetter from '../User/UserGetter.js' import { Subscription } from '../../models/Subscription.js' import { isProfessionalGroupPlan } from './PlansHelper.mjs' @@ -18,7 +19,6 @@ import { InactiveError, SubtotalLimitExceededError, HasPastDueInvoiceError, - HasNoAdditionalLicenseWhenManuallyCollectedError, } from './Errors.js' /** @@ -151,13 +151,26 @@ async function addSeatsToGroupSubscription(req, res) { await SubscriptionGroupHandler.promises.ensureSubscriptionHasNoPastDueInvoice( subscription ) - await SubscriptionGroupHandler.promises.checkBillingInfoExistence( - paymentProviderSubscription, - userId - ) - await SubscriptionGroupHandler.promises.ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual( - paymentProviderSubscription - ) + + const { variant: flexibleLicensingForManuallyBilledSubscriptionsVariant } = + await SplitTestHandler.promises.getAssignment( + req, + res, + 'flexible-group-licensing-for-manually-billed-subscriptions' + ) + + if (flexibleLicensingForManuallyBilledSubscriptionsVariant === 'enabled') { + await SubscriptionGroupHandler.promises.checkBillingInfoExistence( + paymentProviderSubscription, + userId + ) + } else { + await SubscriptionGroupHandler.promises.ensureSubscriptionCollectionMethodIsNotManual( + paymentProviderSubscription + ) + // Check if the user has missing billing details + await Modules.promises.hooks.fire('getPaymentMethod', userId) + } res.render('subscriptions/add-seats', { subscriptionId: subscription._id, @@ -174,7 +187,7 @@ async function addSeatsToGroupSubscription(req, res) { ) } - if (error instanceof HasNoAdditionalLicenseWhenManuallyCollectedError) { + if (error instanceof ManuallyCollectedError) { return res.redirect( '/user/subscription/group/manually-collected-subscription' ) @@ -215,10 +228,10 @@ async function previewAddSeatsSubscriptionChange(req, res) { } catch (error) { if ( error instanceof MissingBillingInfoError || + error instanceof ManuallyCollectedError || error instanceof PendingChangeError || error instanceof InactiveError || - error instanceof HasPastDueInvoiceError || - error instanceof HasNoAdditionalLicenseWhenManuallyCollectedError + error instanceof HasPastDueInvoiceError ) { return res.status(422).end() } @@ -258,10 +271,10 @@ async function createAddSeatsSubscriptionChange(req, res) { } catch (error) { if ( error instanceof MissingBillingInfoError || + error instanceof ManuallyCollectedError || error instanceof PendingChangeError || error instanceof InactiveError || - error instanceof HasPastDueInvoiceError || - error instanceof HasNoAdditionalLicenseWhenManuallyCollectedError + error instanceof HasPastDueInvoiceError ) { return res.status(422).end() } @@ -398,6 +411,12 @@ async function manuallyCollectedSubscription(req, res) { const subscription = await SubscriptionLocator.promises.getUsersSubscription(userId) + await SplitTestHandler.promises.getAssignment( + req, + res, + 'flexible-group-licensing-for-manually-billed-subscriptions' + ) + res.render('subscriptions/manually-collected-subscription', { groupName: subscription.teamName, }) diff --git a/services/web/app/src/Features/Subscription/SubscriptionGroupHandler.js b/services/web/app/src/Features/Subscription/SubscriptionGroupHandler.js index 977218fdd2..bc179f8d23 100644 --- a/services/web/app/src/Features/Subscription/SubscriptionGroupHandler.js +++ b/services/web/app/src/Features/Subscription/SubscriptionGroupHandler.js @@ -18,7 +18,6 @@ const { PendingChangeError, InactiveError, HasPastDueInvoiceError, - HasNoAdditionalLicenseWhenManuallyCollectedError, } = require('./Errors') const EmailHelper = require('../Helpers/EmailHelper') const { InvalidEmailError } = require('../Errors/Errors') @@ -124,22 +123,6 @@ async function ensureSubscriptionHasNoPastDueInvoice(subscription) { } } -async function ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual( - paymentProviderSubscription -) { - if ( - paymentProviderSubscription.isCollectionMethodManual && - !paymentProviderSubscription.hasAddOn(MEMBERS_LIMIT_ADD_ON_CODE) - ) { - throw new HasNoAdditionalLicenseWhenManuallyCollectedError( - 'This subscription is being collected manually has no "additional-license" add-on', - { - subscription_id: paymentProviderSubscription.id, - } - ) - } -} - async function getUsersGroupSubscriptionDetails(userId) { const subscription = await SubscriptionLocator.promises.getUsersSubscription(userId) @@ -487,10 +470,6 @@ module.exports = { ensureSubscriptionHasNoPastDueInvoice: callbackify( ensureSubscriptionHasNoPastDueInvoice ), - ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual: - callbackify( - ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual - ), getTotalConfirmedUsersInGroup: callbackify(getTotalConfirmedUsersInGroup), isUserPartOfGroup: callbackify(isUserPartOfGroup), getGroupPlanUpgradePreview: callbackify(getGroupPlanUpgradePreview), @@ -505,7 +484,6 @@ module.exports = { ensureSubscriptionCollectionMethodIsNotManual, ensureSubscriptionHasNoPendingChanges, ensureSubscriptionHasNoPastDueInvoice, - ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual, getTotalConfirmedUsersInGroup, isUserPartOfGroup, getUsersGroupSubscriptionDetails, diff --git a/services/web/app/src/Features/Subscription/SubscriptionLocator.js b/services/web/app/src/Features/Subscription/SubscriptionLocator.js index d9956e42cb..c0c107eecf 100644 --- a/services/web/app/src/Features/Subscription/SubscriptionLocator.js +++ b/services/web/app/src/Features/Subscription/SubscriptionLocator.js @@ -4,7 +4,6 @@ const { callbackifyAll } = require('@overleaf/promise-utils') const { Subscription } = require('../../models/Subscription') -const SubscriptionHelper = require('./SubscriptionHelper') const { DeletedSubscription } = require('../../models/DeletedSubscription') const logger = require('@overleaf/logger') const { @@ -176,8 +175,7 @@ const SubscriptionLocator = { const hasActiveGroupSubscription = memberSubscriptions.some( subscription => - subscription.groupPlan && - SubscriptionHelper.getPaidSubscriptionState(subscription) === 'active' + subscription.recurlyStatus?.state === 'active' && subscription.groupPlan ) if (hasActiveGroupSubscription) { // Member of a group plan @@ -189,8 +187,7 @@ const SubscriptionLocator = { if (personalSubscription) { const hasActivePersonalSubscription = - SubscriptionHelper.getPaidSubscriptionState(personalSubscription) === - 'active' + personalSubscription.recurlyStatus?.state === 'active' if (hasActivePersonalSubscription) { if (personalSubscription.groupPlan) { // Owner of a group plan diff --git a/services/web/app/src/Features/UserMembership/UserMembershipHandler.js b/services/web/app/src/Features/UserMembership/UserMembershipHandler.js index 56d92e9c96..0632f6e50e 100644 --- a/services/web/app/src/Features/UserMembership/UserMembershipHandler.js +++ b/services/web/app/src/Features/UserMembership/UserMembershipHandler.js @@ -79,7 +79,11 @@ async function getPopulatedListOfMembers(entity, attributes) { } } - const users = await UserMembershipViewModel.promises.buildAsync(userObjects) + const users = await Promise.all( + userObjects.map(userObject => + UserMembershipViewModel.promises.buildAsync(userObject) + ) + ) for (const user of users) { if ( diff --git a/services/web/app/src/Features/UserMembership/UserMembershipViewModel.js b/services/web/app/src/Features/UserMembership/UserMembershipViewModel.js index 1d83a258ef..6c94f677b7 100644 --- a/services/web/app/src/Features/UserMembership/UserMembershipViewModel.js +++ b/services/web/app/src/Features/UserMembership/UserMembershipViewModel.js @@ -23,59 +23,30 @@ const UserMembershipViewModel = { } }, - buildAsync(userOrIdOrEmailArray, callback) { + buildAsync(userOrIdOrEmail, callback) { if (callback == null) { callback = function () {} } + if (!isObjectIdInstance(userOrIdOrEmail)) { + // userOrIdOrEmail is a user or an email and can be parsed by #build + return callback(null, UserMembershipViewModel.build(userOrIdOrEmail)) + } - const userObjectIds = userOrIdOrEmailArray.filter(isObjectIdInstance) - - return UserGetter.getUsers( - userObjectIds, - { - email: 1, - first_name: 1, - last_name: 1, - lastLoggedIn: 1, - lastActive: 1, - enrollment: 1, - }, - function (error, users) { - const results = [] - - if (error != null) { - userOrIdOrEmailArray.forEach(item => { - if (isObjectIdInstance(item)) { - results.push(buildUserViewModelWithId(item.toString())) - } else { - // `item` is a user or an email and can be parsed by #build - results.push(UserMembershipViewModel.build(item)) - } - }) - } else { - const usersMap = new Map() - for (const user of users) { - usersMap.set(user._id.toString(), user) - } - - userOrIdOrEmailArray.forEach(item => { - if (isObjectIdInstance(item)) { - const user = usersMap.get(item.toString()) - if (user == null) { - results.push(buildUserViewModelWithId(item.toString())) - } else { - results.push(buildUserViewModel(user)) - } - } else { - // `item` is a user or an email and can be parsed by #build - results.push(UserMembershipViewModel.build(item)) - } - }) - } - - callback(null, results) + const userId = userOrIdOrEmail + const projection = { + email: 1, + first_name: 1, + last_name: 1, + lastLoggedIn: 1, + lastActive: 1, + enrollment: 1, + } + return UserGetter.getUser(userId, projection, function (error, user) { + if (error != null || user == null) { + return callback(null, buildUserViewModelWithId(userId.toString())) } - ) + return callback(null, buildUserViewModel(user)) + }) }, } diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index c41512312a..6e6dfdc5d4 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -872,7 +872,8 @@ "is_email_affiliated": "", "issued_on": "", "it_looks_like_that_didnt_work_you_can_try_again_or_get_in_touch": "", - "it_looks_like_your_account_is_billed_manually_purchasing_additional_license_or_upgrading_subscription": "", + "it_looks_like_your_account_is_billed_manually": "", + "it_looks_like_your_account_is_billed_manually_upgrading_subscription": "", "it_looks_like_your_payment_details_are_missing_please_update_your_billing_information": "", "italics": "", "join_beta_program": "", @@ -1025,7 +1026,6 @@ "math_inline": "", "maximum_files_uploaded_together": "", "maybe_later": "", - "members_added": "", "members_management": "", "mendeley_dynamic_sync_description": "", "mendeley_groups_loading_error": "", @@ -1146,10 +1146,10 @@ "on_free_plan_upgrade_to_access_features": "", "one_step_away_from_professional_features": "", "only_group_admin_or_managers_can_delete_your_account_1": "", - "only_group_admin_or_managers_can_delete_your_account_10": "", "only_group_admin_or_managers_can_delete_your_account_3": "", "only_group_admin_or_managers_can_delete_your_account_6": "", "only_group_admin_or_managers_can_delete_your_account_7": "", + "only_group_admin_or_managers_can_delete_your_account_8": "", "only_group_admin_or_managers_can_delete_your_account_9": "", "only_importer_can_refresh": "", "open_action_menu": "", diff --git a/services/web/frontend/js/features/group-management/components/add-seats/add-seats.tsx b/services/web/frontend/js/features/group-management/components/add-seats/add-seats.tsx index 609620a958..ab9f96a975 100644 --- a/services/web/frontend/js/features/group-management/components/add-seats/add-seats.tsx +++ b/services/web/frontend/js/features/group-management/components/add-seats/add-seats.tsx @@ -30,6 +30,7 @@ import { } from '../../../../../../types/subscription/subscription-change-preview' import { MergeAndOverride, Nullable } from '../../../../../../types/utils' import { sendMB } from '../../../../infrastructure/event-tracking' +import { useFeatureFlag } from '@/shared/context/split-test-context' export const MAX_NUMBER_OF_USERS = 20 export const MAX_NUMBER_OF_PO_NUMBER_CHARACTERS = 50 @@ -49,6 +50,9 @@ function AddSeats() { const [addSeatsInputError, setAddSeatsInputError] = useState() const [poNumberInputError, setPoNumberInputError] = useState() const [shouldContactSales, setShouldContactSales] = useState(false) + const isFlexibleGroupLicensingForManuallyBilledSubscriptions = useFeatureFlag( + 'flexible-group-licensing-for-manually-billed-subscriptions' + ) const controller = useAbortController() const { signal: addSeatsSignal } = useAbortController() const { signal: contactSalesSignal } = useAbortController() @@ -369,12 +373,13 @@ function AddSeats() { {addSeatsInputError} )} - {isCollectionMethodManual && ( - - )} + {isFlexibleGroupLicensingForManuallyBilledSubscriptions && + isCollectionMethodManual && ( + + )} ('') @@ -142,13 +140,6 @@ export default function GroupMembers() { data-testid="add-more-members-form" >

{t('invite_more_members')}

- {memberAdded && ( - - )}
diff --git a/services/web/frontend/js/features/group-management/components/manually-collected-subscription.tsx b/services/web/frontend/js/features/group-management/components/manually-collected-subscription.tsx index b2f2580b5f..971d4fa791 100644 --- a/services/web/frontend/js/features/group-management/components/manually-collected-subscription.tsx +++ b/services/web/frontend/js/features/group-management/components/manually-collected-subscription.tsx @@ -2,9 +2,14 @@ import { Trans, useTranslation } from 'react-i18next' import OLNotification from '@/features/ui/components/ol/ol-notification' import Card from '@/features/group-management/components/card' import useWaitForI18n from '@/shared/hooks/use-wait-for-i18n' +import { useFeatureFlag } from '@/shared/context/split-test-context' function ManuallyCollectedSubscription() { const { t } = useTranslation() + const isFlexibleGroupLicensingForManuallyBilledSubscriptions = useFeatureFlag( + 'flexible-group-licensing-for-manually-billed-subscriptions' + ) + const { isReady } = useWaitForI18n() if (!isReady) { @@ -17,13 +22,23 @@ function ManuallyCollectedSubscription() { type="error" title={t('account_billed_manually')} content={ - , - ]} - /> + isFlexibleGroupLicensingForManuallyBilledSubscriptions ? ( + , + ]} + /> + ) : ( + , + ]} + /> + ) } className="m-0" /> diff --git a/services/web/frontend/js/features/group-management/components/members-table/managed-user-status.tsx b/services/web/frontend/js/features/group-management/components/members-table/managed-user-status.tsx index 14f4657fe7..5ed360de9c 100644 --- a/services/web/frontend/js/features/group-management/components/members-table/managed-user-status.tsx +++ b/services/web/frontend/js/features/group-management/components/members-table/managed-user-status.tsx @@ -30,6 +30,9 @@ export default function ManagedUserStatus({ user }: ManagedUserStatusProps) { ) + if (user.isEntityAdmin) { + return + } if (user.invite) { return managedUserInvite } diff --git a/services/web/frontend/js/features/group-management/components/members-table/members-list.tsx b/services/web/frontend/js/features/group-management/components/members-table/members-list.tsx index 8a46a225bb..6be4c7cbda 100644 --- a/services/web/frontend/js/features/group-management/components/members-table/members-list.tsx +++ b/services/web/frontend/js/features/group-management/components/members-table/members-list.tsx @@ -1,4 +1,4 @@ -import { useState, useRef, useEffect, useMemo } from 'react' +import { useState, useRef, useEffect } from 'react' import { useTranslation } from 'react-i18next' import { User } from '../../../../../../types/group-management/user' import { useGroupMembersContext } from '../../context/group-members-context' @@ -13,9 +13,6 @@ import getMeta from '@/utils/meta' import UnlinkUserModal from './unlink-user-modal' import OLTable from '@/features/ui/components/ol/ol-table' import OLTooltip from '@/features/ui/components/ol/ol-tooltip' -import Pagination from '@/shared/components/pagination' - -const USERS_DISPLAY_LIMIT = 50 type ManagedUsersListProps = { groupId: string @@ -34,30 +31,13 @@ export default function MembersList({ groupId }: ManagedUsersListProps) { const managedUsersActive = getMeta('ol-managedUsersActive') const groupSSOActive = getMeta('ol-groupSSOActive') const tHeadRowRef = useRef(null) - const [pagination, setPagination] = useState({ currPage: 1, totalPages: 1 }) - - const usersForCurrentPage = useMemo( - () => - users.slice( - (pagination.currPage - 1) * USERS_DISPLAY_LIMIT, - pagination.currPage * USERS_DISPLAY_LIMIT - ), - [users, pagination.currPage] - ) - - const handlePageClick = ( - _e: React.MouseEvent, - page: number - ) => { - setPagination(p => ({ ...p, currPage: page })) - } + const [colSpan, setColSpan] = useState(0) useEffect(() => { - setPagination(p => ({ - ...p, - totalPages: Math.ceil(users.length / USERS_DISPLAY_LIMIT), - })) - }, [users.length]) + if (tHeadRowRef.current) { + setColSpan(tHeadRowRef.current.querySelectorAll('th').length) + } + }, []) return (
@@ -113,17 +93,12 @@ export default function MembersList({ groupId }: ManagedUsersListProps) { {users.length === 0 && ( - + {t('no_members')} )} - {usersForCurrentPage.map(user => ( + {users.map(user => ( - {pagination.totalPages > 1 && ( -
- -
- )} {userToOffboard && ( void inviteMemberLoading: boolean inviteError?: APIError - memberAdded: boolean paths: { [key: string]: string } } @@ -59,7 +58,6 @@ export function GroupMembersProvider({ children }: GroupMembersProviderProps) { const [inviteError, setInviteError] = useState() const [removeMemberInflightCount, setRemoveMemberInflightCount] = useState(0) const [removeMemberError, setRemoveMemberError] = useState() - const [memberAdded, setMemberAdded] = useState(false) const groupId = getMeta('ol-groupId') @@ -76,9 +74,7 @@ export function GroupMembersProvider({ children }: GroupMembersProviderProps) { const addMembers = useCallback( (emailString: string) => { setInviteError(undefined) - setMemberAdded(false) const emails = parseEmails(emailString) - let isError = false mapSeries(emails, async email => { setInviteUserInflightCount(count => count + 1) try { @@ -98,15 +94,8 @@ export function GroupMembersProvider({ children }: GroupMembersProviderProps) { } catch (error: unknown) { debugConsole.error(error) setInviteError((error as FetchError)?.data?.error || {}) - isError = true } - setInviteUserInflightCount(count => { - const newCount = count - 1 - if (newCount === 0 && !isError) { - setMemberAdded(true) - } - return newCount - }) + setInviteUserInflightCount(count => count - 1) }) }, [paths.addMember, users, setUsers] @@ -184,7 +173,6 @@ export function GroupMembersProvider({ children }: GroupMembersProviderProps) { removeMemberError, inviteMemberLoading: inviteUserInflightCount > 0, inviteError, - memberAdded, paths, }), [ @@ -203,7 +191,6 @@ export function GroupMembersProvider({ children }: GroupMembersProviderProps) { removeMemberError, inviteUserInflightCount, inviteError, - memberAdded, paths, ] ) diff --git a/services/web/frontend/js/utils/meta.ts b/services/web/frontend/js/utils/meta.ts index f8da39f909..69552f9b1a 100644 --- a/services/web/frontend/js/utils/meta.ts +++ b/services/web/frontend/js/utils/meta.ts @@ -247,7 +247,6 @@ export interface Meta { 'ol-splitTestVariants': { [name: string]: string } 'ol-ssoDisabled': boolean 'ol-ssoErrorMessage': string - 'ol-stripeAccountId': string 'ol-stripeCustomerId': string 'ol-subscription': any // TODO: mixed types, split into two fields 'ol-subscriptionChangePreview': SubscriptionChangePreview diff --git a/services/web/locales/en.json b/services/web/locales/en.json index 1794050bf8..3593bfab7b 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -1130,7 +1130,8 @@ "issued_on": "Issued: __date__", "it": "Italian", "it_looks_like_that_didnt_work_you_can_try_again_or_get_in_touch": "It looks like that didn’t work. You can try again or <0>get in touch with our Support team for more help.", - "it_looks_like_your_account_is_billed_manually_purchasing_additional_license_or_upgrading_subscription": "It looks like your account is being billed manually - purchasing additional licenses or upgrading your subscription can only be done by the Support team. Please <0>get in touch for help.", + "it_looks_like_your_account_is_billed_manually": "It looks like your account is being billed manually - adding seats or upgrading your subscription can only be done by the Support team. Please <0>get in touch for help.", + "it_looks_like_your_account_is_billed_manually_upgrading_subscription": "It looks like your account is being billed manually - upgrading your subscription can only be done by the Support team. Please <0>get in touch for help.", "it_looks_like_your_payment_details_are_missing_please_update_your_billing_information": "It looks like your payment details are missing. Please <0>update your billing information, or <1>get in touch with our Support team for more help.", "italics": "Italics", "ja": "Japanese", @@ -1341,7 +1342,6 @@ "may": "May", "maybe_later": "Maybe later", "member_picker": "Select number of users for group plan", - "members_added": "Member(s) added.", "members_management": "Members management", "mendeley": "Mendeley", "mendeley_dynamic_sync_description": "With the Mendeley integration, you can import your references into __appName__. You can either import all your references at once or dynamically search your Mendeley library directly from __appName__.", @@ -1504,10 +1504,10 @@ "ongoing_experiments": "Ongoing experiments", "online_latex_editor": "Online LaTeX Editor", "only_group_admin_or_managers_can_delete_your_account_1": "By becoming a managed user, your organization will have admin rights over your account and control over your stuff, including the right to close your account and access, delete and share your stuff. As a result:", - "only_group_admin_or_managers_can_delete_your_account_10": "If you have an individual subscription, we’ll automatically terminate it and cancel its renewal when your account becomes managed. To request a pro-rata refund for the remainder, please contact Support.", "only_group_admin_or_managers_can_delete_your_account_3": "Your group admin and group managers will be able to reassign ownership of your projects to another group member.", "only_group_admin_or_managers_can_delete_your_account_6": "Only your group admin or group managers will be able to delete your account or change your account back into an unmanaged account.", "only_group_admin_or_managers_can_delete_your_account_7": "Only your group admin or group managers will be able to delete your account or change your account into an unmanaged account.", + "only_group_admin_or_managers_can_delete_your_account_8": "We’ll cancel the renewal of your subscription, reach out to Support to request a pro-rata refund. Your individual subscription will be terminated when your account becomes managed.", "only_group_admin_or_managers_can_delete_your_account_9": "Once you have become a managed user, <0>you yourself cannot change it back to an unmanaged account. <1>Learn more about managed Overleaf accounts.", "only_importer_can_refresh": "Only the person who originally imported this __provider__ file can refresh it.", "open_action_menu": "Open __name__ action menu", diff --git a/services/web/test/frontend/features/group-management/components/add-seats.spec.tsx b/services/web/test/frontend/features/group-management/components/add-seats.spec.tsx index f26357d842..ead0c74a1c 100644 --- a/services/web/test/frontend/features/group-management/components/add-seats.spec.tsx +++ b/services/web/test/frontend/features/group-management/components/add-seats.spec.tsx @@ -14,6 +14,9 @@ describe('', function () { win.metaAttributesCache.set('ol-totalLicenses', this.totalLicenses) win.metaAttributesCache.set('ol-isProfessional', false) win.metaAttributesCache.set('ol-isCollectionMethodManual', true) + win.metaAttributesCache.set('ol-splitTestVariants', { + 'flexible-group-licensing-for-manually-billed-subscriptions': 'enabled', + }) }) cy.mount( diff --git a/services/web/test/frontend/features/group-management/components/members-table/managed-user-status.spec.tsx b/services/web/test/frontend/features/group-management/components/members-table/managed-user-status.spec.tsx index 5c09987695..49cadfe0c9 100644 --- a/services/web/test/frontend/features/group-management/components/members-table/managed-user-status.spec.tsx +++ b/services/web/test/frontend/features/group-management/components/members-table/managed-user-status.spec.tsx @@ -61,4 +61,26 @@ describe('MemberStatus', function () { cy.get('.security-state-not-managed').contains('Managed') }) }) + + describe('with the group admin', function () { + const user: User = { + _id: 'some-user', + email: 'some.user@example.com', + first_name: 'Some', + last_name: 'User', + invite: false, + last_active_at: new Date(), + enrollment: undefined, + isEntityAdmin: true, + } + beforeEach(function () { + cy.mount() + }) + + it('should render no state indicator', function () { + cy.get('.security-state-group-admin') + .contains('Managed') + .should('not.exist') + }) + }) }) diff --git a/services/web/test/frontend/features/group-management/components/members-table/members-list.spec.tsx b/services/web/test/frontend/features/group-management/components/members-table/members-list.spec.tsx index 32958b34d0..0861ab6c34 100644 --- a/services/web/test/frontend/features/group-management/components/members-table/members-list.spec.tsx +++ b/services/web/test/frontend/features/group-management/components/members-table/members-list.spec.tsx @@ -121,16 +121,6 @@ describe('MembersList', function () { users[1].last_name ) }) - it('should render the pagination navigation', function () { - cy.window().then(win => { - win.metaAttributesCache.set( - 'ol-users', - Array.from({ length: 50 }).flatMap(() => users.flat()) - ) - }) - mountManagedUsersList() - cy.findByRole('navigation', { name: /pagination navigation/i }) - }) }) describe('empty user list', function () { diff --git a/services/web/test/unit/src/Subscription/SubscriptionGroupController.test.mjs b/services/web/test/unit/src/Subscription/SubscriptionGroupController.test.mjs index ddfa8b0790..2b52f2be37 100644 --- a/services/web/test/unit/src/Subscription/SubscriptionGroupController.test.mjs +++ b/services/web/test/unit/src/Subscription/SubscriptionGroupController.test.mjs @@ -73,8 +73,6 @@ describe('SubscriptionGroupController', function () { .resolves(ctx.previewSubscriptionChangeData), checkBillingInfoExistence: sinon.stub().resolves(ctx.paymentMethod), updateSubscriptionPaymentTerms: sinon.stub().resolves(), - ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual: - sinon.stub().resolves(), }, } @@ -107,6 +105,12 @@ describe('SubscriptionGroupController', function () { }, } + ctx.SplitTestHandler = { + promises: { + getAssignment: sinon.stub().resolves({ variant: 'enabled' }), + }, + } + ctx.UserGetter = { promises: { getUserEmail: sinon.stub().resolves(ctx.user.email), @@ -136,7 +140,6 @@ describe('SubscriptionGroupController', function () { InactiveError: class extends Error {}, SubtotalLimitExceededError: class extends Error {}, HasPastDueInvoiceError: class extends Error {}, - HasNoAdditionalLicenseWhenManuallyCollectedError: class extends Error {}, } vi.doMock( @@ -168,6 +171,13 @@ describe('SubscriptionGroupController', function () { default: ctx.Modules, })) + vi.doMock( + '../../../../app/src/Features/SplitTests/SplitTestHandler', + () => ({ + default: ctx.SplitTestHandler, + }) + ) + vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({ default: ctx.UserGetter, })) @@ -446,9 +456,6 @@ describe('SubscriptionGroupController', function () { ctx.SubscriptionGroupHandler.promises.checkBillingInfoExistence .calledWith(ctx.recurlySubscription, ctx.adminUserId) .should.equal(true) - ctx.SubscriptionGroupHandler.promises.ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual - .calledWith(ctx.recurlySubscription) - .should.equal(true) page.should.equal('subscriptions/add-seats') props.subscriptionId.should.equal(ctx.subscriptionId) props.groupName.should.equal(ctx.subscription.teamName) @@ -514,28 +521,6 @@ describe('SubscriptionGroupController', function () { }) }) - it('should redirect to manually collected subscription error page when collection method is manual and has no additional license add-on', async function (ctx) { - await new Promise(resolve => { - ctx.SubscriptionGroupHandler.promises.ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual = - sinon - .stub() - .throws( - new ctx.Errors.HasNoAdditionalLicenseWhenManuallyCollectedError() - ) - - const res = { - redirect: url => { - url.should.equal( - '/user/subscription/group/manually-collected-subscription' - ) - resolve() - }, - } - - ctx.Controller.addSeatsToGroupSubscription(ctx.req, res) - }) - }) - it('should redirect to subscription page when there is a pending change', async function (ctx) { await new Promise(resolve => { ctx.SubscriptionGroupHandler.promises.ensureSubscriptionHasNoPendingChanges = diff --git a/services/web/test/unit/src/Subscription/SubscriptionGroupHandlerTests.js b/services/web/test/unit/src/Subscription/SubscriptionGroupHandlerTests.js index 87793fe440..63a6e25fde 100644 --- a/services/web/test/unit/src/Subscription/SubscriptionGroupHandlerTests.js +++ b/services/web/test/unit/src/Subscription/SubscriptionGroupHandlerTests.js @@ -806,49 +806,6 @@ describe('SubscriptionGroupHandler', function () { }) }) - describe('ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual', function () { - it('should throw if the subscription is manually collected and has no additional license add-on', async function () { - await expect( - this.Handler.promises.ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual( - { - isCollectionMethodManual: true, - hasAddOn: sinon - .stub() - .withArgs('additional-license') - .returns(false), - } - ) - ).to.be.rejectedWith( - 'This subscription is being collected manually has no "additional-license" add-on' - ) - }) - - it('should not throw if the subscription is not manually collected and has no additional license add-on and ', async function () { - await expect( - this.Handler.promises.ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual( - { - isCollectionMethodManual: false, - hasAddOn: sinon - .stub() - .withArgs('additional-license') - .returns(false), - } - ) - ).to.not.be.rejected - }) - - it('should not throw if the subscription is not manually collected and has additional license add-on', async function () { - await expect( - this.Handler.promises.ensureSubscriptionHasAdditionalLicenseAddOnWhenCollectionMethodIsManual( - { - isCollectionMethodManual: true, - hasAddOn: sinon.stub().withArgs('additional-license').returns(true), - } - ) - ).to.not.be.rejected - }) - }) - describe('getGroupPlanUpgradePreview', function () { it('should generate preview for subscription upgrade', async function () { const result = await this.Handler.promises.getGroupPlanUpgradePreview( diff --git a/services/web/test/unit/src/UserMembership/UserMembershipHandlerTests.js b/services/web/test/unit/src/UserMembership/UserMembershipHandlerTests.js index 3bf74c0ab7..f7b665aecd 100644 --- a/services/web/test/unit/src/UserMembership/UserMembershipHandlerTests.js +++ b/services/web/test/unit/src/UserMembership/UserMembershipHandlerTests.js @@ -48,7 +48,7 @@ describe('UserMembershipHandler', function () { this.UserMembershipViewModel = { promises: { - buildAsync: sinon.stub().resolves([{ _id: 'mock-member-id' }]), + buildAsync: sinon.stub().resolves({ _id: 'mock-member-id' }), }, build: sinon.stub().returns(this.newUser), } @@ -118,26 +118,26 @@ describe('UserMembershipHandler', function () { this.subscription, EntityConfigs.group ) + const expectedCallcount = + this.subscription.member_ids.length + + this.subscription.invited_emails.length + + this.subscription.teamInvites.length expect( - this.UserMembershipViewModel.promises.buildAsync - ).to.be.calledOnceWith( - this.subscription.invited_emails.concat( - this.subscription.teamInvites[0].email, - this.subscription.member_ids - ) - ) + this.UserMembershipViewModel.promises.buildAsync.callCount + ).to.equal(expectedCallcount) }) }) - describe('group managers', function () { + describe('group mamagers', function () { it('build view model for all managers', async function () { await this.UserMembershipHandler.promises.getUsers( this.subscription, EntityConfigs.groupManagers ) + const expectedCallcount = this.subscription.manager_ids.length expect( - this.UserMembershipViewModel.promises.buildAsync - ).to.be.calledOnceWith(this.subscription.manager_ids) + this.UserMembershipViewModel.promises.buildAsync.callCount + ).to.equal(expectedCallcount) }) }) @@ -147,9 +147,11 @@ describe('UserMembershipHandler', function () { this.institution, EntityConfigs.institution ) + + const expectedCallcount = this.institution.managerIds.length expect( - this.UserMembershipViewModel.promises.buildAsync - ).to.be.calledOnceWith(this.institution.managerIds) + this.UserMembershipViewModel.promises.buildAsync.callCount + ).to.equal(expectedCallcount) }) }) }) diff --git a/services/web/test/unit/src/UserMembership/UserMembershipViewModelTests.js b/services/web/test/unit/src/UserMembership/UserMembershipViewModelTests.js index c5e21a5f48..a8ce9d158f 100644 --- a/services/web/test/unit/src/UserMembership/UserMembershipViewModelTests.js +++ b/services/web/test/unit/src/UserMembership/UserMembershipViewModelTests.js @@ -25,7 +25,7 @@ const { describe('UserMembershipViewModel', function () { beforeEach(function () { - this.UserGetter = { getUsers: sinon.stub() } + this.UserGetter = { getUser: sinon.stub() } this.UserMembershipViewModel = SandboxedModule.require(modulePath, { requires: { 'mongodb-legacy': { ObjectId }, @@ -87,10 +87,9 @@ describe('UserMembershipViewModel', function () { }) it('build email', function (done) { - this.UserGetter.getUsers.yields(null, []) return this.UserMembershipViewModel.buildAsync( - [this.email], - (error, [viewModel]) => { + this.email, + (error, viewModel) => { assertCalledWith(this.UserMembershipViewModel.build, this.email) return done() } @@ -98,10 +97,9 @@ describe('UserMembershipViewModel', function () { }) it('build user', function (done) { - this.UserGetter.getUsers.yields(null, []) return this.UserMembershipViewModel.buildAsync( - [this.user], - (error, [viewModel]) => { + this.user, + (error, viewModel) => { assertCalledWith(this.UserMembershipViewModel.build, this.user) return done() } @@ -109,34 +107,30 @@ describe('UserMembershipViewModel', function () { }) it('build user id', function (done) { - const user = { - ...this.user, - _id: new ObjectId(), - } - this.UserGetter.getUsers.yields(null, [user]) + this.UserGetter.getUser.yields(null, this.user) return this.UserMembershipViewModel.buildAsync( - [user._id], - (error, [viewModel]) => { + new ObjectId(), + (error, viewModel) => { expect(error).not.to.exist assertNotCalled(this.UserMembershipViewModel.build) - expect(viewModel._id.toString()).to.equal(user._id.toString()) - expect(viewModel.email).to.equal(user.email) - expect(viewModel.first_name).to.equal(user.first_name) + expect(viewModel._id).to.equal(this.user._id) + expect(viewModel.email).to.equal(this.user.email) + expect(viewModel.first_name).to.equal(this.user.first_name) expect(viewModel.invite).to.equal(false) expect(viewModel.email).to.exist expect(viewModel.enrollment).to.exist - expect(viewModel.enrollment).to.deep.equal(user.enrollment) + expect(viewModel.enrollment).to.deep.equal(this.user.enrollment) return done() } ) }) it('build user id with error', function (done) { - this.UserGetter.getUsers.yields(new Error('nope'), []) + this.UserGetter.getUser.yields(new Error('nope')) const userId = new ObjectId() return this.UserMembershipViewModel.buildAsync( - [userId], - (error, [viewModel]) => { + userId, + (error, viewModel) => { expect(error).not.to.exist assertNotCalled(this.UserMembershipViewModel.build) expect(viewModel._id).to.equal(userId.toString())