Skip to content

Commit 257acc3

Browse files
fix: Auto Assignment of team member to event when that member is (calcom#24731)
auto-accepted during direct invite to sub-team
1 parent 91063c4 commit 257acc3

2 files changed

Lines changed: 148 additions & 39 deletions

File tree

packages/trpc/server/routers/viewer/teams/inviteMember/inviteMember.handler.integration-test.ts

Lines changed: 117 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -30,25 +30,7 @@ async function verifyMembershipExists(userId: number, teamId: number): Promise<M
3030
});
3131
}
3232

33-
async function verifyUserOrganizationConsistency(userId: number): Promise<{
34-
profileCount: number;
35-
acceptedMembershipCount: number;
36-
pendingMembershipCount: number;
37-
}> {
38-
const profiles = await prisma.profile.count({ where: { userId } });
39-
const acceptedMemberships = await prisma.membership.count({
40-
where: { userId, accepted: true },
41-
});
42-
const pendingMemberships = await prisma.membership.count({
43-
where: { userId, accepted: false },
44-
});
4533

46-
return {
47-
profileCount: profiles,
48-
acceptedMembershipCount: acceptedMemberships,
49-
pendingMembershipCount: pendingMemberships,
50-
};
51-
}
5234

5335
// Test data creation helpers with unique identifiers
5436
function generateUniqueId() {
@@ -67,7 +49,9 @@ async function createTestUser(data: { email: string; username: string; name?: st
6749
await prisma.user.deleteMany({
6850
where: { OR: [{ email: uniqueEmail }, { username: uniqueUsername }] },
6951
});
70-
} catch {}
52+
} catch {
53+
// Ignore cleanup errors
54+
}
7155

7256
return await prisma.user.create({
7357
data: {
@@ -83,7 +67,7 @@ async function createTestTeam(data: {
8367
slug: string;
8468
isOrganization?: boolean;
8569
parentId?: number;
86-
metadata?: any;
70+
metadata?: Record<string, unknown>;
8771
organizationSettings?: {
8872
orgAutoAcceptEmail: string;
8973
isOrganizationVerified?: boolean;
@@ -329,8 +313,7 @@ describe("inviteMember.handler Integration Tests", () => {
329313
await inviteMemberHandler({
330314
ctx: {
331315
user: createUserContext(inviterUser, organization.id),
332-
session: {} as any,
333-
} as any,
316+
},
334317
input: {
335318
teamId: organization.id,
336319
usernameOrEmail: nonMatchingUser.email,
@@ -418,7 +401,7 @@ describe("inviteMember.handler Integration Tests", () => {
418401
],
419402
});
420403

421-
const orgMembership = await verifyMembershipExists(
404+
await verifyMembershipExists(
422405
unverifiedUserWithUnacceptedMembership.id,
423406
organization.id
424407
);
@@ -435,4 +418,115 @@ describe("inviteMember.handler Integration Tests", () => {
435418
expect(originalMembership?.accepted).toBe(true);
436419
});
437420
});
421+
422+
describe("Subteam Direct Invite Flow", () => {
423+
it("should immediately add auto-accepted new users to event types with assignAllTeamMembers=true", async () => {
424+
// Setup: Create organization and team
425+
const organization = trackTeam(
426+
await createTestTeam({
427+
name: "Test Organization",
428+
slug: "test-org",
429+
isOrganization: true,
430+
metadata: {},
431+
organizationSettings: {
432+
orgAutoAcceptEmail: "company.com",
433+
isOrganizationVerified: true,
434+
},
435+
})
436+
);
437+
438+
const team = trackTeam(
439+
await createTestTeam({
440+
name: "Sales Team",
441+
slug: "sales-team",
442+
isOrganization: false,
443+
parentId: organization.id,
444+
})
445+
);
446+
447+
// Create inviter user
448+
const inviterUser = trackUser(
449+
await createTestUser({
450+
email: "inviter@company.com",
451+
username: "inviter",
452+
})
453+
);
454+
455+
await prisma.membership.create({
456+
data: {
457+
userId: inviterUser.id,
458+
teamId: organization.id,
459+
role: MembershipRole.OWNER,
460+
accepted: true,
461+
},
462+
});
463+
464+
await prisma.membership.create({
465+
data: {
466+
userId: inviterUser.id,
467+
teamId: team.id,
468+
role: MembershipRole.OWNER,
469+
accepted: true,
470+
},
471+
});
472+
473+
// Create event type with assignAllTeamMembers enabled
474+
const eventType = await prisma.eventType.create({
475+
data: {
476+
title: "Team Event",
477+
slug: "team-event",
478+
length: 30,
479+
teamId: team.id,
480+
assignAllTeamMembers: true,
481+
},
482+
});
483+
484+
// Act: Invite a new user (non-existing) with auto-accept domain
485+
const newUserEmail = `newuser-${generateUniqueId()}@company.com`;
486+
487+
await inviteMembersWithNoInviterPermissionCheck({
488+
inviterName: inviterUser.name,
489+
teamId: team.id,
490+
language: "en",
491+
creationSource: "WEBAPP" as const,
492+
orgSlug: organization.slug,
493+
invitations: [
494+
{
495+
usernameOrEmail: newUserEmail,
496+
role: MembershipRole.MEMBER,
497+
},
498+
],
499+
});
500+
501+
// Assert: Verify user was created
502+
const createdUser = await prisma.user.findUnique({
503+
where: { email: newUserEmail },
504+
});
505+
expect(createdUser).toBeTruthy();
506+
507+
if (createdUser) {
508+
trackUser(createdUser);
509+
510+
// Verify membership is auto-accepted
511+
const membership = await verifyMembershipExists(createdUser.id, team.id);
512+
expect(membership).toBeTruthy();
513+
expect(membership?.accepted).toBe(true);
514+
515+
// Verify user is immediately added as host to the event type
516+
const host = await prisma.host.findFirst({
517+
where: {
518+
userId: createdUser.id,
519+
eventTypeId: eventType.id,
520+
},
521+
});
522+
523+
expect(host).toBeTruthy();
524+
expect(host?.userId).toBe(createdUser.id);
525+
expect(host?.eventTypeId).toBe(eventType.id);
526+
}
527+
528+
// Cleanup event type
529+
await prisma.eventType.delete({ where: { id: eventType.id } });
530+
});
531+
});
438532
});

packages/trpc/server/routers/viewer/teams/inviteMember/utils.ts

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -647,15 +647,17 @@ export const groupUsersByJoinability = ({
647647
connectionInfoMap,
648648
});
649649

650-
autoJoinStatus.autoAccept
651-
? usersToAutoJoin.push({
652-
...existingUserWithMemberships,
653-
...autoJoinStatus,
654-
})
655-
: regularUsers.push({
656-
...existingUserWithMemberships,
657-
...autoJoinStatus,
658-
});
650+
if (autoJoinStatus.autoAccept) {
651+
usersToAutoJoin.push({
652+
...existingUserWithMemberships,
653+
...autoJoinStatus,
654+
});
655+
} else {
656+
regularUsers.push({
657+
...existingUserWithMemberships,
658+
...autoJoinStatus,
659+
});
660+
}
659661
}
660662

661663
return [usersToAutoJoin, regularUsers];
@@ -755,12 +757,6 @@ export const sendExistingUserTeamInviteEmails = async ({
755757
await sendEmails(sendEmailsPromises);
756758
};
757759

758-
type inviteMemberHandlerInput = {
759-
teamId: number;
760-
role?: "ADMIN" | "MEMBER" | "OWNER";
761-
language: string;
762-
};
763-
764760
export async function handleExistingUsersInvites({
765761
invitableExistingUsers,
766762
team,
@@ -991,7 +987,7 @@ export async function handleNewUsersInvites({
991987
}) {
992988
const translation = await getTranslation(language, "common");
993989

994-
await createNewUsersConnectToOrgIfExists({
990+
const createdUsers = await createNewUsersConnectToOrgIfExists({
995991
invitations: invitationsForNewUsers,
996992
isOrg,
997993
teamId: teamId,
@@ -1002,6 +998,25 @@ export async function handleNewUsersInvites({
1002998
creationSource,
1003999
});
10041000

1001+
// Add auto-accepted users to team event types with assignAllTeamMembers immediately
1002+
// Only teams have event types with assignAllTeamMembers, not organizations
1003+
if (!isOrg) {
1004+
const autoAcceptedUserIds = createdUsers
1005+
.filter((user) => orgConnectInfoByUsernameOrEmail[user.email].autoAccept)
1006+
.map((user) => user.id);
1007+
1008+
if (autoAcceptedUserIds.length > 0) {
1009+
const results = await Promise.allSettled(
1010+
autoAcceptedUserIds.map((userId) => updateNewTeamMemberEventTypes(userId, teamId))
1011+
);
1012+
results.forEach((result) => {
1013+
if (result.status === "rejected") {
1014+
log.error("Error updating new team member event types for user", result.reason);
1015+
}
1016+
});
1017+
}
1018+
}
1019+
10051020
const sendVerifyEmailsPromises = invitationsForNewUsers.map((invitation) => {
10061021
return sendSignupToOrganizationEmail({
10071022
usernameOrEmail: invitation.usernameOrEmail,

0 commit comments

Comments
 (0)