From f58719459394905c1573a71977478d8dbb6bbafc Mon Sep 17 00:00:00 2001 From: Deepak Sharma Date: Sat, 20 Dec 2025 14:21:28 +0530 Subject: [PATCH 01/15] feat: add custom calendar reminder for Google Calendar events Allows users to set default reminder notifications (10, 30, or 60 minutes) for events created via Cal.com bookings in Google Calendar. This addresses the issue where Google Calendar's default reminders don't apply to API-created events. - Add customCalendarReminder field to DestinationCalendar model - Create reminder selector UI in destination calendar settings - Update GoogleCalendarService to apply custom reminders (popup + email) - Add TRPC endpoint for updating reminder settings --- apps/web/public/static/locales/en/common.json | 3 + .../googlecalendar/lib/CalendarService.ts | 44 +++++++++++-- .../lib/__tests__/CalendarService.test.ts | 63 +++++++++++++++++++ .../DestinationCalendar.tsx | 28 ++++++++- .../DestinationReminderSelector.tsx | 47 ++++++++++++++ .../DestinationCalendarSettingsWebWrapper.tsx | 25 ++++++++ .../migration.sql | 2 + packages/prisma/schema.prisma | 1 + .../routers/viewer/calendars/_router.tsx | 9 +++ .../setDestinationReminder.handler.ts | 30 +++++++++ .../setDestinationReminder.schema.ts | 9 +++ 11 files changed, 252 insertions(+), 9 deletions(-) create mode 100644 packages/platform/atoms/destination-calendar/DestinationReminderSelector.tsx create mode 100644 packages/prisma/migrations/20251220034814_add_custom_calendar_reminder/migration.sql create mode 100644 packages/trpc/server/routers/viewer/calendars/setDestinationReminder.handler.ts create mode 100644 packages/trpc/server/routers/viewer/calendars/setDestinationReminder.schema.ts diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index d3a9eb82388c5f..1e10fe4a44ed18 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -429,6 +429,9 @@ "add_to_calendar": "Add to calendar", "add_to_calendar_description": "Select where to add events when you're booked.", "add_events_to": "Add events to", + "default_reminder": "Default reminder", + "default_reminder_description": "Set the default reminder time for events added to your Google Calendar.", + "remind_minutes_before": "{{count}} minutes before", "add_another_calendar": "Add another calendar", "other": "Other", "email_sign_in_subject": "Your sign-in link for {{appName}}", diff --git a/packages/app-store/googlecalendar/lib/CalendarService.ts b/packages/app-store/googlecalendar/lib/CalendarService.ts index 41ae0080c3a3f6..5b1ac7a566a950 100644 --- a/packages/app-store/googlecalendar/lib/CalendarService.ts +++ b/packages/app-store/googlecalendar/lib/CalendarService.ts @@ -11,6 +11,7 @@ import { ORGANIZER_EMAIL_EXEMPT_DOMAINS } from "@calcom/lib/constants"; import logger from "@calcom/lib/logger"; import { safeStringify } from "@calcom/lib/safeStringify"; import { SelectedCalendarRepository } from "@calcom/lib/server/repository/selectedCalendar"; +import { prisma } from "@calcom/prisma"; import type { Prisma } from "@calcom/prisma/client"; import type { Calendar, @@ -56,6 +57,35 @@ export default class GoogleCalendarService implements Calendar { this.log = log.getSubLogger({ prefix: [`[[lib] ${this.integrationName}`] }); } + private async getReminderDuration(credentialId: number): Promise { + try { + const destinationCalendar = await prisma.destinationCalendar.findFirst({ + where: { credentialId }, + select: { customCalendarReminder: true }, + }); + return destinationCalendar?.customCalendarReminder ?? null; + } catch (error) { + this.log.warn("Failed to fetch custom calendar reminder", safeStringify(error)); + return null; + } + } + + private getReminders(customReminderMinutes: number | null): { + useDefault: boolean; + overrides?: { method: string; minutes: number }[]; + } { + if (customReminderMinutes !== null) { + return { + useDefault: false, + overrides: [ + { method: "popup", minutes: customReminderMinutes }, + { method: "email", minutes: customReminderMinutes }, + ], + }; + } + return { useDefault: true }; + } + public getCredentialId() { return this.credential.id; } @@ -171,6 +201,9 @@ export default class GoogleCalendarService implements Calendar { ): Promise { this.log.debug("Creating event"); + // Fetch custom reminder duration for this credential's destination calendar + const customReminderMinutes = await this.getReminderDuration(credentialId); + const payload: calendar_v3.Schema$Event = { summary: calEvent.title, description: calEvent.calendarDescription, @@ -183,9 +216,7 @@ export default class GoogleCalendarService implements Calendar { timeZone: calEvent.organizer.timeZone, }, attendees: this.getAttendees({ event: calEvent, hostExternalCalendarId: externalCalendarId }), - reminders: { - useDefault: true, - }, + reminders: this.getReminders(customReminderMinutes), guestsCanSeeOtherGuests: calEvent.seatsPerTimeSlot ? calEvent.seatsShowAttendees : true, iCalUID: calEvent.iCalUID, }; @@ -330,6 +361,9 @@ export default class GoogleCalendarService implements Calendar { } async updateEvent(uid: string, event: CalendarServiceEvent, externalCalendarId: string): Promise { + // Fetch custom reminder duration for this credential's destination calendar + const customReminderMinutes = await this.getReminderDuration(this.credential.id); + const payload: calendar_v3.Schema$Event = { summary: event.title, description: event.calendarDescription, @@ -342,9 +376,7 @@ export default class GoogleCalendarService implements Calendar { timeZone: event.organizer.timeZone, }, attendees: this.getAttendees({ event, hostExternalCalendarId: externalCalendarId }), - reminders: { - useDefault: true, - }, + reminders: this.getReminders(customReminderMinutes), guestsCanSeeOtherGuests: event.seatsPerTimeSlot ? event.seatsShowAttendees : true, }; diff --git a/packages/app-store/googlecalendar/lib/__tests__/CalendarService.test.ts b/packages/app-store/googlecalendar/lib/__tests__/CalendarService.test.ts index 318d0ce6241f4b..dd17aab5080566 100644 --- a/packages/app-store/googlecalendar/lib/__tests__/CalendarService.test.ts +++ b/packages/app-store/googlecalendar/lib/__tests__/CalendarService.test.ts @@ -458,6 +458,7 @@ describe("createEvent", () => { domainWideDelegationCredentialId: null, createdAt: new Date("2024-06-15T11:00:00Z"), updatedAt: new Date("2024-06-15T11:00:00Z"), + customCalendarReminder: 10, }, ], iCalUID: "test-ical-uid@google.com", @@ -632,6 +633,7 @@ describe("createEvent", () => { domainWideDelegationCredentialId: null, createdAt: new Date("2024-06-15T11:00:00Z"), updatedAt: new Date("2024-06-15T11:00:00Z"), + customCalendarReminder: 10, }, ], calendarDescription: "Weekly team meeting", @@ -670,4 +672,65 @@ describe("createEvent", () => { log.info("createEvent recurring event test passed"); }); + + test("should use default reminders when no custom reminder is configured", async () => { + const calendarService = new CalendarService(mockCredential); + setFullMockOAuthManagerRequest(); + + const mockGoogleEvent = { + id: "mock-event-default-reminder", + summary: "Test Meeting Default Reminder", + start: { dateTime: "2024-06-15T10:00:00Z", timeZone: "UTC" }, + end: { dateTime: "2024-06-15T11:00:00Z", timeZone: "UTC" }, + }; + + const eventsInsertMock = vi.fn().mockResolvedValue({ + data: mockGoogleEvent, + }); + calendarMock.calendar_v3.Calendar().events.insert = eventsInsertMock; + + const testCalEvent = { + type: "test-event-type", + uid: "cal-event-uid-456", + title: "Test Meeting Default Reminder", + startTime: "2024-06-15T10:00:00Z", + endTime: "2024-06-15T11:00:00Z", + organizer: { + id: 1, + name: "Test Organizer", + email: "organizer@example.com", + timeZone: "UTC", + language: { translate: (...args: any[]) => args[0], locale: "en" }, + }, + attendees: [], + calendarDescription: "Test meeting description", + destinationCalendar: [ + { + id: 1, + integration: "google_calendar", + externalId: "primary", + primaryEmail: null, + userId: mockCredential.userId, + eventTypeId: null, + credentialId: mockCredential.id, + delegationCredentialId: null, + domainWideDelegationCredentialId: null, + createdAt: new Date("2024-06-15T11:00:00Z"), + updatedAt: new Date("2024-06-15T11:00:00Z"), + customCalendarReminder: 10, + }, + ], + }; + + await calendarService.createEvent(testCalEvent, mockCredential.id); + + const insertCall = eventsInsertMock.mock.calls[0][0]; + + // When no custom reminder is configured (or fetch fails), should use default + expect(insertCall.requestBody.reminders).toEqual({ + useDefault: true, + }); + + log.info("createEvent with default reminders test passed"); + }); }); diff --git a/packages/platform/atoms/destination-calendar/DestinationCalendar.tsx b/packages/platform/atoms/destination-calendar/DestinationCalendar.tsx index e7a69f493ce7d8..6a53a3a3fc51db 100644 --- a/packages/platform/atoms/destination-calendar/DestinationCalendar.tsx +++ b/packages/platform/atoms/destination-calendar/DestinationCalendar.tsx @@ -4,6 +4,7 @@ import { Label } from "@calcom/ui/components/form"; import { cn } from "../src/lib/utils"; import type { DestinationCalendarProps } from "./DestinationCalendarSelector"; import { DestinationCalendarSelector } from "./DestinationCalendarSelector"; +import { DestinationReminderSelector } from "./DestinationReminderSelector"; type DestinationHeaderClassnames = { container?: string; @@ -16,10 +17,20 @@ export type DestinationCalendarClassNames = { header?: DestinationHeaderClassnames; }; -export const DestinationCalendarSettings = ( - props: DestinationCalendarProps & { classNames?: string; classNamesObject?: DestinationCalendarClassNames } -) => { +type DestinationCalendarSettingsProps = DestinationCalendarProps & { + classNames?: string; + classNamesObject?: DestinationCalendarClassNames; + onReminderChange?: ((value: number) => void) | null; + reminderValue?: number; + isReminderPending?: boolean; +}; + +export const DestinationCalendarSettings = (props: DestinationCalendarSettingsProps) => { const { t } = useLocale(); + const showReminderSelector = + props.onReminderChange !== null && + props.onReminderChange !== undefined && + props.destinationCalendar?.integration === "google_calendar"; return (
{t("add_events_to")}
+ {showReminderSelector && ( +
+ +

{t("default_reminder_description")}

+ +
+ )} diff --git a/packages/platform/atoms/destination-calendar/DestinationReminderSelector.tsx b/packages/platform/atoms/destination-calendar/DestinationReminderSelector.tsx new file mode 100644 index 00000000000000..e50bd6febb81f4 --- /dev/null +++ b/packages/platform/atoms/destination-calendar/DestinationReminderSelector.tsx @@ -0,0 +1,47 @@ +"use client"; + +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { Select } from "@calcom/ui/components/form"; + +const REMINDER_OPTIONS = [ + { value: 10, label: "remind_minutes_before" }, + { value: 30, label: "remind_minutes_before" }, + { value: 60, label: "remind_minutes_before" }, +]; + +interface DestinationReminderSelectorProps { + value: number; + onChange: (value: number) => void; + isPending?: boolean; +} + +export const DestinationReminderSelector = ({ + value, + onChange, + isPending, +}: DestinationReminderSelectorProps) => { + const { t } = useLocale(); + + const options = REMINDER_OPTIONS.map((opt) => ({ + value: opt.value, + label: t(opt.label, { count: opt.value }), + })); + + const selectedOption = options.find((opt) => opt.value === value) || options[0]; + + return ( + { hidePlaceholder onChange={mutation.mutate} onReminderChange={handleReminderChange} - reminderValue={(calendars.data.destinationCalendar.customCalendarReminder ?? 10) as ReminderMinutes} + reminderValue={calendars.data.destinationCalendar.customCalendarReminder} isReminderPending={reminderMutation.isPending} /> diff --git a/packages/prisma/migrations/20251220034814_add_custom_calendar_reminder/migration.sql b/packages/prisma/migrations/20251220034814_add_custom_calendar_reminder/migration.sql index 2ec84732eedc80..0d1b7624652129 100644 --- a/packages/prisma/migrations/20251220034814_add_custom_calendar_reminder/migration.sql +++ b/packages/prisma/migrations/20251220034814_add_custom_calendar_reminder/migration.sql @@ -1,2 +1,2 @@ -- AlterTable -ALTER TABLE "public"."DestinationCalendar" ADD COLUMN "customCalendarReminder" INTEGER NOT NULL DEFAULT 10; +ALTER TABLE "public"."DestinationCalendar" ADD COLUMN "customCalendarReminder" INTEGER; diff --git a/packages/prisma/migrations/20260106192502_allow_null_custom_calendar_reminder/migration.sql b/packages/prisma/migrations/20260106192502_allow_null_custom_calendar_reminder/migration.sql new file mode 100644 index 00000000000000..d372b0e50aee56 --- /dev/null +++ b/packages/prisma/migrations/20260106192502_allow_null_custom_calendar_reminder/migration.sql @@ -0,0 +1,4 @@ +-- AlterTable +-- Change customCalendarReminder from NOT NULL with default to nullable +ALTER TABLE "public"."DestinationCalendar" ALTER COLUMN "customCalendarReminder" DROP NOT NULL; +ALTER TABLE "public"."DestinationCalendar" ALTER COLUMN "customCalendarReminder" DROP DEFAULT; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 1104a1b9bbfa0b..ea0f7494304c9d 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -334,7 +334,7 @@ model DestinationCalendar { delegationCredentialId String? domainWideDelegation DomainWideDelegation? @relation(fields: [domainWideDelegationCredentialId], references: [id], onDelete: Cascade) domainWideDelegationCredentialId String? - customCalendarReminder Int @default(10) + customCalendarReminder Int? @@index([userId]) @@index([eventTypeId]) diff --git a/packages/trpc/server/routers/viewer/calendars/setDestinationReminder.handler.ts b/packages/trpc/server/routers/viewer/calendars/setDestinationReminder.handler.ts index ea70501eace5f9..8d610954cb10b8 100644 --- a/packages/trpc/server/routers/viewer/calendars/setDestinationReminder.handler.ts +++ b/packages/trpc/server/routers/viewer/calendars/setDestinationReminder.handler.ts @@ -1,4 +1,4 @@ -import { prisma } from "@calcom/prisma"; +import { DestinationCalendarRepository } from "@calcom/lib/server/repository/destinationCalendar"; import type { TrpcSessionUser } from "@calcom/trpc/server/types"; import type { TSetDestinationReminderInputSchema } from "./setDestinationReminder.schema"; @@ -15,15 +15,11 @@ export const setDestinationReminderHandler = async ({ ctx, input }: SetDestinati const { credentialId, integration, defaultReminder } = input; // Update the destination calendar's custom reminder setting - await prisma.destinationCalendar.updateMany({ - where: { - userId: user.id, - credentialId, - integration, - }, - data: { - customCalendarReminder: defaultReminder, - }, + await DestinationCalendarRepository.updateCustomReminder({ + userId: user.id, + credentialId, + integration, + customCalendarReminder: defaultReminder, }); return { success: true }; diff --git a/packages/trpc/server/routers/viewer/calendars/setDestinationReminder.schema.ts b/packages/trpc/server/routers/viewer/calendars/setDestinationReminder.schema.ts index 7a41e2f543a8f0..395c2ec094a005 100644 --- a/packages/trpc/server/routers/viewer/calendars/setDestinationReminder.schema.ts +++ b/packages/trpc/server/routers/viewer/calendars/setDestinationReminder.schema.ts @@ -2,12 +2,12 @@ import { z } from "zod"; export const VALID_REMINDER_MINUTES = [0, 10, 30, 60] as const; -const reminderSchema = z.union([z.literal(0), z.literal(10), z.literal(30), z.literal(60)]); +const reminderSchema = z.union([z.literal(0), z.literal(10), z.literal(30), z.literal(60), z.null()]); export const ZSetDestinationReminderInputSchema = z.object({ credentialId: z.number(), integration: z.string(), - defaultReminder: reminderSchema.default(10), + defaultReminder: reminderSchema.nullable(), }); export type TSetDestinationReminderInputSchema = z.infer; From 61a17621accce335b60841e720c906ef53d6ea55 Mon Sep 17 00:00:00 2001 From: Deepak Sharma Date: Wed, 7 Jan 2026 21:12:27 +0530 Subject: [PATCH 11/15] refactor: remove redundant migration file The original migration already creates customCalendarReminder as nullable, so this second migration to make it nullable is not needed. --- .../migration.sql | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 packages/prisma/migrations/20260106192502_allow_null_custom_calendar_reminder/migration.sql diff --git a/packages/prisma/migrations/20260106192502_allow_null_custom_calendar_reminder/migration.sql b/packages/prisma/migrations/20260106192502_allow_null_custom_calendar_reminder/migration.sql deleted file mode 100644 index d372b0e50aee56..00000000000000 --- a/packages/prisma/migrations/20260106192502_allow_null_custom_calendar_reminder/migration.sql +++ /dev/null @@ -1,4 +0,0 @@ --- AlterTable --- Change customCalendarReminder from NOT NULL with default to nullable -ALTER TABLE "public"."DestinationCalendar" ALTER COLUMN "customCalendarReminder" DROP NOT NULL; -ALTER TABLE "public"."DestinationCalendar" ALTER COLUMN "customCalendarReminder" DROP DEFAULT; From ab68922635cacfcc5fab75eb60d4b51895e946ff Mon Sep 17 00:00:00 2001 From: Deepak Sharma Date: Thu, 8 Jan 2026 19:17:52 +0530 Subject: [PATCH 12/15] fix: type error for customCalendarReminder in destination calendar settings --- .../wrappers/DestinationCalendarSettingsWebWrapper.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/platform/atoms/destination-calendar/wrappers/DestinationCalendarSettingsWebWrapper.tsx b/packages/platform/atoms/destination-calendar/wrappers/DestinationCalendarSettingsWebWrapper.tsx index 88008342ef8d0d..5ca9508e70c6b8 100644 --- a/packages/platform/atoms/destination-calendar/wrappers/DestinationCalendarSettingsWebWrapper.tsx +++ b/packages/platform/atoms/destination-calendar/wrappers/DestinationCalendarSettingsWebWrapper.tsx @@ -51,7 +51,7 @@ export const DestinationCalendarSettingsWebWrapper = () => { hidePlaceholder onChange={mutation.mutate} onReminderChange={handleReminderChange} - reminderValue={calendars.data.destinationCalendar.customCalendarReminder} + reminderValue={calendars.data.destinationCalendar.customCalendarReminder as ReminderMinutes} isReminderPending={reminderMutation.isPending} /> From 150faed11bffb74720ba0f86625e5834ef03ac47 Mon Sep 17 00:00:00 2001 From: Deepak Sharma Date: Thu, 8 Jan 2026 19:36:28 +0530 Subject: [PATCH 13/15] refactor: validate customCalendarReminder with Zod schema instead of type casting --- .../DestinationCalendarSettingsWebWrapper.tsx | 14 ++++++++++++-- .../calendars/setDestinationReminder.schema.ts | 2 +- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/platform/atoms/destination-calendar/wrappers/DestinationCalendarSettingsWebWrapper.tsx b/packages/platform/atoms/destination-calendar/wrappers/DestinationCalendarSettingsWebWrapper.tsx index 5ca9508e70c6b8..5865552df36794 100644 --- a/packages/platform/atoms/destination-calendar/wrappers/DestinationCalendarSettingsWebWrapper.tsx +++ b/packages/platform/atoms/destination-calendar/wrappers/DestinationCalendarSettingsWebWrapper.tsx @@ -1,6 +1,9 @@ import { useLocale } from "@calcom/lib/hooks/useLocale"; import { trpc } from "@calcom/trpc/react"; -import type { ReminderMinutes } from "@calcom/trpc/server/routers/viewer/calendars/setDestinationReminder.schema"; +import { + reminderSchema, + type ReminderMinutes, +} from "@calcom/trpc/server/routers/viewer/calendars/setDestinationReminder.schema"; import { showToast } from "@calcom/ui/components/toast"; import { AtomsWrapper } from "../../src/components/atoms-wrapper"; @@ -41,6 +44,13 @@ export const DestinationCalendarSettingsWebWrapper = () => { } }; + const validatedReminderValue = reminderSchema.safeParse( + calendars.data.destinationCalendar.customCalendarReminder + ); + const reminderValue: ReminderMinutes = validatedReminderValue.success + ? validatedReminderValue.data + : null; + return ( { hidePlaceholder onChange={mutation.mutate} onReminderChange={handleReminderChange} - reminderValue={calendars.data.destinationCalendar.customCalendarReminder as ReminderMinutes} + reminderValue={reminderValue} isReminderPending={reminderMutation.isPending} /> diff --git a/packages/trpc/server/routers/viewer/calendars/setDestinationReminder.schema.ts b/packages/trpc/server/routers/viewer/calendars/setDestinationReminder.schema.ts index 395c2ec094a005..dbcc70da40513a 100644 --- a/packages/trpc/server/routers/viewer/calendars/setDestinationReminder.schema.ts +++ b/packages/trpc/server/routers/viewer/calendars/setDestinationReminder.schema.ts @@ -2,7 +2,7 @@ import { z } from "zod"; export const VALID_REMINDER_MINUTES = [0, 10, 30, 60] as const; -const reminderSchema = z.union([z.literal(0), z.literal(10), z.literal(30), z.literal(60), z.null()]); +export const reminderSchema = z.union([z.literal(0), z.literal(10), z.literal(30), z.literal(60), z.null()]); export const ZSetDestinationReminderInputSchema = z.object({ credentialId: z.number(), From 9337b22929e234a1a6f8dd1391369efae977adc7 Mon Sep 17 00:00:00 2001 From: Deepak Sharma Date: Fri, 9 Jan 2026 21:31:32 +0530 Subject: [PATCH 14/15] refactor: use instance methods in DestinationCalendarRepository Changed from static to instance methods with getInstance() pattern as requested in review. --- .../googlecalendar/lib/CalendarService.ts | 2 +- .../server/repository/destinationCalendar.ts | 22 ++++++++++++++----- .../setDestinationReminder.handler.ts | 2 +- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/packages/app-store/googlecalendar/lib/CalendarService.ts b/packages/app-store/googlecalendar/lib/CalendarService.ts index b7ef34c684afbd..d829f732c53767 100644 --- a/packages/app-store/googlecalendar/lib/CalendarService.ts +++ b/packages/app-store/googlecalendar/lib/CalendarService.ts @@ -59,7 +59,7 @@ export default class GoogleCalendarService implements Calendar { private async getReminderDuration(credentialId: number): Promise { try { - return await DestinationCalendarRepository.getCustomReminderByCredentialId(credentialId); + return await DestinationCalendarRepository.getInstance().getCustomReminderByCredentialId(credentialId); } catch (error) { this.log.warn("Failed to fetch custom calendar reminder", safeStringify(error)); return null; diff --git a/packages/lib/server/repository/destinationCalendar.ts b/packages/lib/server/repository/destinationCalendar.ts index 8da104547002e8..eb5308438921a6 100644 --- a/packages/lib/server/repository/destinationCalendar.ts +++ b/packages/lib/server/repository/destinationCalendar.ts @@ -1,15 +1,27 @@ -import { prisma } from "@calcom/prisma"; +import type { PrismaClient } from "@calcom/prisma"; +import { prisma as defaultPrisma } from "@calcom/prisma"; export class DestinationCalendarRepository { - static async getCustomReminderByCredentialId(credentialId: number): Promise { - const destinationCalendar = await prisma.destinationCalendar.findFirst({ + constructor(private prisma: PrismaClient = defaultPrisma) {} + + private static _instance: DestinationCalendarRepository; + + static getInstance(): DestinationCalendarRepository { + if (!DestinationCalendarRepository._instance) { + DestinationCalendarRepository._instance = new DestinationCalendarRepository(); + } + return DestinationCalendarRepository._instance; + } + + async getCustomReminderByCredentialId(credentialId: number): Promise { + const destinationCalendar = await this.prisma.destinationCalendar.findFirst({ where: { credentialId }, select: { customCalendarReminder: true }, }); return destinationCalendar?.customCalendarReminder ?? null; } - static async updateCustomReminder({ + async updateCustomReminder({ userId, credentialId, integration, @@ -20,7 +32,7 @@ export class DestinationCalendarRepository { integration: string; customCalendarReminder: number | null; }) { - return await prisma.destinationCalendar.updateMany({ + return await this.prisma.destinationCalendar.updateMany({ where: { userId, credentialId, diff --git a/packages/trpc/server/routers/viewer/calendars/setDestinationReminder.handler.ts b/packages/trpc/server/routers/viewer/calendars/setDestinationReminder.handler.ts index 8d610954cb10b8..d91e19ff12f76c 100644 --- a/packages/trpc/server/routers/viewer/calendars/setDestinationReminder.handler.ts +++ b/packages/trpc/server/routers/viewer/calendars/setDestinationReminder.handler.ts @@ -15,7 +15,7 @@ export const setDestinationReminderHandler = async ({ ctx, input }: SetDestinati const { credentialId, integration, defaultReminder } = input; // Update the destination calendar's custom reminder setting - await DestinationCalendarRepository.updateCustomReminder({ + await DestinationCalendarRepository.getInstance().updateCustomReminder({ userId: user.id, credentialId, integration, From b6a0949e5a8cc5a706b766b847dec11f10008aa0 Mon Sep 17 00:00:00 2001 From: Deepak Sharma Date: Mon, 12 Jan 2026 20:41:48 +0530 Subject: [PATCH 15/15] refactor: replace singleton with DI for DestinationCalendarRepository - Remove getInstance() singleton pattern - Add DI tokens, module, and container - Update GoogleCalendarService and tRPC handler to use DI --- .../googlecalendar/lib/CalendarService.ts | 5 +++-- .../di/containers/DestinationCalendar.ts | 11 ++++++++++ .../di/modules/DestinationCalendar.ts | 21 +++++++++++++++++++ packages/features/di/tokens.ts | 2 ++ .../server/repository/destinationCalendar.ts | 12 +---------- .../setDestinationReminder.handler.ts | 6 ++++-- 6 files changed, 42 insertions(+), 15 deletions(-) create mode 100644 packages/features/di/containers/DestinationCalendar.ts create mode 100644 packages/features/di/modules/DestinationCalendar.ts diff --git a/packages/app-store/googlecalendar/lib/CalendarService.ts b/packages/app-store/googlecalendar/lib/CalendarService.ts index 6ac87dbdb55c2f..0944313648a053 100644 --- a/packages/app-store/googlecalendar/lib/CalendarService.ts +++ b/packages/app-store/googlecalendar/lib/CalendarService.ts @@ -7,7 +7,7 @@ import { getLocation, getRichDescription } from "@calcom/lib/CalEventParser"; import { ORGANIZER_EMAIL_EXEMPT_DOMAINS } from "@calcom/lib/constants"; import logger from "@calcom/lib/logger"; import { safeStringify } from "@calcom/lib/safeStringify"; -import { DestinationCalendarRepository } from "@calcom/lib/server/repository/destinationCalendar"; +import { getDestinationCalendarRepository } from "@calcom/features/di/containers/DestinationCalendar"; import { SelectedCalendarRepository } from "@calcom/lib/server/repository/selectedCalendar"; import type { Prisma } from "@calcom/prisma/client"; import type { @@ -51,7 +51,8 @@ export default class GoogleCalendarService implements Calendar { private async getReminderDuration(credentialId: number): Promise { try { - return await DestinationCalendarRepository.getInstance().getCustomReminderByCredentialId(credentialId); + const destinationCalendarRepository = getDestinationCalendarRepository(); + return await destinationCalendarRepository.getCustomReminderByCredentialId(credentialId); } catch (error) { this.log.warn("Failed to fetch custom calendar reminder", safeStringify(error)); return null; diff --git a/packages/features/di/containers/DestinationCalendar.ts b/packages/features/di/containers/DestinationCalendar.ts new file mode 100644 index 00000000000000..b4681825966620 --- /dev/null +++ b/packages/features/di/containers/DestinationCalendar.ts @@ -0,0 +1,11 @@ +import type { DestinationCalendarRepository } from "@calcom/lib/server/repository/destinationCalendar"; + +import { createContainer } from "../di"; +import { moduleLoader as destinationCalendarRepositoryModuleLoader } from "../modules/DestinationCalendar"; + +const container = createContainer(); + +export function getDestinationCalendarRepository() { + destinationCalendarRepositoryModuleLoader.loadModule(container); + return container.get(destinationCalendarRepositoryModuleLoader.token); +} diff --git a/packages/features/di/modules/DestinationCalendar.ts b/packages/features/di/modules/DestinationCalendar.ts new file mode 100644 index 00000000000000..5a69df86848b3e --- /dev/null +++ b/packages/features/di/modules/DestinationCalendar.ts @@ -0,0 +1,21 @@ +import { DestinationCalendarRepository } from "@calcom/lib/server/repository/destinationCalendar"; +import { DI_TOKENS } from "@calcom/features/di/tokens"; +import { moduleLoader as prismaModuleLoader } from "@calcom/features/di/modules/Prisma"; + +import { createModule, bindModuleToClassOnToken, type ModuleLoader } from "../di"; + +export const destinationCalendarRepositoryModule = createModule(); +const token = DI_TOKENS.DESTINATION_CALENDAR_REPOSITORY; +const moduleToken = DI_TOKENS.DESTINATION_CALENDAR_REPOSITORY_MODULE; +const loadModule = bindModuleToClassOnToken({ + module: destinationCalendarRepositoryModule, + moduleToken, + token, + classs: DestinationCalendarRepository, + dep: prismaModuleLoader, +}); + +export const moduleLoader: ModuleLoader = { + token, + loadModule, +}; diff --git a/packages/features/di/tokens.ts b/packages/features/di/tokens.ts index 0e41688270952b..b9d8bb1fd9121c 100644 --- a/packages/features/di/tokens.ts +++ b/packages/features/di/tokens.ts @@ -66,6 +66,8 @@ export const DI_TOKENS = { ASSIGNMENT_REASON_REPOSITORY_MODULE: Symbol("AssignmentReasonRepositoryModule"), CREDENTIAL_REPOSITORY: Symbol("CredentialRepository"), CREDENTIAL_REPOSITORY_MODULE: Symbol("CredentialRepositoryModule"), + DESTINATION_CALENDAR_REPOSITORY: Symbol("DestinationCalendarRepository"), + DESTINATION_CALENDAR_REPOSITORY_MODULE: Symbol("DestinationCalendarRepositoryModule"), MANAGED_EVENT_REASSIGNMENT_SERVICE: Symbol("ManagedEventReassignmentService"), MANAGED_EVENT_REASSIGNMENT_SERVICE_MODULE: Symbol("ManagedEventReassignmentServiceModule"), ORG_MEMBERSHIP_LOOKUP: Symbol("OrgMembershipLookup"), diff --git a/packages/lib/server/repository/destinationCalendar.ts b/packages/lib/server/repository/destinationCalendar.ts index eb5308438921a6..6f64e2a89edadb 100644 --- a/packages/lib/server/repository/destinationCalendar.ts +++ b/packages/lib/server/repository/destinationCalendar.ts @@ -1,17 +1,7 @@ import type { PrismaClient } from "@calcom/prisma"; -import { prisma as defaultPrisma } from "@calcom/prisma"; export class DestinationCalendarRepository { - constructor(private prisma: PrismaClient = defaultPrisma) {} - - private static _instance: DestinationCalendarRepository; - - static getInstance(): DestinationCalendarRepository { - if (!DestinationCalendarRepository._instance) { - DestinationCalendarRepository._instance = new DestinationCalendarRepository(); - } - return DestinationCalendarRepository._instance; - } + constructor(private prisma: PrismaClient) {} async getCustomReminderByCredentialId(credentialId: number): Promise { const destinationCalendar = await this.prisma.destinationCalendar.findFirst({ diff --git a/packages/trpc/server/routers/viewer/calendars/setDestinationReminder.handler.ts b/packages/trpc/server/routers/viewer/calendars/setDestinationReminder.handler.ts index d91e19ff12f76c..eced57f0c0df0c 100644 --- a/packages/trpc/server/routers/viewer/calendars/setDestinationReminder.handler.ts +++ b/packages/trpc/server/routers/viewer/calendars/setDestinationReminder.handler.ts @@ -1,4 +1,4 @@ -import { DestinationCalendarRepository } from "@calcom/lib/server/repository/destinationCalendar"; +import { getDestinationCalendarRepository } from "@calcom/features/di/containers/DestinationCalendar"; import type { TrpcSessionUser } from "@calcom/trpc/server/types"; import type { TSetDestinationReminderInputSchema } from "./setDestinationReminder.schema"; @@ -14,8 +14,10 @@ export const setDestinationReminderHandler = async ({ ctx, input }: SetDestinati const { user } = ctx; const { credentialId, integration, defaultReminder } = input; + const destinationCalendarRepository = getDestinationCalendarRepository(); + // Update the destination calendar's custom reminder setting - await DestinationCalendarRepository.getInstance().updateCustomReminder({ + await destinationCalendarRepository.updateCustomReminder({ userId: user.id, credentialId, integration,