diff --git a/apps/web/test/lib/handleChildrenEventTypes.test.ts b/apps/web/test/lib/handleChildrenEventTypes.test.ts index 0b96e82b4f..d784f653ca 100644 --- a/apps/web/test/lib/handleChildrenEventTypes.test.ts +++ b/apps/web/test/lib/handleChildrenEventTypes.test.ts @@ -240,6 +240,9 @@ describe("handleChildrenEventTypes", () => { // @ts-expect-error - partial mock for test purposes prismaMock.eventType.findMany.mockResolvedValue([{ userId: 4, metadata: {} }]); + // @ts-expect-error - partial mock for test purposes + prismaMock.eventType.update.mockResolvedValue({ id: 100 }); + const result = await updateChildrenEventTypes({ eventTypeId: 1, oldEventType: { children: [{ userId: 4 }], team: { name: "" } }, @@ -313,6 +316,9 @@ describe("handleChildrenEventTypes", () => { // @ts-expect-error - partial mock for test purposes prismaMock.eventType.findMany.mockResolvedValue([{ userId: 4, metadata: {} }]); + // @ts-expect-error - partial mock for test purposes + prismaMock.eventType.update.mockResolvedValue({ id: 101 }); + const result = await updateChildrenEventTypes({ eventTypeId: 1, oldEventType: { children: [{ userId: 4 }, { userId: 1 }], team: { name: "" } }, @@ -450,6 +456,9 @@ describe("handleChildrenEventTypes", () => { // @ts-expect-error - partial mock for test purposes prismaMock.eventType.findMany.mockResolvedValue([{ userId: 4, metadata: {} }]); + // @ts-expect-error - partial mock for test purposes + prismaMock.eventType.update.mockResolvedValue({ id: 102 }); + prismaMock.eventType.deleteMany.mockResolvedValue([123] as unknown as Prisma.BatchPayload); const result = await updateChildrenEventTypes({ eventTypeId: 1, @@ -960,4 +969,98 @@ describe("handleChildrenEventTypes", () => { }); }); }); + + describe("CalVideoSettings propagation", () => { + it("Creates CalVideoSettings for new children when parent has settings", async () => { + const { schedulingType, teamId, timeZone, ...evType } = mockFindFirstEventType({ + id: 123, + metadata: { managedEventConfig: {} }, + locations: [], + }); + + setupTransactionMock(); + prismaMock.eventType.createManyAndReturn.mockResolvedValue([ + { ...evType, id: 456, userId: 4, schedulingType, teamId, timeZone } as unknown as EventType, + ]); + + await updateChildrenEventTypes({ + eventTypeId: 1, + oldEventType: { children: [], team: { name: "" } }, + children: [{ hidden: false, owner: { id: 4, name: "", email: "", eventTypeSlugs: [] } }], + updatedEventType: { schedulingType: "MANAGED", slug: "something" }, + currentUserId: 1, + prisma: prismaMock, + profileId: null, + updatedValues: {}, + calVideoSettings: { + enableAutomaticRecordingForOrganizer: true, + enableAutomaticTranscription: true, + }, + }); + + expect(prismaMock.calVideoSettings.createMany).toHaveBeenCalledWith({ + data: [ + expect.objectContaining({ + eventTypeId: 456, + enableAutomaticRecordingForOrganizer: true, + enableAutomaticTranscription: true, + }), + ], + skipDuplicates: true, + }); + }); + + it("Syncs/deletes CalVideoSettings for existing children based on parent settings", async () => { + const mockExistingChildUpdate = () => { + mockFindFirstEventType({ metadata: { managedEventConfig: {} }, locations: [] }); + // @ts-expect-error - partial mock + prismaMock.eventType.findMany.mockResolvedValue([{ userId: 4, metadata: {} }]); + prismaMock.eventType.update.mockResolvedValue({ + id: 789, + userId: 4, + schedulingType: SchedulingType.MANAGED, + } as unknown as EventType); + }; + + const baseParams = { + eventTypeId: 1, + oldEventType: { children: [{ userId: 4 }], team: { name: "" } }, + children: [{ hidden: false, owner: { id: 4, name: "", email: "", eventTypeSlugs: [] } }], + updatedEventType: { schedulingType: "MANAGED" as const, slug: "something" }, + currentUserId: 1, + prisma: prismaMock, + profileId: null, + updatedValues: {}, + }; + + // Test upsert when calVideoSettings provided + mockExistingChildUpdate(); + await updateChildrenEventTypes({ + ...baseParams, + calVideoSettings: { enableAutomaticRecordingForOrganizer: true }, + }); + expect(prismaMock.calVideoSettings.upsert).toHaveBeenCalledWith( + expect.objectContaining({ + where: { eventTypeId: 789 }, + update: expect.objectContaining({ enableAutomaticRecordingForOrganizer: true }), + create: expect.objectContaining({ eventTypeId: 789, enableAutomaticRecordingForOrganizer: true }), + }) + ); + + // Test deleteMany when calVideoSettings is null + vi.clearAllMocks(); + mockExistingChildUpdate(); + await updateChildrenEventTypes({ ...baseParams, calVideoSettings: null }); + expect(prismaMock.calVideoSettings.deleteMany).toHaveBeenCalledWith({ + where: { eventTypeId: { in: [789] } }, + }); + + // Test no-op when calVideoSettings is undefined + vi.clearAllMocks(); + mockExistingChildUpdate(); + await updateChildrenEventTypes({ ...baseParams, calVideoSettings: undefined }); + expect(prismaMock.calVideoSettings.upsert).not.toHaveBeenCalled(); + expect(prismaMock.calVideoSettings.deleteMany).not.toHaveBeenCalled(); + }); + }); }); diff --git a/packages/features/ee/managed-event-types/lib/handleChildrenEventTypes.ts b/packages/features/ee/managed-event-types/lib/handleChildrenEventTypes.ts index ac633b6b75..fb2fa83291 100644 --- a/packages/features/ee/managed-event-types/lib/handleChildrenEventTypes.ts +++ b/packages/features/ee/managed-event-types/lib/handleChildrenEventTypes.ts @@ -40,6 +40,16 @@ interface handleChildrenEventTypesProps { | undefined; prisma: PrismaClient | DeepMockProxy; updatedValues: Prisma.EventTypeUpdateInput; + calVideoSettings?: { + disableRecordingForGuests?: boolean | null; + disableRecordingForOrganizer?: boolean | null; + enableAutomaticTranscription?: boolean | null; + enableAutomaticRecordingForOrganizer?: boolean | null; + disableTranscriptionForGuests?: boolean | null; + disableTranscriptionForOrganizer?: boolean | null; + redirectUrlOnExit?: string | null; + requireEmailForGuests?: boolean | null; + } | null; } const sendAllSlugReplacementEmails = async ( @@ -138,6 +148,7 @@ export default async function handleChildrenEventTypes({ prisma, profileId, updatedValues: _updatedValues, + calVideoSettings, }: handleChildrenEventTypesProps) { // Check we are dealing with a managed event type if (updatedEventType?.schedulingType !== SchedulingType.MANAGED) @@ -269,6 +280,26 @@ export default async function handleChildrenEventTypes({ skipDuplicates: true, }); } + + // Create CalVideoSettings for new children if parent has settings + if (calVideoSettings && createdEvents.length > 0) { + const calVideoSettingsToCreate = createdEvents.map((event) => ({ + eventTypeId: event.id, + disableRecordingForGuests: calVideoSettings.disableRecordingForGuests ?? false, + disableRecordingForOrganizer: calVideoSettings.disableRecordingForOrganizer ?? false, + enableAutomaticTranscription: calVideoSettings.enableAutomaticTranscription ?? false, + enableAutomaticRecordingForOrganizer: calVideoSettings.enableAutomaticRecordingForOrganizer ?? false, + disableTranscriptionForGuests: calVideoSettings.disableTranscriptionForGuests ?? false, + disableTranscriptionForOrganizer: calVideoSettings.disableTranscriptionForOrganizer ?? false, + redirectUrlOnExit: calVideoSettings.redirectUrlOnExit ?? null, + requireEmailForGuests: calVideoSettings.requireEmailForGuests ?? false, + })); + + await tx.calVideoSettings.createMany({ + data: calVideoSettingsToCreate, + skipDuplicates: true, + }); + } }); } @@ -422,6 +453,58 @@ export default async function handleChildrenEventTypes({ } }); } + + // Sync CalVideoSettings to existing children + if (oldEventTypes.length > 0) { + const childEventTypeIds = oldEventTypes.map((e) => e.id); + const BATCH_SIZE = 50; + + if (calVideoSettings) { + // Parent has CalVideoSettings - upsert for all children + for (let i = 0; i < childEventTypeIds.length; i += BATCH_SIZE) { + const batch = childEventTypeIds.slice(i, i + BATCH_SIZE); + await Promise.all( + batch.map((eventTypeId) => + prisma.calVideoSettings.upsert({ + where: { eventTypeId }, + update: { + disableRecordingForGuests: calVideoSettings.disableRecordingForGuests ?? false, + disableRecordingForOrganizer: calVideoSettings.disableRecordingForOrganizer ?? false, + enableAutomaticTranscription: calVideoSettings.enableAutomaticTranscription ?? false, + enableAutomaticRecordingForOrganizer: + calVideoSettings.enableAutomaticRecordingForOrganizer ?? false, + disableTranscriptionForGuests: calVideoSettings.disableTranscriptionForGuests ?? false, + disableTranscriptionForOrganizer: calVideoSettings.disableTranscriptionForOrganizer ?? false, + redirectUrlOnExit: calVideoSettings.redirectUrlOnExit ?? null, + requireEmailForGuests: calVideoSettings.requireEmailForGuests ?? false, + updatedAt: new Date(), + }, + create: { + eventTypeId, + disableRecordingForGuests: calVideoSettings.disableRecordingForGuests ?? false, + disableRecordingForOrganizer: calVideoSettings.disableRecordingForOrganizer ?? false, + enableAutomaticTranscription: calVideoSettings.enableAutomaticTranscription ?? false, + enableAutomaticRecordingForOrganizer: + calVideoSettings.enableAutomaticRecordingForOrganizer ?? false, + disableTranscriptionForGuests: calVideoSettings.disableTranscriptionForGuests ?? false, + disableTranscriptionForOrganizer: calVideoSettings.disableTranscriptionForOrganizer ?? false, + redirectUrlOnExit: calVideoSettings.redirectUrlOnExit ?? null, + requireEmailForGuests: calVideoSettings.requireEmailForGuests ?? false, + }, + }) + ) + ); + } + } else if (calVideoSettings === null) { + // Parent's CalVideoSettings were removed - delete from all children + await prisma.calVideoSettings.deleteMany({ + where: { + eventTypeId: { in: childEventTypeIds }, + }, + }); + } + // Note: If calVideoSettings is undefined, don't touch children's settings + } } // Old users deleted diff --git a/packages/trpc/server/routers/viewer/eventTypes/heavy/update.handler.ts b/packages/trpc/server/routers/viewer/eventTypes/heavy/update.handler.ts index cef6bd1253..4fbaf38a8e 100644 --- a/packages/trpc/server/routers/viewer/eventTypes/heavy/update.handler.ts +++ b/packages/trpc/server/routers/viewer/eventTypes/heavy/update.handler.ts @@ -716,6 +716,17 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => { return acc; }, {}); + // Determine calVideoSettings to pass to children: + // - If calVideoSettings provided in input, sync to children + // - If Cal Video location removed, delete from children (pass null) + // - Otherwise, leave children's settings untouched (pass undefined) + let calVideoSettingsForChildren: typeof calVideoSettings | null | undefined = undefined; + if (calVideoSettings !== undefined) { + calVideoSettingsForChildren = calVideoSettings; + } else if (eventType.calVideoSettings && !isCalVideoLocationActive) { + calVideoSettingsForChildren = null; + } + // Handling updates to children event types (managed events types) await updateChildrenEventTypes({ eventTypeId: id, @@ -726,6 +737,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => { profileId: ctx.user.profile.id, prisma: ctx.prisma, updatedValues, + calVideoSettings: calVideoSettingsForChildren, }); // Clean up empty host groups