diff --git a/packages/server/utils/__tests__/isRequestToJoinDomainAllowed.test.ts b/packages/server/utils/__tests__/isRequestToJoinDomainAllowed.test.ts index 562f556ee42..0593c17a62f 100644 --- a/packages/server/utils/__tests__/isRequestToJoinDomainAllowed.test.ts +++ b/packages/server/utils/__tests__/isRequestToJoinDomainAllowed.test.ts @@ -45,13 +45,15 @@ type TestOrganizationUser = Pick< const addOrg = async ( activeDomain: string | null, members: TestOrganizationUser[], - featureFlags?: string[] + rest?: {featureFlags?: string[]; tier?: string} ) => { + const {featureFlags, tier} = rest ?? {} const orgId = generateUID() const org = { id: orgId, activeDomain, - featureFlags: featureFlags ?? [] + featureFlags: featureFlags ?? [], + tier: tier ?? 'starter' } const orgUsers = members.map((member) => ({ @@ -143,7 +145,7 @@ test('Org with noPromptToJoinOrg feature flag is ignored', async () => { userId: 'user2' } ], - ['noPromptToJoinOrg'] + {featureFlags: ['noPromptToJoinOrg']} ) const orgIds = await getEligibleOrgIdsByDomain('parabol.co', 'newUser', dataLoader) @@ -285,7 +287,84 @@ test('Org matching the user are ignored', async () => { expect(userLoader.loadMany).toHaveBeenCalledTimes(0) }) -test('All orgs with verified emails qualify', async () => { +test('Only the biggest org with verified emails qualify', async () => { + const org = await addOrg('parabol.co', [ + { + joinedAt: new Date('2023-09-06'), + userId: 'founder1' + }, + { + joinedAt: new Date('2023-09-07'), + userId: 'member1' + } + ]) + const biggerOrg = await addOrg('parabol.co', [ + { + joinedAt: new Date('2023-09-06'), + userId: 'founder2' + }, + { + joinedAt: new Date('2023-09-07'), + userId: 'member2' + }, + { + joinedAt: new Date('2023-09-07'), + userId: 'member3' + } + ]) + await addOrg('parabol.co', [ + { + joinedAt: new Date('2023-09-06'), + userId: 'founder3' + }, + { + joinedAt: new Date('2023-09-07'), + userId: 'member3' + } + ]) + + userLoader.loadMany.mockImplementation((userIds) => { + const users = { + founder1: { + email: 'user1@parabol.co', + identities: [ + { + isEmailVerified: true + } + ] + }, + founder2: { + email: 'user2@parabol.co', + identities: [ + { + isEmailVerified: true + } + ] + }, + founder3: { + email: 'user3@parabol.co', + identities: [ + { + isEmailVerified: false + } + ] + } + } + return userIds.map((id) => ({ + id, + ...users[id] + })) + }) + + const orgIds = await getEligibleOrgIdsByDomain('parabol.co', 'newUser', dataLoader) + expect(userLoader.loadMany).toHaveBeenCalledTimes(3) + expect(userLoader.loadMany).toHaveBeenCalledWith(['founder1']) + expect(userLoader.loadMany).toHaveBeenCalledWith(['founder2']) + expect(userLoader.loadMany).toHaveBeenCalledWith(['founder3']) + expect(orgIds).toIncludeSameMembers([biggerOrg]) +}) + +test('All the biggest orgs with verified emails qualify', async () => { const org1 = await addOrg('parabol.co', [ { joinedAt: new Date('2023-09-06'), @@ -358,6 +437,172 @@ test('All orgs with verified emails qualify', async () => { expect(orgIds).toIncludeSameMembers([org1, org2]) }) +test('Team trumps starter tier with more users org', async () => { + const teamOrg = await addOrg( + 'parabol.co', + [ + { + joinedAt: new Date('2023-09-06'), + userId: 'founder1' + }, + { + joinedAt: new Date('2023-09-07'), + userId: 'member1' + } + ], + {tier: 'team'} + ) + const biggerStarterOrg = await addOrg('parabol.co', [ + { + joinedAt: new Date('2023-09-06'), + userId: 'founder2' + }, + { + joinedAt: new Date('2023-09-07'), + userId: 'member2' + }, + { + joinedAt: new Date('2023-09-07'), + userId: 'member3' + } + ]) + await addOrg('parabol.co', [ + { + joinedAt: new Date('2023-09-06'), + userId: 'founder3' + }, + { + joinedAt: new Date('2023-09-07'), + userId: 'member3' + } + ]) + + userLoader.loadMany.mockImplementation((userIds) => { + const users = { + founder1: { + email: 'user1@parabol.co', + identities: [ + { + isEmailVerified: true + } + ] + }, + founder2: { + email: 'user2@parabol.co', + identities: [ + { + isEmailVerified: true + } + ] + }, + founder3: { + email: 'user3@parabol.co', + identities: [ + { + isEmailVerified: false + } + ] + } + } + return userIds.map((id) => ({ + id, + ...users[id] + })) + }) + + const orgIds = await getEligibleOrgIdsByDomain('parabol.co', 'newUser', dataLoader) + expect(userLoader.loadMany).toHaveBeenCalledTimes(3) + expect(userLoader.loadMany).toHaveBeenCalledWith(['founder1']) + expect(userLoader.loadMany).toHaveBeenCalledWith(['founder2']) + expect(userLoader.loadMany).toHaveBeenCalledWith(['founder3']) + expect(orgIds).toIncludeSameMembers([teamOrg]) +}) + +test('Enterprise trumps team tier with more users org', async () => { + const enterpriseOrg = await addOrg( + 'parabol.co', + [ + { + joinedAt: new Date('2023-09-06'), + userId: 'founder1' + }, + { + joinedAt: new Date('2023-09-07'), + userId: 'member1' + } + ], + {tier: 'enterprise'} + ) + const starterOrg = await addOrg( + 'parabol.co', + [ + { + joinedAt: new Date('2023-09-06'), + userId: 'founder2' + }, + { + joinedAt: new Date('2023-09-07'), + userId: 'member2' + }, + { + joinedAt: new Date('2023-09-07'), + userId: 'member3' + } + ], + {tier: 'team'} + ) + await addOrg('parabol.co', [ + { + joinedAt: new Date('2023-09-06'), + userId: 'founder3' + }, + { + joinedAt: new Date('2023-09-07'), + userId: 'member3' + } + ]) + + userLoader.loadMany.mockImplementation((userIds) => { + const users = { + founder1: { + email: 'user1@parabol.co', + identities: [ + { + isEmailVerified: true + } + ] + }, + founder2: { + email: 'user2@parabol.co', + identities: [ + { + isEmailVerified: true + } + ] + }, + founder3: { + email: 'user3@parabol.co', + identities: [ + { + isEmailVerified: false + } + ] + } + } + return userIds.map((id) => ({ + id, + ...users[id] + })) + }) + + const orgIds = await getEligibleOrgIdsByDomain('parabol.co', 'newUser', dataLoader) + expect(userLoader.loadMany).toHaveBeenCalledTimes(3) + expect(userLoader.loadMany).toHaveBeenCalledWith(['founder1']) + expect(userLoader.loadMany).toHaveBeenCalledWith(['founder2']) + expect(userLoader.loadMany).toHaveBeenCalledWith(['founder3']) + expect(orgIds).toIncludeSameMembers([enterpriseOrg]) +}) + test('Orgs with verified emails from different domains do not qualify', async () => { const org1 = await addOrg('parabol.co', [ { diff --git a/packages/server/utils/isRequestToJoinDomainAllowed.ts b/packages/server/utils/isRequestToJoinDomainAllowed.ts index f5c06672ed3..75ae60caa6a 100644 --- a/packages/server/utils/isRequestToJoinDomainAllowed.ts +++ b/packages/server/utils/isRequestToJoinDomainAllowed.ts @@ -5,6 +5,7 @@ import User from '../database/types/User' import {DataLoaderWorker} from '../graphql/graphql' import isValid from '../graphql/isValid' import TeamMember from '../database/types/TeamMember' +import Organization from '../database/types/Organization' export const getEligibleOrgIdsByDomain = async ( activeDomain: string, @@ -39,7 +40,8 @@ export const getEligibleOrgIdsByDomain = async ( ) .run() - const eligibleOrgs = await Promise.all( + type OrgWithActiveMembers = Organization & {activeMembers: number} + const eligibleOrgs = (await Promise.all( orgs.map(async (org) => { const {founder} = org const importantMembers = org.billingLeads.slice() as TeamMember[] @@ -57,8 +59,33 @@ export const getEligibleOrgIdsByDomain = async ( } return org }) + )) as OrgWithActiveMembers[] + + const highestTierOrgs = eligibleOrgs.filter(isValid).reduce((acc, org) => { + if (acc.length === 0) { + return [org] + } + const highestTier = acc[0]!.tier + if (org.tier === highestTier) { + return [...acc, org] + } + if (org.tier === 'enterprise') { + return [org] + } + if (highestTier === 'starter' && org.tier === 'team') { + return [org] + } + return acc + }, [] as OrgWithActiveMembers[]) + + const biggestSize = highestTierOrgs.reduce( + (acc, org) => (org.activeMembers > acc ? org.activeMembers : acc), + 0 ) - return eligibleOrgs.filter(isValid).map(({id}) => id) + + return highestTierOrgs + .filter(({activeMembers}) => activeMembers === biggestSize) + .map(({id}) => id) } const isRequestToJoinDomainAllowed = async (