Skip to content

Commit 0cdd7c9

Browse files
Udit-takkarhariombalharahbjORbj
authored
refactor: create team service (calcom#22036)
* refactor: create team service * refactor: finish * tests: add unit test for team service * Add alternative approach to testing --------- Co-authored-by: Hariom <hariombalhara@gmail.com> Co-authored-by: Benny Joo <sldisek783@gmail.com>
1 parent f40a1c9 commit 0cdd7c9

12 files changed

Lines changed: 421 additions & 176 deletions

File tree

apps/web/app/(use-page-wrapper)/(main-nav)/teams/server-page.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { unstable_cache } from "next/cache";
55

66
import { TeamsListing } from "@calcom/features/ee/teams/components/TeamsListing";
77
import { TeamRepository } from "@calcom/lib/server/repository/team";
8+
import { TeamService } from "@calcom/lib/server/service/team";
89
import { meRouter } from "@calcom/trpc/server/routers/viewer/me/_router";
910

1011
import { TRPCError } from "@trpc/server";
@@ -37,7 +38,7 @@ export const ServerTeamsListing = async ({
3738

3839
if (token) {
3940
try {
40-
teamNameFromInvite = await TeamRepository.inviteMemberByToken(token, userId);
41+
teamNameFromInvite = await TeamService.inviteMemberByToken(token, userId);
4142
} catch (e) {
4243
errorMsgFromInvite = "Error while fetching teams";
4344
if (e instanceof TRPCError) errorMsgFromInvite = e.message;

packages/features/ee/workflows/lib/reminders/whatsappReminderManager.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import {
1616
} from "../reminders/templates/whatsapp/ContentSidMapping";
1717
import { scheduleSmsOrFallbackEmail, sendSmsOrFallbackEmail } from "./messageDispatcher";
1818
import type { ScheduleTextReminderArgs, timeUnitLowerCase } from "./smsReminderManager";
19-
import { deleteScheduledSMSReminder } from "./smsReminderManager";
2019
import {
2120
whatsappEventCancelledTemplate,
2221
whatsappEventCompletedTemplate,
@@ -268,7 +267,3 @@ export const scheduleWhatsappReminder = async (args: ScheduleTextReminderArgs) =
268267
}
269268
}
270269
};
271-
272-
export const deleteScheduledWhatsappReminder = async (reminderId: number, referenceId: string | null) => {
273-
return await deleteScheduledSMSReminder(reminderId, referenceId);
274-
};
Lines changed: 67 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,10 @@
11
import prismaMock from "../../../../tests/libs/__mocks__/prismaMock";
22

3+
import type { Team } from "@prisma/client";
34
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
45

5-
import { TeamBilling } from "@calcom/features/ee/billing/teams";
6-
7-
import { TRPCError } from "@trpc/server";
8-
96
import { TeamRepository } from "./team";
107

11-
vi.mock("@calcom/features/ee/billing/teams", () => ({
12-
TeamBilling: {
13-
findAndInit: vi.fn(),
14-
findAndInitMany: vi.fn(),
15-
},
16-
}));
17-
18-
vi.mock("@calcom/lib/domainManager/organization", () => ({
19-
deleteDomain: vi.fn(),
20-
}));
21-
22-
vi.mock("@calcom/features/ee/teams/lib/removeMember", () => ({
23-
default: vi.fn(),
24-
}));
25-
268
describe("TeamRepository", () => {
279
beforeEach(() => {
2810
vi.resetAllMocks();
@@ -54,83 +36,102 @@ describe("TeamRepository", () => {
5436
isPlatform: true,
5537
requestedSlug: null,
5638
};
57-
prismaMock.team.findUnique.mockResolvedValue(mockTeam);
39+
prismaMock.team.findUnique.mockResolvedValue(mockTeam as unknown as Team);
5840
const result = await TeamRepository.findById({ id: 1 });
5941
expect(result).toEqual(mockTeam);
6042
});
6143
});
6244

6345
describe("deleteById", () => {
6446
it("should delete team and related data", async () => {
65-
const mockDeletedTeam = { id: 1, name: "Deleted Team", isOrganization: true, slug: "deleted-team" };
66-
prismaMock.team.delete.mockResolvedValue(mockDeletedTeam);
67-
68-
const mockTeamBilling = { cancel: vi.fn() };
69-
TeamBilling.findAndInit.mockResolvedValue(mockTeamBilling);
70-
71-
// Mock the Prisma transaction
72-
const mockTransaction = {
73-
eventType: { deleteMany: vi.fn() },
74-
membership: { deleteMany: vi.fn() },
75-
team: { delete: vi.fn().mockResolvedValue(mockDeletedTeam) },
76-
};
77-
78-
// Mock the transaction calls so we can spy on it
79-
prismaMock.$transaction.mockImplementation((callback) => callback(mockTransaction));
47+
const mockDeletedTeam = { id: 1, name: "Deleted Team" };
48+
const deleteManyEventTypeMock = vi.fn();
49+
const deleteManyMembershipMock = vi.fn();
50+
const deleteTeamMock = vi.fn().mockResolvedValue(mockDeletedTeam as unknown as Team);
51+
prismaMock.$transaction.mockImplementation(async (callback) => {
52+
const mockTx = {
53+
...prismaMock,
54+
eventType: {
55+
...prismaMock.eventType,
56+
deleteMany: deleteManyEventTypeMock,
57+
},
58+
membership: {
59+
...prismaMock.membership,
60+
deleteMany: deleteManyMembershipMock,
61+
},
62+
team: {
63+
...prismaMock.team,
64+
delete: deleteTeamMock,
65+
},
66+
};
67+
return callback(mockTx);
68+
});
8069

8170
const result = await TeamRepository.deleteById({ id: 1 });
8271

83-
expect(mockTransaction.eventType.deleteMany).toHaveBeenCalledWith({
72+
expect(deleteManyEventTypeMock).toHaveBeenCalledWith({
8473
where: {
8574
teamId: 1,
8675
schedulingType: "MANAGED",
8776
},
8877
});
89-
expect(mockTransaction.membership.deleteMany).toHaveBeenCalledWith({
78+
expect(deleteManyMembershipMock).toHaveBeenCalledWith({
9079
where: {
9180
teamId: 1,
9281
},
9382
});
94-
expect(mockTransaction.team.delete).toHaveBeenCalledWith({
83+
expect(deleteTeamMock).toHaveBeenCalledWith({
9584
where: {
9685
id: 1,
9786
},
9887
});
99-
100-
expect(mockTeamBilling.cancel).toHaveBeenCalled();
10188
expect(result).toEqual(mockDeletedTeam);
10289
});
10390
});
10491

105-
describe("inviteMemberByToken", () => {
106-
it("should throw error if verification token is not found", async () => {
107-
prismaMock.verificationToken.findFirst.mockResolvedValue(null);
108-
await expect(TeamRepository.inviteMemberByToken("invalid-token", 1)).rejects.toThrow(TRPCError);
109-
});
110-
111-
it("should create membership and update billing", async () => {
112-
const mockToken = { teamId: 1, team: { name: "Test Team" } };
113-
prismaMock.verificationToken.findFirst.mockResolvedValue(mockToken);
114-
115-
const mockTeamBilling = { updateQuantity: vi.fn() };
116-
TeamBilling.findAndInit.mockResolvedValue(mockTeamBilling);
117-
118-
const result = await TeamRepository.inviteMemberByToken("valid-token", 1);
119-
120-
expect(prismaMock.membership.create).toHaveBeenCalled();
121-
expect(mockTeamBilling.updateQuantity).toHaveBeenCalled();
122-
expect(result).toBe("Test Team");
92+
describe("findAllByParentId", () => {
93+
it("should return all teams with given parentId", async () => {
94+
const mockTeams = [{ id: 1 }, { id: 2 }];
95+
prismaMock.team.findMany.mockResolvedValue(mockTeams as unknown as Team[]);
96+
const result = await TeamRepository.findAllByParentId({ parentId: 1 });
97+
expect(prismaMock.team.findMany).toHaveBeenCalledWith({
98+
where: { parentId: 1 },
99+
select: {
100+
id: true,
101+
name: true,
102+
slug: true,
103+
logoUrl: true,
104+
parentId: true,
105+
metadata: true,
106+
isOrganization: true,
107+
organizationSettings: true,
108+
isPlatform: true,
109+
},
110+
});
111+
expect(result).toEqual(mockTeams);
123112
});
124113
});
125114

126-
describe("publish", () => {
127-
it("should call publish on TeamBilling", async () => {
128-
const mockTeamBilling = { publish: vi.fn() };
129-
TeamBilling.findAndInit.mockResolvedValue(mockTeamBilling);
130-
131-
await TeamRepository.publish(1);
132-
133-
expect(mockTeamBilling.publish).toHaveBeenCalled();
115+
describe("findTeamWithMembers", () => {
116+
it("should return team with its members", async () => {
117+
const mockTeam = { id: 1, members: [] };
118+
prismaMock.team.findUnique.mockResolvedValue(mockTeam as unknown as Team & { members: [] });
119+
const result = await TeamRepository.findTeamWithMembers(1);
120+
expect(prismaMock.team.findUnique).toHaveBeenCalledWith({
121+
where: { id: 1 },
122+
select: {
123+
members: {
124+
select: {
125+
accepted: true,
126+
},
127+
},
128+
id: true,
129+
metadata: true,
130+
parentId: true,
131+
isOrganization: true,
132+
},
133+
});
134+
expect(result).toEqual(mockTeam);
134135
});
135136
});
136137
});

packages/lib/server/repository/team.ts

Lines changed: 0 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,10 @@ import { Prisma } from "@prisma/client";
22
import type { z } from "zod";
33

44
import { whereClauseForOrgWithSlugOrRequestedSlug } from "@calcom/ee/organizations/lib/orgDomains";
5-
import { TeamBilling } from "@calcom/features/ee/billing/teams";
6-
import removeMember from "@calcom/features/ee/teams/lib/removeMember";
7-
import { deleteDomain } from "@calcom/lib/domainManager/organization";
85
import logger from "@calcom/lib/logger";
96
import prisma from "@calcom/prisma";
10-
import { MembershipRole } from "@calcom/prisma/enums";
117
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
128

13-
import { TRPCError } from "@trpc/server";
14-
15-
import { WorkflowService } from "../service/workflows";
169
import { getParsedTeam } from "./teamUtils";
1710

1811
type TeamGetPayloadWithParsedMetadata<TeamSelect extends Prisma.TeamSelect> =
@@ -220,12 +213,6 @@ export class TeamRepository {
220213
}
221214

222215
static async deleteById({ id }: { id: number }) {
223-
try {
224-
await WorkflowService.deleteWorkflowRemindersOfRemovedTeam(id);
225-
} catch (e) {
226-
console.error(e);
227-
}
228-
229216
const deletedTeam = await prisma.$transaction(async (tx) => {
230217
await tx.eventType.deleteMany({
231218
where: {
@@ -247,94 +234,12 @@ export class TeamRepository {
247234
},
248235
});
249236

250-
const teamBilling = await TeamBilling.findAndInit(id);
251-
await teamBilling.cancel();
252237
return deletedTeam;
253238
});
254239

255-
if (deletedTeam?.isOrganization && deletedTeam.slug) deleteDomain(deletedTeam.slug);
256-
257240
return deletedTeam;
258241
}
259242

260-
// TODO: Move errors away from TRPC error to make it more generic
261-
static async inviteMemberByToken(token: string, userId: number) {
262-
const verificationToken = await prisma.verificationToken.findFirst({
263-
where: {
264-
token,
265-
OR: [{ expiresInDays: null }, { expires: { gte: new Date() } }],
266-
},
267-
include: {
268-
team: {
269-
select: {
270-
name: true,
271-
},
272-
},
273-
},
274-
});
275-
276-
if (!verificationToken) throw new TRPCError({ code: "NOT_FOUND", message: "Invite not found" });
277-
if (!verificationToken.teamId || !verificationToken.team)
278-
throw new TRPCError({
279-
code: "NOT_FOUND",
280-
message: "Invite token is not associated with any team",
281-
});
282-
283-
try {
284-
await prisma.membership.create({
285-
data: {
286-
createdAt: new Date(),
287-
teamId: verificationToken.teamId,
288-
userId: userId,
289-
role: MembershipRole.MEMBER,
290-
accepted: false,
291-
},
292-
});
293-
} catch (e) {
294-
if (e instanceof Prisma.PrismaClientKnownRequestError) {
295-
if (e.code === "P2002") {
296-
throw new TRPCError({
297-
code: "FORBIDDEN",
298-
message: "This user is a member of this team / has a pending invitation.",
299-
});
300-
}
301-
} else throw e;
302-
}
303-
304-
const teamBilling = await TeamBilling.findAndInit(verificationToken.teamId);
305-
await teamBilling.updateQuantity();
306-
307-
return verificationToken.team.name;
308-
}
309-
310-
static async publish(teamId: number) {
311-
const teamBilling = await TeamBilling.findAndInit(teamId);
312-
return teamBilling.publish();
313-
}
314-
315-
static async removeMembers(teamIds: number[], memberIds: number[], isOrg = false) {
316-
const deleteMembershipPromises = [];
317-
318-
for (const memberId of memberIds) {
319-
for (const teamId of teamIds) {
320-
deleteMembershipPromises.push(
321-
// This removeMember function is from @calcom/features/ee/teams/lib/removeMember.ts we should probably move it to this repository.
322-
removeMember({
323-
teamId,
324-
memberId,
325-
isOrg,
326-
})
327-
);
328-
}
329-
}
330-
331-
await Promise.all(deleteMembershipPromises);
332-
333-
const teamsBilling = await TeamBilling.findAndInitMany(teamIds);
334-
const teamBillingPromises = teamsBilling.map((teamBilling) => teamBilling.updateQuantity());
335-
await Promise.allSettled(teamBillingPromises);
336-
}
337-
338243
static async findTeamWithMembers(teamId: number) {
339244
return await prisma.team.findUnique({
340245
where: { id: teamId },

packages/lib/server/repository/workflow.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { z } from "zod";
33
import type { WorkflowType } from "@calcom/ee/workflows/components/WorkflowListPage";
44
import { deleteScheduledEmailReminder } from "@calcom/ee/workflows/lib/reminders/emailReminderManager";
55
import { deleteScheduledSMSReminder } from "@calcom/ee/workflows/lib/reminders/smsReminderManager";
6-
import { deleteScheduledWhatsappReminder } from "@calcom/ee/workflows/lib/reminders/whatsappReminderManager";
76
import type { WorkflowStep } from "@calcom/ee/workflows/lib/types";
87
import { hasFilter } from "@calcom/features/filters/lib/hasFilter";
98
import prisma from "@calcom/prisma";
@@ -22,6 +21,8 @@ export const ZGetInputSchema = z.object({
2221

2322
export type TGetInputSchema = z.infer<typeof ZGetInputSchema>;
2423

24+
const deleteScheduledWhatsappReminder = deleteScheduledSMSReminder;
25+
2526
const { include: includedFields } = Prisma.validator<Prisma.WorkflowDefaultArgs>()({
2627
include: {
2728
activeOn: {

0 commit comments

Comments
 (0)