fix: Auto Assignment of team member to event when that member is (#24731)
auto-accepted during direct invite to sub-team
This commit is contained in:
+117
-23
@@ -30,25 +30,7 @@ async function verifyMembershipExists(userId: number, teamId: number): Promise<M
|
||||
});
|
||||
}
|
||||
|
||||
async function verifyUserOrganizationConsistency(userId: number): Promise<{
|
||||
profileCount: number;
|
||||
acceptedMembershipCount: number;
|
||||
pendingMembershipCount: number;
|
||||
}> {
|
||||
const profiles = await prisma.profile.count({ where: { userId } });
|
||||
const acceptedMemberships = await prisma.membership.count({
|
||||
where: { userId, accepted: true },
|
||||
});
|
||||
const pendingMemberships = await prisma.membership.count({
|
||||
where: { userId, accepted: false },
|
||||
});
|
||||
|
||||
return {
|
||||
profileCount: profiles,
|
||||
acceptedMembershipCount: acceptedMemberships,
|
||||
pendingMembershipCount: pendingMemberships,
|
||||
};
|
||||
}
|
||||
|
||||
// Test data creation helpers with unique identifiers
|
||||
function generateUniqueId() {
|
||||
@@ -67,7 +49,9 @@ async function createTestUser(data: { email: string; username: string; name?: st
|
||||
await prisma.user.deleteMany({
|
||||
where: { OR: [{ email: uniqueEmail }, { username: uniqueUsername }] },
|
||||
});
|
||||
} catch {}
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
|
||||
return await prisma.user.create({
|
||||
data: {
|
||||
@@ -83,7 +67,7 @@ async function createTestTeam(data: {
|
||||
slug: string;
|
||||
isOrganization?: boolean;
|
||||
parentId?: number;
|
||||
metadata?: any;
|
||||
metadata?: Record<string, unknown>;
|
||||
organizationSettings?: {
|
||||
orgAutoAcceptEmail: string;
|
||||
isOrganizationVerified?: boolean;
|
||||
@@ -329,8 +313,7 @@ describe("inviteMember.handler Integration Tests", () => {
|
||||
await inviteMemberHandler({
|
||||
ctx: {
|
||||
user: createUserContext(inviterUser, organization.id),
|
||||
session: {} as any,
|
||||
} as any,
|
||||
},
|
||||
input: {
|
||||
teamId: organization.id,
|
||||
usernameOrEmail: nonMatchingUser.email,
|
||||
@@ -418,7 +401,7 @@ describe("inviteMember.handler Integration Tests", () => {
|
||||
],
|
||||
});
|
||||
|
||||
const orgMembership = await verifyMembershipExists(
|
||||
await verifyMembershipExists(
|
||||
unverifiedUserWithUnacceptedMembership.id,
|
||||
organization.id
|
||||
);
|
||||
@@ -435,4 +418,115 @@ describe("inviteMember.handler Integration Tests", () => {
|
||||
expect(originalMembership?.accepted).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Subteam Direct Invite Flow", () => {
|
||||
it("should immediately add auto-accepted new users to event types with assignAllTeamMembers=true", async () => {
|
||||
// Setup: Create organization and team
|
||||
const organization = trackTeam(
|
||||
await createTestTeam({
|
||||
name: "Test Organization",
|
||||
slug: "test-org",
|
||||
isOrganization: true,
|
||||
metadata: {},
|
||||
organizationSettings: {
|
||||
orgAutoAcceptEmail: "company.com",
|
||||
isOrganizationVerified: true,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const team = trackTeam(
|
||||
await createTestTeam({
|
||||
name: "Sales Team",
|
||||
slug: "sales-team",
|
||||
isOrganization: false,
|
||||
parentId: organization.id,
|
||||
})
|
||||
);
|
||||
|
||||
// Create inviter user
|
||||
const inviterUser = trackUser(
|
||||
await createTestUser({
|
||||
email: "inviter@company.com",
|
||||
username: "inviter",
|
||||
})
|
||||
);
|
||||
|
||||
await prisma.membership.create({
|
||||
data: {
|
||||
userId: inviterUser.id,
|
||||
teamId: organization.id,
|
||||
role: MembershipRole.OWNER,
|
||||
accepted: true,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.membership.create({
|
||||
data: {
|
||||
userId: inviterUser.id,
|
||||
teamId: team.id,
|
||||
role: MembershipRole.OWNER,
|
||||
accepted: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Create event type with assignAllTeamMembers enabled
|
||||
const eventType = await prisma.eventType.create({
|
||||
data: {
|
||||
title: "Team Event",
|
||||
slug: "team-event",
|
||||
length: 30,
|
||||
teamId: team.id,
|
||||
assignAllTeamMembers: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Act: Invite a new user (non-existing) with auto-accept domain
|
||||
const newUserEmail = `newuser-${generateUniqueId()}@company.com`;
|
||||
|
||||
await inviteMembersWithNoInviterPermissionCheck({
|
||||
inviterName: inviterUser.name,
|
||||
teamId: team.id,
|
||||
language: "en",
|
||||
creationSource: "WEBAPP" as const,
|
||||
orgSlug: organization.slug,
|
||||
invitations: [
|
||||
{
|
||||
usernameOrEmail: newUserEmail,
|
||||
role: MembershipRole.MEMBER,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Assert: Verify user was created
|
||||
const createdUser = await prisma.user.findUnique({
|
||||
where: { email: newUserEmail },
|
||||
});
|
||||
expect(createdUser).toBeTruthy();
|
||||
|
||||
if (createdUser) {
|
||||
trackUser(createdUser);
|
||||
|
||||
// Verify membership is auto-accepted
|
||||
const membership = await verifyMembershipExists(createdUser.id, team.id);
|
||||
expect(membership).toBeTruthy();
|
||||
expect(membership?.accepted).toBe(true);
|
||||
|
||||
// Verify user is immediately added as host to the event type
|
||||
const host = await prisma.host.findFirst({
|
||||
where: {
|
||||
userId: createdUser.id,
|
||||
eventTypeId: eventType.id,
|
||||
},
|
||||
});
|
||||
|
||||
expect(host).toBeTruthy();
|
||||
expect(host?.userId).toBe(createdUser.id);
|
||||
expect(host?.eventTypeId).toBe(eventType.id);
|
||||
}
|
||||
|
||||
// Cleanup event type
|
||||
await prisma.eventType.delete({ where: { id: eventType.id } });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -647,15 +647,17 @@ export const groupUsersByJoinability = ({
|
||||
connectionInfoMap,
|
||||
});
|
||||
|
||||
autoJoinStatus.autoAccept
|
||||
? usersToAutoJoin.push({
|
||||
...existingUserWithMemberships,
|
||||
...autoJoinStatus,
|
||||
})
|
||||
: regularUsers.push({
|
||||
...existingUserWithMemberships,
|
||||
...autoJoinStatus,
|
||||
});
|
||||
if (autoJoinStatus.autoAccept) {
|
||||
usersToAutoJoin.push({
|
||||
...existingUserWithMemberships,
|
||||
...autoJoinStatus,
|
||||
});
|
||||
} else {
|
||||
regularUsers.push({
|
||||
...existingUserWithMemberships,
|
||||
...autoJoinStatus,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return [usersToAutoJoin, regularUsers];
|
||||
@@ -755,12 +757,6 @@ export const sendExistingUserTeamInviteEmails = async ({
|
||||
await sendEmails(sendEmailsPromises);
|
||||
};
|
||||
|
||||
type inviteMemberHandlerInput = {
|
||||
teamId: number;
|
||||
role?: "ADMIN" | "MEMBER" | "OWNER";
|
||||
language: string;
|
||||
};
|
||||
|
||||
export async function handleExistingUsersInvites({
|
||||
invitableExistingUsers,
|
||||
team,
|
||||
@@ -991,7 +987,7 @@ export async function handleNewUsersInvites({
|
||||
}) {
|
||||
const translation = await getTranslation(language, "common");
|
||||
|
||||
await createNewUsersConnectToOrgIfExists({
|
||||
const createdUsers = await createNewUsersConnectToOrgIfExists({
|
||||
invitations: invitationsForNewUsers,
|
||||
isOrg,
|
||||
teamId: teamId,
|
||||
@@ -1002,6 +998,25 @@ export async function handleNewUsersInvites({
|
||||
creationSource,
|
||||
});
|
||||
|
||||
// Add auto-accepted users to team event types with assignAllTeamMembers immediately
|
||||
// Only teams have event types with assignAllTeamMembers, not organizations
|
||||
if (!isOrg) {
|
||||
const autoAcceptedUserIds = createdUsers
|
||||
.filter((user) => orgConnectInfoByUsernameOrEmail[user.email].autoAccept)
|
||||
.map((user) => user.id);
|
||||
|
||||
if (autoAcceptedUserIds.length > 0) {
|
||||
const results = await Promise.allSettled(
|
||||
autoAcceptedUserIds.map((userId) => updateNewTeamMemberEventTypes(userId, teamId))
|
||||
);
|
||||
results.forEach((result) => {
|
||||
if (result.status === "rejected") {
|
||||
log.error("Error updating new team member event types for user", result.reason);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const sendVerifyEmailsPromises = invitationsForNewUsers.map((invitation) => {
|
||||
return sendSignupToOrganizationEmail({
|
||||
usernameOrEmail: invitation.usernameOrEmail,
|
||||
|
||||
Reference in New Issue
Block a user