refactor: decouple @calcom/features from @calcom/trpc/server [2] (#27821)

* inviteMemberUtils in featuers

* update imports

* wip

* wip

* fix: remove PII from log statements in inviteMemberUtils

- Remove email (usernameOrEmail) from error log in sendSignupToOrganizationEmail
- Replace full user object with userId in debug log in sendExistingUserTeamInviteEmails

Co-Authored-By: unknown <>

* revert: restore original code for pure relocation (no formatting/logic changes)

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix

* fix: update test mocks to use new import path for inviteMemberUtils

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* revert: restore original formatting in proration test and handleGroupEvents

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Benny Joo
2026-02-10 09:29:32 -03:00
committed by GitHub
co-authored by unknown <> benny@cal.com <sldisek783@gmail.com> benny@cal.com <sldisek783@gmail.com> benny@cal.com <sldisek783@gmail.com> Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent 431fbe44fc
commit ae31e05bec
8 changed files with 348 additions and 306 deletions
@@ -1,9 +1,9 @@
import { createMemberships } from "@calcom/features/ee/teams/lib/inviteMemberUtils";
import { TeamService } from "@calcom/features/ee/teams/services/teamService";
import type { IFeaturesRepository } from "@calcom/features/flags/features.repository.interface";
import prisma from "@calcom/prisma";
import type { Team, User } from "@calcom/prisma/client";
import { MembershipRole } from "@calcom/prisma/enums";
import { createMemberships } from "@calcom/trpc/server/routers/viewer/teams/inviteMember/utils";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { buildMonthlyProrationMetadata } from "../../../lib/proration-utils";
@@ -1,5 +1,9 @@
import type { DirectorySyncEvent, Group } from "@boxyhq/saml-jackson";
import {
getTeamOrThrow,
sendExistingUserTeamInviteEmails,
sendSignupToOrganizationEmail,
} from "@calcom/features/ee/teams/lib/inviteMemberUtils";
import { addNewMembersToEventTypes } from "@calcom/features/ee/teams/lib/queries";
import { ProfileRepository } from "@calcom/features/profile/repositories/ProfileRepository";
import logger from "@calcom/lib/logger";
@@ -7,12 +11,6 @@ import { safeStringify } from "@calcom/lib/safeStringify";
import { getTranslation } from "@calcom/lib/server/i18n";
import prisma from "@calcom/prisma";
import { IdentityProvider, MembershipRole } from "@calcom/prisma/enums";
import {
getTeamOrThrow,
sendSignupToOrganizationEmail,
sendExistingUserTeamInviteEmails,
} from "@calcom/trpc/server/routers/viewer/teams/inviteMember/utils";
import createUsersAndConnectToOrg from "./users/createUsersAndConnectToOrg";
const handleGroupEvents = async (event: DirectorySyncEvent, organizationId: number) => {
@@ -21,7 +21,7 @@ vi.mock("@calcom/lib/server/i18n", () => ({
getTranslation: vi.fn().mockResolvedValue((key: string) => key),
}));
vi.mock("@calcom/trpc/server/routers/viewer/teams/inviteMember/utils", () => ({
vi.mock("@calcom/features/ee/teams/lib/inviteMemberUtils", () => ({
getTeamOrThrow: vi.fn(),
sendExistingUserTeamInviteEmails: vi.fn(),
sendSignupToOrganizationEmail: vi.fn(),
@@ -112,7 +112,7 @@ describe("handleUserEvents", () => {
slug: organizationSlug,
});
const { getTeamOrThrow } = await import("@calcom/trpc/server/routers/viewer/teams/inviteMember/utils");
const { getTeamOrThrow } = await import("@calcom/features/ee/teams/lib/inviteMemberUtils");
vi.mocked(getTeamOrThrow).mockResolvedValue({
id: organizationId,
name: organizationName,
@@ -299,7 +299,7 @@ describe("handleUserEvents", () => {
const inviteExistingUserToOrg = (await import("./users/inviteExistingUserToOrg")).default;
const sendExistingUserTeamInviteEmails = (
await import("@calcom/trpc/server/routers/viewer/teams/inviteMember/utils")
await import("@calcom/features/ee/teams/lib/inviteMemberUtils")
).sendExistingUserTeamInviteEmails;
await handleUserEvents(event, organizationId);
@@ -419,7 +419,7 @@ describe("handleUserEvents", () => {
},
};
const { getTeamOrThrow } = await import("@calcom/trpc/server/routers/viewer/teams/inviteMember/utils");
const { getTeamOrThrow } = await import("@calcom/features/ee/teams/lib/inviteMemberUtils");
vi.mocked(getTeamOrThrow).mockResolvedValue(
null as unknown as Awaited<ReturnType<typeof getTeamOrThrow>>
);
@@ -1,17 +1,17 @@
import type { DirectorySyncEvent, User } from "@boxyhq/saml-jackson";
import removeUserFromOrg from "@calcom/features/ee/dsync/lib/removeUserFromOrg";
import type { UserWithMembership } from "@calcom/features/ee/teams/lib/inviteMemberUtils";
import {
getTeamOrThrow,
sendExistingUserTeamInviteEmails,
sendSignupToOrganizationEmail,
} from "@calcom/features/ee/teams/lib/inviteMemberUtils";
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import { getTranslation } from "@calcom/lib/server/i18n";
import prisma from "@calcom/prisma";
import { IdentityProvider } from "@calcom/prisma/enums";
import { getTeamOrThrow } from "@calcom/trpc/server/routers/viewer/teams/inviteMember/utils";
import type { UserWithMembership } from "@calcom/trpc/server/routers/viewer/teams/inviteMember/utils";
import { sendExistingUserTeamInviteEmails } from "@calcom/trpc/server/routers/viewer/teams/inviteMember/utils";
import { sendSignupToOrganizationEmail } from "@calcom/trpc/server/routers/viewer/teams/inviteMember/utils";
import { assignValueToUserInOrgBulk } from "./assignValueToUser";
import getAttributesFromScimPayload from "./getAttributesFromScimPayload";
import createUsersAndConnectToOrg from "./users/createUsersAndConnectToOrg";
@@ -1,9 +1,8 @@
import type { TFunction } from "i18next";
import type { UserWithMembership } from "@calcom/features/ee/teams/lib/inviteMemberUtils";
import { sendExistingUserTeamInviteEmails } from "@calcom/features/ee/teams/lib/inviteMemberUtils";
import { createAProfileForAnExistingUser } from "@calcom/features/profile/lib/createAProfileForAnExistingUser";
import prisma from "@calcom/prisma";
import { sendExistingUserTeamInviteEmails } from "@calcom/trpc/server/routers/viewer/teams/inviteMember/utils";
import type { UserWithMembership } from "@calcom/trpc/server/routers/viewer/teams/inviteMember/utils";
import type { TFunction } from "i18next";
/**
* This should only be used in a dsync context
@@ -1,8 +1,7 @@
import type { TFunction } from "i18next";
import type { UserWithMembership } from "@calcom/features/ee/teams/lib/inviteMemberUtils";
import { createAProfileForAnExistingUser } from "@calcom/features/profile/lib/createAProfileForAnExistingUser";
import prisma from "@calcom/prisma";
import type { UserWithMembership } from "@calcom/trpc/server/routers/viewer/teams/inviteMember/utils";
import type { TFunction } from "i18next";
/**
* This should only be used in a dsync context
@@ -0,0 +1,308 @@
import { randomBytes } from "node:crypto";
import { getOrgFullOrigin } from "@calcom/ee/organizations/lib/orgDomains";
import { sendTeamInviteEmail } from "@calcom/emails/organization-email-service";
import { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner";
import { SeatChangeTrackingService } from "@calcom/features/ee/billing/service/seatTracking/SeatChangeTrackingService";
import { OnboardingPathService } from "@calcom/features/onboarding/lib/onboarding-path.service";
import { WEBAPP_URL } from "@calcom/lib/constants";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import { prisma } from "@calcom/prisma";
import type {
Membership,
Profile as ProfileType,
UserPassword,
User as UserType,
} from "@calcom/prisma/client";
import { Prisma } from "@calcom/prisma/client";
import { MembershipRole } from "@calcom/prisma/enums";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import { TRPCError } from "@trpc/server";
import type { TFunction } from "i18next";
const log = logger.getSubLogger({ prefix: ["inviteMember.utils"] });
const isEmail = (str: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(str);
export type Invitee = Pick<
UserType,
"id" | "email" | "username" | "identityProvider" | "completedOnboarding"
>;
export type UserWithMembership = Invitee & {
teams?: Pick<Membership, "userId" | "teamId" | "accepted" | "role">[];
profiles: ProfileType[];
password: UserPassword | null;
};
type InvitableExistingUser = UserWithMembership & {
newRole: MembershipRole;
};
type InvitableExistingUserWithProfile = InvitableExistingUser & {
profile: {
username: string;
} | null;
};
export async function getTeamOrThrow(teamId: number) {
const team = await prisma.team.findUnique({
where: {
id: teamId,
},
include: {
organizationSettings: true,
parent: {
include: {
organizationSettings: true,
},
},
},
});
if (!team)
throw new TRPCError({
code: "NOT_FOUND",
message: `Team not found`,
});
return { ...team, metadata: teamMetadataSchema.parse(team.metadata) };
}
const createVerificationToken = async (identifier: string, teamId: number) => {
const token = randomBytes(32).toString("hex");
return prisma.verificationToken.create({
data: {
identifier,
token,
expires: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // +1 week
team: {
connect: {
id: teamId,
},
},
},
});
};
export async function sendSignupToOrganizationEmail({
usernameOrEmail,
team,
translation,
inviterName,
teamId,
isOrg,
}: {
usernameOrEmail: string;
team: { name: string; parent: { name: string } | null };
translation: TFunction;
inviterName: string;
teamId: number;
isOrg: boolean;
}) {
try {
const verificationToken = await createVerificationToken(usernameOrEmail, teamId);
const gettingStartedPath = await OnboardingPathService.getGettingStartedPathWhenInvited();
await sendTeamInviteEmail({
language: translation,
from: inviterName || `${team.name}'s admin`,
to: usernameOrEmail,
teamName: team.name,
joinLink: `${WEBAPP_URL}/signup?token=${verificationToken.token}&callbackUrl=${gettingStartedPath}`,
isCalcomMember: false,
isOrg: isOrg,
parentTeamName: team?.parent?.name,
isAutoJoin: false,
isExistingUserMovedToOrg: false,
// For a new user there is no prev and new links.
prevLink: null,
newLink: null,
});
} catch (error) {
logger.error(
"Failed to send signup to organization email",
safeStringify({
usernameOrEmail,
orgId: teamId,
}),
error
);
}
}
export const sendEmails = async (emailPromises: Promise<void>[]) => {
const sentEmails = await Promise.allSettled(emailPromises);
sentEmails.forEach((sentEmail) => {
if (sentEmail.status === "rejected") {
logger.error("Could not send email to user. Reason:", sentEmail.reason);
}
});
};
export const sendExistingUserTeamInviteEmails = async ({
existingUsersWithMemberships,
language,
currentUserTeamName,
currentUserName,
currentUserParentTeamName,
isOrg,
teamId,
isAutoJoin,
orgSlug,
}: {
language: TFunction;
isAutoJoin: boolean;
existingUsersWithMemberships: Omit<InvitableExistingUserWithProfile, "canBeInvited" | "newRole">[];
currentUserTeamName?: string;
currentUserParentTeamName: string | undefined;
currentUserName?: string | null;
isOrg: boolean;
teamId: number;
orgSlug: string | null;
}) => {
const sendEmailsPromises = existingUsersWithMemberships.map(async (user) => {
let sendTo = user.email;
if (!isEmail(user.email)) {
sendTo = user.email;
}
log.debug("Sending team invite email to", safeStringify({ user, currentUserName, currentUserTeamName }));
if (!currentUserTeamName) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "The team doesn't have a name",
});
}
// inform user of membership by email
if (currentUserTeamName) {
const inviteTeamOptions = {
joinLink: `${WEBAPP_URL}/auth/login?callbackUrl=/settings/teams`,
isCalcomMember: true,
};
/**
* Here we want to redirect to a different place if onboarding has been completed or not. This prevents the flash of going to teams -> Then to onboarding - also show a different email template.
* This only changes if the user is a CAL user and has not completed onboarding and has no password
*/
if (!user.completedOnboarding && !user.password?.hash && user.identityProvider === "CAL") {
const verificationToken = await createVerificationToken(user.email, teamId);
const gettingStartedPath = await OnboardingPathService.getGettingStartedPathWhenInvited();
inviteTeamOptions.joinLink = `${WEBAPP_URL}/signup?token=${verificationToken.token}&callbackUrl=${gettingStartedPath}`;
inviteTeamOptions.isCalcomMember = false;
} else if (!isAutoJoin) {
let verificationToken = await prisma.verificationToken.findFirst({
where: {
identifier: user.email,
teamId: teamId,
},
});
if (!verificationToken) {
verificationToken = await createVerificationToken(user.email, teamId);
}
inviteTeamOptions.joinLink = `${WEBAPP_URL}/teams?token=${verificationToken.token}&autoAccept=true`;
}
return sendTeamInviteEmail({
language,
isAutoJoin,
from: currentUserName ?? `${currentUserTeamName}'s admin`,
to: sendTo,
teamName: currentUserTeamName,
...inviteTeamOptions,
isOrg: isOrg,
parentTeamName: currentUserParentTeamName,
isExistingUserMovedToOrg: true,
prevLink: `${getOrgFullOrigin("")}/${user.username || ""}`,
newLink: user.profile ? `${getOrgFullOrigin(orgSlug ?? "")}/${user.profile.username}` : null,
});
}
});
await sendEmails(sendEmailsPromises);
};
export async function createMemberships({
teamId,
language,
invitees,
parentId,
accepted,
}: {
teamId: number;
language: string;
invitees: (UserWithMembership & {
newRole: MembershipRole;
needToCreateOrgMembership: boolean | null;
})[];
parentId: number | null;
accepted: boolean;
}) {
log.debug("Creating memberships for", safeStringify({ teamId, language, invitees, parentId, accepted }));
try {
await prisma.membership.createMany({
data: invitees.flatMap((invitee) => {
const organizationRole = parentId
? invitee?.teams?.find((membership) => membership.teamId === parentId)?.role
: undefined;
const data = [];
const createdAt = new Date();
// membership for the team
data.push({
createdAt,
teamId,
userId: invitee.id,
accepted,
role: checkAdminOrOwner(organizationRole) ? organizationRole : invitee.newRole,
});
// membership for the org
if (parentId && invitee.needToCreateOrgMembership) {
data.push({
createdAt,
accepted,
teamId: parentId,
userId: invitee.id,
role: MembershipRole.MEMBER,
});
}
return data;
}),
});
const seatTracker = new SeatChangeTrackingService();
const teamSeatAdditions = parentId ? 0 : invitees.length;
const organizationSeatAdditions = parentId
? invitees.filter((invitee) => invitee.needToCreateOrgMembership).length
: 0;
const trackingPromises: Promise<void>[] = [];
if (teamSeatAdditions > 0) {
trackingPromises.push(
seatTracker.logSeatAddition({
teamId,
seatCount: teamSeatAdditions,
})
);
}
if (parentId && organizationSeatAdditions > 0) {
trackingPromises.push(
seatTracker.logSeatAddition({
teamId: parentId,
seatCount: organizationSeatAdditions,
})
);
}
await Promise.all(trackingPromises);
} catch (e) {
if (e instanceof Prisma.PrismaClientKnownRequestError) {
logger.error("Failed to create memberships", teamId);
} else {
throw e;
}
}
}
@@ -1,43 +1,42 @@
import { randomBytes } from "node:crypto";
import { getOrgFullOrigin } from "@calcom/ee/organizations/lib/orgDomains";
import { sendTeamInviteEmail } from "@calcom/emails/organization-email-service";
import { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner";
import { SeatChangeTrackingService } from "@calcom/features/ee/billing/service/seatTracking/SeatChangeTrackingService";
import { getParsedTeam } from "@calcom/features/ee/teams/lib/getParsedTeam";
import {
createMemberships,
getTeamOrThrow,
sendEmails,
sendExistingUserTeamInviteEmails,
sendSignupToOrganizationEmail,
type UserWithMembership,
} from "@calcom/features/ee/teams/lib/inviteMemberUtils";
import { updateNewTeamMemberEventTypes } from "@calcom/features/ee/teams/lib/queries";
import { OnboardingPathService } from "@calcom/features/onboarding/lib/onboarding-path.service";
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
import { createAProfileForAnExistingUser } from "@calcom/features/profile/lib/createAProfileForAnExistingUser";
import { ProfileRepository } from "@calcom/features/profile/repositories/ProfileRepository";
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/availability";
import { ENABLE_PROFILE_SWITCHER, WEBAPP_URL } from "@calcom/lib/constants";
import { ENABLE_PROFILE_SWITCHER } from "@calcom/lib/constants";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import { getTranslation } from "@calcom/lib/server/i18n";
import slugify from "@calcom/lib/slugify";
import { prisma } from "@calcom/prisma";
import type { Membership, OrganizationSettings, Profile as ProfileType, Team } from "@calcom/prisma/client";
import { Prisma, type UserPassword, type User as UserType } from "@calcom/prisma/client";
import type { OrganizationSettings, Team } from "@calcom/prisma/client";
import type { CreationSource } from "@calcom/prisma/enums";
import { MembershipRole } from "@calcom/prisma/enums";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import { TRPCError } from "@trpc/server";
import type { TFunction } from "i18next";
import { isEmail } from "../util";
import type { TeamWithParent } from "./types";
const log = logger.getSubLogger({ prefix: ["inviteMember.utils"] });
export type Invitee = Pick<
UserType,
"id" | "email" | "username" | "identityProvider" | "completedOnboarding"
>;
export type { Invitee, UserWithMembership } from "@calcom/features/ee/teams/lib/inviteMemberUtils";
export {
createMemberships,
getTeamOrThrow,
sendEmails,
sendExistingUserTeamInviteEmails,
sendSignupToOrganizationEmail,
} from "@calcom/features/ee/teams/lib/inviteMemberUtils";
export type UserWithMembership = Invitee & {
teams?: Pick<Membership, "userId" | "teamId" | "accepted" | "role">[];
profiles: ProfileType[];
password: UserPassword | null;
};
const log = logger.getSubLogger({ prefix: ["inviteMember.utils"] });
export type Invitation = {
usernameOrEmail: string;
@@ -88,30 +87,6 @@ export function checkInputEmailIsValid(email: string) {
});
}
export async function getTeamOrThrow(teamId: number) {
const team = await prisma.team.findUnique({
where: {
id: teamId,
},
include: {
organizationSettings: true,
parent: {
include: {
organizationSettings: true,
},
},
},
});
if (!team)
throw new TRPCError({
code: "NOT_FOUND",
message: `Team not found`,
});
return { ...team, metadata: teamMetadataSchema.parse(team.metadata) };
}
export async function getUniqueInvitationsOrThrowIfEmpty(invitations: Invitation[]) {
const usernamesOrEmailsSet = new Set<string>();
const uniqueInvitations: Invitation[] = [];
@@ -425,149 +400,6 @@ export async function createNewUsersConnectToOrgIfExists({
return createdUsers;
}
export async function createMemberships({
teamId,
language,
invitees,
parentId,
accepted,
}: {
teamId: number;
language: string;
invitees: (InvitableExistingUser & {
needToCreateOrgMembership: boolean | null;
})[];
parentId: number | null;
accepted: boolean;
}) {
log.debug("Creating memberships for", safeStringify({ teamId, language, invitees, parentId, accepted }));
try {
await prisma.membership.createMany({
data: invitees.flatMap((invitee) => {
const organizationRole = parentId
? invitee?.teams?.find((membership) => membership.teamId === parentId)?.role
: undefined;
const data = [];
const createdAt = new Date();
// membership for the team
data.push({
createdAt,
teamId,
userId: invitee.id,
accepted,
role: checkAdminOrOwner(organizationRole) ? organizationRole : invitee.newRole,
});
// membership for the org
if (parentId && invitee.needToCreateOrgMembership) {
data.push({
createdAt,
accepted,
teamId: parentId,
userId: invitee.id,
role: MembershipRole.MEMBER,
});
}
return data;
}),
});
const seatTracker = new SeatChangeTrackingService();
const teamSeatAdditions = parentId ? 0 : invitees.length;
const organizationSeatAdditions = parentId
? invitees.filter((invitee) => invitee.needToCreateOrgMembership).length
: 0;
const trackingPromises: Promise<void>[] = [];
if (teamSeatAdditions > 0) {
trackingPromises.push(
seatTracker.logSeatAddition({
teamId,
seatCount: teamSeatAdditions,
})
);
}
if (parentId && organizationSeatAdditions > 0) {
trackingPromises.push(
seatTracker.logSeatAddition({
teamId: parentId,
seatCount: organizationSeatAdditions,
})
);
}
await Promise.all(trackingPromises);
} catch (e) {
if (e instanceof Prisma.PrismaClientKnownRequestError) {
logger.error("Failed to create memberships", teamId);
} else {
throw e;
}
}
}
const createVerificationToken = async (identifier: string, teamId: number) => {
const token = randomBytes(32).toString("hex");
return prisma.verificationToken.create({
data: {
identifier,
token,
expires: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // +1 week
team: {
connect: {
id: teamId,
},
},
},
});
};
export async function sendSignupToOrganizationEmail({
usernameOrEmail,
team,
translation,
inviterName,
teamId,
isOrg,
}: {
usernameOrEmail: string;
team: { name: string; parent: { name: string } | null };
translation: TFunction;
inviterName: string;
teamId: number;
isOrg: boolean;
}) {
try {
const verificationToken = await createVerificationToken(usernameOrEmail, teamId);
const gettingStartedPath = await OnboardingPathService.getGettingStartedPathWhenInvited();
await sendTeamInviteEmail({
language: translation,
from: inviterName || `${team.name}'s admin`,
to: usernameOrEmail,
teamName: team.name,
joinLink: `${WEBAPP_URL}/signup?token=${verificationToken.token}&callbackUrl=${gettingStartedPath}`,
isCalcomMember: false,
isOrg: isOrg,
parentTeamName: team?.parent?.name,
isAutoJoin: false,
isExistingUserMovedToOrg: false,
// For a new user there is no prev and new links.
prevLink: null,
newLink: null,
});
} catch (error) {
logger.error(
"Failed to send signup to organization email",
safeStringify({
usernameOrEmail,
orgId: teamId,
}),
error
);
}
}
type TeamAndOrganizationSettings = Team & {
organizationSettings?: OrganizationSettings | null;
};
@@ -699,100 +531,6 @@ export const groupUsersByJoinability = ({
return [usersToAutoJoin, regularUsers];
};
export const sendEmails = async (emailPromises: Promise<void>[]) => {
const sentEmails = await Promise.allSettled(emailPromises);
sentEmails.forEach((sentEmail) => {
if (sentEmail.status === "rejected") {
logger.error("Could not send email to user. Reason:", sentEmail.reason);
}
});
};
export const sendExistingUserTeamInviteEmails = async ({
existingUsersWithMemberships,
language,
currentUserTeamName,
currentUserName,
currentUserParentTeamName,
isOrg,
teamId,
isAutoJoin,
orgSlug,
}: {
language: TFunction;
isAutoJoin: boolean;
existingUsersWithMemberships: Omit<InvitableExistingUserWithProfile, "canBeInvited" | "newRole">[];
currentUserTeamName?: string;
currentUserParentTeamName: string | undefined;
currentUserName?: string | null;
isOrg: boolean;
teamId: number;
orgSlug: string | null;
}) => {
const sendEmailsPromises = existingUsersWithMemberships.map(async (user) => {
let sendTo = user.email;
if (!isEmail(user.email)) {
sendTo = user.email;
}
log.debug("Sending team invite email to", safeStringify({ user, currentUserName, currentUserTeamName }));
if (!currentUserTeamName) {
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR",
message: "The team doesn't have a name",
});
}
// inform user of membership by email
if (currentUserTeamName) {
const inviteTeamOptions = {
joinLink: `${WEBAPP_URL}/auth/login?callbackUrl=/settings/teams`,
isCalcomMember: true,
};
/**
* Here we want to redirect to a different place if onboarding has been completed or not. This prevents the flash of going to teams -> Then to onboarding - also show a different email template.
* This only changes if the user is a CAL user and has not completed onboarding and has no password
*/
if (!user.completedOnboarding && !user.password?.hash && user.identityProvider === "CAL") {
const verificationToken = await createVerificationToken(user.email, teamId);
const gettingStartedPath = await OnboardingPathService.getGettingStartedPathWhenInvited();
inviteTeamOptions.joinLink = `${WEBAPP_URL}/signup?token=${verificationToken.token}&callbackUrl=${gettingStartedPath}`;
inviteTeamOptions.isCalcomMember = false;
} else if (!isAutoJoin) {
let verificationToken = await prisma.verificationToken.findFirst({
where: {
identifier: user.email,
teamId: teamId,
},
});
if (!verificationToken) {
verificationToken = await createVerificationToken(user.email, teamId);
}
inviteTeamOptions.joinLink = `${WEBAPP_URL}/teams?token=${verificationToken.token}&autoAccept=true`;
}
return sendTeamInviteEmail({
language,
isAutoJoin,
from: currentUserName ?? `${currentUserTeamName}'s admin`,
to: sendTo,
teamName: currentUserTeamName,
...inviteTeamOptions,
isOrg: isOrg,
parentTeamName: currentUserParentTeamName,
isExistingUserMovedToOrg: true,
prevLink: `${getOrgFullOrigin("")}/${user.username || ""}`,
newLink: user.profile ? `${getOrgFullOrigin(orgSlug ?? "")}/${user.profile.username}` : null,
});
}
});
await sendEmails(sendEmailsPromises);
};
export async function handleExistingUsersInvites({
invitableExistingUsers,
team,