Skip to content

Commit 6c3b106

Browse files
authored
fix: managed event type bug (#27047)
1 parent cb6b52b commit 6c3b106

3 files changed

Lines changed: 198 additions & 0 deletions

File tree

apps/web/test/lib/handleChildrenEventTypes.test.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,9 @@ describe("handleChildrenEventTypes", () => {
240240
// @ts-expect-error - partial mock for test purposes
241241
prismaMock.eventType.findMany.mockResolvedValue([{ userId: 4, metadata: {} }]);
242242

243+
// @ts-expect-error - partial mock for test purposes
244+
prismaMock.eventType.update.mockResolvedValue({ id: 100 });
245+
243246
const result = await updateChildrenEventTypes({
244247
eventTypeId: 1,
245248
oldEventType: { children: [{ userId: 4 }], team: { name: "" } },
@@ -313,6 +316,9 @@ describe("handleChildrenEventTypes", () => {
313316
// @ts-expect-error - partial mock for test purposes
314317
prismaMock.eventType.findMany.mockResolvedValue([{ userId: 4, metadata: {} }]);
315318

319+
// @ts-expect-error - partial mock for test purposes
320+
prismaMock.eventType.update.mockResolvedValue({ id: 101 });
321+
316322
const result = await updateChildrenEventTypes({
317323
eventTypeId: 1,
318324
oldEventType: { children: [{ userId: 4 }, { userId: 1 }], team: { name: "" } },
@@ -450,6 +456,9 @@ describe("handleChildrenEventTypes", () => {
450456
// @ts-expect-error - partial mock for test purposes
451457
prismaMock.eventType.findMany.mockResolvedValue([{ userId: 4, metadata: {} }]);
452458

459+
// @ts-expect-error - partial mock for test purposes
460+
prismaMock.eventType.update.mockResolvedValue({ id: 102 });
461+
453462
prismaMock.eventType.deleteMany.mockResolvedValue([123] as unknown as Prisma.BatchPayload);
454463
const result = await updateChildrenEventTypes({
455464
eventTypeId: 1,
@@ -960,4 +969,98 @@ describe("handleChildrenEventTypes", () => {
960969
});
961970
});
962971
});
972+
973+
describe("CalVideoSettings propagation", () => {
974+
it("Creates CalVideoSettings for new children when parent has settings", async () => {
975+
const { schedulingType, teamId, timeZone, ...evType } = mockFindFirstEventType({
976+
id: 123,
977+
metadata: { managedEventConfig: {} },
978+
locations: [],
979+
});
980+
981+
setupTransactionMock();
982+
prismaMock.eventType.createManyAndReturn.mockResolvedValue([
983+
{ ...evType, id: 456, userId: 4, schedulingType, teamId, timeZone } as unknown as EventType,
984+
]);
985+
986+
await updateChildrenEventTypes({
987+
eventTypeId: 1,
988+
oldEventType: { children: [], team: { name: "" } },
989+
children: [{ hidden: false, owner: { id: 4, name: "", email: "", eventTypeSlugs: [] } }],
990+
updatedEventType: { schedulingType: "MANAGED", slug: "something" },
991+
currentUserId: 1,
992+
prisma: prismaMock,
993+
profileId: null,
994+
updatedValues: {},
995+
calVideoSettings: {
996+
enableAutomaticRecordingForOrganizer: true,
997+
enableAutomaticTranscription: true,
998+
},
999+
});
1000+
1001+
expect(prismaMock.calVideoSettings.createMany).toHaveBeenCalledWith({
1002+
data: [
1003+
expect.objectContaining({
1004+
eventTypeId: 456,
1005+
enableAutomaticRecordingForOrganizer: true,
1006+
enableAutomaticTranscription: true,
1007+
}),
1008+
],
1009+
skipDuplicates: true,
1010+
});
1011+
});
1012+
1013+
it("Syncs/deletes CalVideoSettings for existing children based on parent settings", async () => {
1014+
const mockExistingChildUpdate = () => {
1015+
mockFindFirstEventType({ metadata: { managedEventConfig: {} }, locations: [] });
1016+
// @ts-expect-error - partial mock
1017+
prismaMock.eventType.findMany.mockResolvedValue([{ userId: 4, metadata: {} }]);
1018+
prismaMock.eventType.update.mockResolvedValue({
1019+
id: 789,
1020+
userId: 4,
1021+
schedulingType: SchedulingType.MANAGED,
1022+
} as unknown as EventType);
1023+
};
1024+
1025+
const baseParams = {
1026+
eventTypeId: 1,
1027+
oldEventType: { children: [{ userId: 4 }], team: { name: "" } },
1028+
children: [{ hidden: false, owner: { id: 4, name: "", email: "", eventTypeSlugs: [] } }],
1029+
updatedEventType: { schedulingType: "MANAGED" as const, slug: "something" },
1030+
currentUserId: 1,
1031+
prisma: prismaMock,
1032+
profileId: null,
1033+
updatedValues: {},
1034+
};
1035+
1036+
// Test upsert when calVideoSettings provided
1037+
mockExistingChildUpdate();
1038+
await updateChildrenEventTypes({
1039+
...baseParams,
1040+
calVideoSettings: { enableAutomaticRecordingForOrganizer: true },
1041+
});
1042+
expect(prismaMock.calVideoSettings.upsert).toHaveBeenCalledWith(
1043+
expect.objectContaining({
1044+
where: { eventTypeId: 789 },
1045+
update: expect.objectContaining({ enableAutomaticRecordingForOrganizer: true }),
1046+
create: expect.objectContaining({ eventTypeId: 789, enableAutomaticRecordingForOrganizer: true }),
1047+
})
1048+
);
1049+
1050+
// Test deleteMany when calVideoSettings is null
1051+
vi.clearAllMocks();
1052+
mockExistingChildUpdate();
1053+
await updateChildrenEventTypes({ ...baseParams, calVideoSettings: null });
1054+
expect(prismaMock.calVideoSettings.deleteMany).toHaveBeenCalledWith({
1055+
where: { eventTypeId: { in: [789] } },
1056+
});
1057+
1058+
// Test no-op when calVideoSettings is undefined
1059+
vi.clearAllMocks();
1060+
mockExistingChildUpdate();
1061+
await updateChildrenEventTypes({ ...baseParams, calVideoSettings: undefined });
1062+
expect(prismaMock.calVideoSettings.upsert).not.toHaveBeenCalled();
1063+
expect(prismaMock.calVideoSettings.deleteMany).not.toHaveBeenCalled();
1064+
});
1065+
});
9631066
});

packages/features/ee/managed-event-types/lib/handleChildrenEventTypes.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,16 @@ interface handleChildrenEventTypesProps {
4040
| undefined;
4141
prisma: PrismaClient | DeepMockProxy<PrismaClient>;
4242
updatedValues: Prisma.EventTypeUpdateInput;
43+
calVideoSettings?: {
44+
disableRecordingForGuests?: boolean | null;
45+
disableRecordingForOrganizer?: boolean | null;
46+
enableAutomaticTranscription?: boolean | null;
47+
enableAutomaticRecordingForOrganizer?: boolean | null;
48+
disableTranscriptionForGuests?: boolean | null;
49+
disableTranscriptionForOrganizer?: boolean | null;
50+
redirectUrlOnExit?: string | null;
51+
requireEmailForGuests?: boolean | null;
52+
} | null;
4353
}
4454

4555
const sendAllSlugReplacementEmails = async (
@@ -138,6 +148,7 @@ export default async function handleChildrenEventTypes({
138148
prisma,
139149
profileId,
140150
updatedValues: _updatedValues,
151+
calVideoSettings,
141152
}: handleChildrenEventTypesProps) {
142153
// Check we are dealing with a managed event type
143154
if (updatedEventType?.schedulingType !== SchedulingType.MANAGED)
@@ -269,6 +280,26 @@ export default async function handleChildrenEventTypes({
269280
skipDuplicates: true,
270281
});
271282
}
283+
284+
// Create CalVideoSettings for new children if parent has settings
285+
if (calVideoSettings && createdEvents.length > 0) {
286+
const calVideoSettingsToCreate = createdEvents.map((event) => ({
287+
eventTypeId: event.id,
288+
disableRecordingForGuests: calVideoSettings.disableRecordingForGuests ?? false,
289+
disableRecordingForOrganizer: calVideoSettings.disableRecordingForOrganizer ?? false,
290+
enableAutomaticTranscription: calVideoSettings.enableAutomaticTranscription ?? false,
291+
enableAutomaticRecordingForOrganizer: calVideoSettings.enableAutomaticRecordingForOrganizer ?? false,
292+
disableTranscriptionForGuests: calVideoSettings.disableTranscriptionForGuests ?? false,
293+
disableTranscriptionForOrganizer: calVideoSettings.disableTranscriptionForOrganizer ?? false,
294+
redirectUrlOnExit: calVideoSettings.redirectUrlOnExit ?? null,
295+
requireEmailForGuests: calVideoSettings.requireEmailForGuests ?? false,
296+
}));
297+
298+
await tx.calVideoSettings.createMany({
299+
data: calVideoSettingsToCreate,
300+
skipDuplicates: true,
301+
});
302+
}
272303
});
273304
}
274305

@@ -422,6 +453,58 @@ export default async function handleChildrenEventTypes({
422453
}
423454
});
424455
}
456+
457+
// Sync CalVideoSettings to existing children
458+
if (oldEventTypes.length > 0) {
459+
const childEventTypeIds = oldEventTypes.map((e) => e.id);
460+
const BATCH_SIZE = 50;
461+
462+
if (calVideoSettings) {
463+
// Parent has CalVideoSettings - upsert for all children
464+
for (let i = 0; i < childEventTypeIds.length; i += BATCH_SIZE) {
465+
const batch = childEventTypeIds.slice(i, i + BATCH_SIZE);
466+
await Promise.all(
467+
batch.map((eventTypeId) =>
468+
prisma.calVideoSettings.upsert({
469+
where: { eventTypeId },
470+
update: {
471+
disableRecordingForGuests: calVideoSettings.disableRecordingForGuests ?? false,
472+
disableRecordingForOrganizer: calVideoSettings.disableRecordingForOrganizer ?? false,
473+
enableAutomaticTranscription: calVideoSettings.enableAutomaticTranscription ?? false,
474+
enableAutomaticRecordingForOrganizer:
475+
calVideoSettings.enableAutomaticRecordingForOrganizer ?? false,
476+
disableTranscriptionForGuests: calVideoSettings.disableTranscriptionForGuests ?? false,
477+
disableTranscriptionForOrganizer: calVideoSettings.disableTranscriptionForOrganizer ?? false,
478+
redirectUrlOnExit: calVideoSettings.redirectUrlOnExit ?? null,
479+
requireEmailForGuests: calVideoSettings.requireEmailForGuests ?? false,
480+
updatedAt: new Date(),
481+
},
482+
create: {
483+
eventTypeId,
484+
disableRecordingForGuests: calVideoSettings.disableRecordingForGuests ?? false,
485+
disableRecordingForOrganizer: calVideoSettings.disableRecordingForOrganizer ?? false,
486+
enableAutomaticTranscription: calVideoSettings.enableAutomaticTranscription ?? false,
487+
enableAutomaticRecordingForOrganizer:
488+
calVideoSettings.enableAutomaticRecordingForOrganizer ?? false,
489+
disableTranscriptionForGuests: calVideoSettings.disableTranscriptionForGuests ?? false,
490+
disableTranscriptionForOrganizer: calVideoSettings.disableTranscriptionForOrganizer ?? false,
491+
redirectUrlOnExit: calVideoSettings.redirectUrlOnExit ?? null,
492+
requireEmailForGuests: calVideoSettings.requireEmailForGuests ?? false,
493+
},
494+
})
495+
)
496+
);
497+
}
498+
} else if (calVideoSettings === null) {
499+
// Parent's CalVideoSettings were removed - delete from all children
500+
await prisma.calVideoSettings.deleteMany({
501+
where: {
502+
eventTypeId: { in: childEventTypeIds },
503+
},
504+
});
505+
}
506+
// Note: If calVideoSettings is undefined, don't touch children's settings
507+
}
425508
}
426509

427510
// Old users deleted

packages/trpc/server/routers/viewer/eventTypes/heavy/update.handler.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -716,6 +716,17 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
716716
return acc;
717717
}, {});
718718

719+
// Determine calVideoSettings to pass to children:
720+
// - If calVideoSettings provided in input, sync to children
721+
// - If Cal Video location removed, delete from children (pass null)
722+
// - Otherwise, leave children's settings untouched (pass undefined)
723+
let calVideoSettingsForChildren: typeof calVideoSettings | null | undefined = undefined;
724+
if (calVideoSettings !== undefined) {
725+
calVideoSettingsForChildren = calVideoSettings;
726+
} else if (eventType.calVideoSettings && !isCalVideoLocationActive) {
727+
calVideoSettingsForChildren = null;
728+
}
729+
719730
// Handling updates to children event types (managed events types)
720731
await updateChildrenEventTypes({
721732
eventTypeId: id,
@@ -726,6 +737,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
726737
profileId: ctx.user.profile.id,
727738
prisma: ctx.prisma,
728739
updatedValues,
740+
calVideoSettings: calVideoSettingsForChildren,
729741
});
730742

731743
// Clean up empty host groups

0 commit comments

Comments
 (0)