From 9909ac53fc60b4f40f7cc778ed4f344a11c9f30e Mon Sep 17 00:00:00 2001 From: Dhairyashil Shinde <93669429+dhairyashiil@users.noreply.github.com> Date: Tue, 23 Dec 2025 03:41:08 +0530 Subject: [PATCH] fix: use app name instead of app slug in email (#25285) * fix: google calender name in email * fix: google calender name in email * cleaner code * test: add test for human-readable app name in appsStatus --- .../utils/bookingScenario/bookingScenario.ts | 2 +- .../features/CalendarEventBuilder.test.ts | 106 ++++++++++++++++++ packages/features/CalendarEventBuilder.ts | 10 +- .../features/bookings/lib/EventManager.ts | 6 +- .../features/calendars/lib/CalendarManager.ts | 4 +- .../features/conferencing/lib/videoClient.ts | 20 ++-- packages/types/Credential.d.ts | 1 + 7 files changed, 132 insertions(+), 17 deletions(-) diff --git a/apps/web/test/utils/bookingScenario/bookingScenario.ts b/apps/web/test/utils/bookingScenario/bookingScenario.ts index f7deeb3b24..db1aefc837 100644 --- a/apps/web/test/utils/bookingScenario/bookingScenario.ts +++ b/apps/web/test/utils/bookingScenario/bookingScenario.ts @@ -2368,7 +2368,7 @@ const getMockAppStatus = ({ } const foundApp = foundEntry[1]; return { - appName: overrideName ?? foundApp.slug, + appName: overrideName ?? foundApp.name, type: foundApp.type, failures, success, diff --git a/packages/features/CalendarEventBuilder.test.ts b/packages/features/CalendarEventBuilder.test.ts index 4bede68260..9fe0f8a4ab 100644 --- a/packages/features/CalendarEventBuilder.test.ts +++ b/packages/features/CalendarEventBuilder.test.ts @@ -1906,5 +1906,111 @@ describe("CalendarEventBuilder", () => { expect(builtFromBooking.bookerUrl).toBe("https://cal.com"); }); + + it("should resolve app type to human-readable app name in appsStatus", async () => { + const mockBooking = { + uid: "booking-app-name-test", + metadata: null, + title: "App Name Test Event", + startTime: new Date(mockStartTime), + endTime: new Date(mockEndTime), + description: null, + location: "integrations:google:meet", + responses: null, + customInputs: null, + iCalUID: null, + iCalSequence: 0, + oneTimePassword: null, + attendees: [ + { + name: "Test Attendee", + email: "attendee@example.com", + timeZone: "UTC", + locale: "en", + phoneNumber: null, + bookingSeat: null, + }, + ], + user: { + id: 1, + name: "Test Host", + email: "host@example.com", + username: "testhost", + timeZone: "UTC", + locale: "en", + timeFormat: 24, + destinationCalendar: null, + profiles: [], + }, + destinationCalendar: null, + eventType: { + id: 1, + title: "Test Event Type", + slug: "test-event", + description: null, + hideCalendarNotes: false, + hideCalendarEventDetails: false, + hideOrganizerEmail: false, + schedulingType: null, + seatsPerTimeSlot: null, + seatsShowAttendees: false, + seatsShowAvailabilityCount: false, + customReplyToEmail: null, + disableRescheduling: false, + disableCancelling: false, + requiresConfirmation: false, + recurringEvent: null, + bookingFields: [], + metadata: null, + eventName: null, + team: null, + users: [], + hosts: [], + workflows: [], + }, + references: [ + { + type: "google_calendar", + uid: "google-cal-uid-123", + meetingId: null, + meetingPassword: null, + meetingUrl: null, + }, + { + type: "google_video", + uid: "google-meet-uid-456", + meetingId: "meet-123", + meetingPassword: null, + meetingUrl: "https://meet.google.com/abc-defg-hij", + }, + ], + seatsReferences: [], + } satisfies BookingForCalEventBuilder; + + const eventFromBooking = await CalendarEventBuilder.fromBooking(mockBooking); + const builtEvent = eventFromBooking.build(); + + expect(builtEvent).not.toBeNull(); + if (builtEvent) { + expect(builtEvent.appsStatus).toBeDefined(); + expect(builtEvent.appsStatus).toHaveLength(2); + + // Verify Google Calendar uses human-readable name, not the type slug + const googleCalendarStatus = builtEvent.appsStatus?.find((app) => app.type === "google_calendar"); + expect(googleCalendarStatus).toBeDefined(); + expect(googleCalendarStatus?.appName).toBe("Google Calendar"); + // Should NOT be the raw type like "google_calendar" or "google-calendar" + expect(googleCalendarStatus?.appName).not.toBe("google_calendar"); + expect(googleCalendarStatus?.appName).not.toBe("google-calendar"); + + // Verify Google Meet uses human-readable name + const googleMeetStatus = builtEvent.appsStatus?.find((app) => app.type === "google_video"); + expect(googleMeetStatus).toBeDefined(); + expect(googleMeetStatus?.appName).toBe("Google Meet"); + // Should NOT be the raw type + expect(googleMeetStatus?.appName).not.toBe("google_video"); + expect(googleMeetStatus?.appName).not.toBe("google-video"); + } + }); }); }); diff --git a/packages/features/CalendarEventBuilder.ts b/packages/features/CalendarEventBuilder.ts index 492b1aed7d..abb168236e 100644 --- a/packages/features/CalendarEventBuilder.ts +++ b/packages/features/CalendarEventBuilder.ts @@ -1,5 +1,6 @@ import type { TFunction } from "i18next"; +import { ALL_APPS } from "@calcom/app-store/utils"; import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses"; import type { BookingRepository } from "@calcom/features/bookings/repositories/BookingRepository"; import { getBookerBaseUrl } from "@calcom/features/ee/organizations/lib/getBookerUrlServer"; @@ -12,6 +13,8 @@ import { bookingResponses as bookingResponsesSchema } from "@calcom/prisma/zod-u import type { CalendarEvent, Person, CalEventResponses, AppsStatus } from "@calcom/types/Calendar"; import type { VideoCallData } from "@calcom/types/VideoApiAdapter"; +const APP_TYPE_TO_NAME_MAP = new Map(ALL_APPS.map((app) => [app.type, app.name])); + export type BookingForCalEventBuilder = NonNullable< Awaited> >; @@ -211,8 +214,9 @@ export class CalendarEventBuilder { references .filter((r) => r && r.type) .forEach((ref) => { + const appName = APP_TYPE_TO_NAME_MAP.get(ref.type) || ref.type.replace("_", "-"); appsStatus.push({ - appName: ref.type.replace("_", "-"), + appName, type: ref.type, success: ref.uid ? 1 : 0, failures: ref.uid ? 0 : 1, @@ -234,9 +238,7 @@ export class CalendarEventBuilder { bookingAttendees.some((attendee) => attendee.email === host.user.email) ); - const hostsWithoutOrganizerData = hostsToInclude.filter( - (host) => host.user.email !== user.email - ); + const hostsWithoutOrganizerData = hostsToInclude.filter((host) => host.user.email !== user.email); const hostsWithoutOrganizer = await Promise.all( hostsWithoutOrganizerData.map((host) => _buildPersonFromUser(host.user)) diff --git a/packages/features/bookings/lib/EventManager.ts b/packages/features/bookings/lib/EventManager.ts index 4fcffc7e7e..11b06e8d24 100644 --- a/packages/features/bookings/lib/EventManager.ts +++ b/packages/features/bookings/lib/EventManager.ts @@ -1174,7 +1174,7 @@ export default class EventManager { if (!calendarReference) { return { - appName: cred.appId || "", + appName: cred.appName || "", type: cred.type, success: false, uid: "", @@ -1267,7 +1267,7 @@ export default class EventManager { if (createdEvent) { createdEvents.push({ type: credential.type, - appName: credential.appId || "", + appName: credential.appName || "", uid, success, createdEvent: { @@ -1300,7 +1300,7 @@ export default class EventManager { updatedEvents.push({ type: credential.type, - appName: credential.appId || "", + appName: credential.appName || "", success, uid: updatedEvent?.id || "", originalEvent: event, diff --git a/packages/features/calendars/lib/CalendarManager.ts b/packages/features/calendars/lib/CalendarManager.ts index 0ef5fc1ffe..10b47e92f6 100644 --- a/packages/features/calendars/lib/CalendarManager.ts +++ b/packages/features/calendars/lib/CalendarManager.ts @@ -365,7 +365,7 @@ export const createEvent = async ( }) ); return { - appName: credential.appId || "", + appName: credential.appName || "", type: credential.type, success, uid, @@ -450,7 +450,7 @@ export const updateEvent = async ( } return { - appName: credential.appId || "", + appName: credential.appName || "", type: credential.type, success, uid, diff --git a/packages/features/conferencing/lib/videoClient.ts b/packages/features/conferencing/lib/videoClient.ts index 6d659a69dc..470b4a7f06 100644 --- a/packages/features/conferencing/lib/videoClient.ts +++ b/packages/features/conferencing/lib/videoClient.ts @@ -13,7 +13,7 @@ import { safeStringify } from "@calcom/lib/safeStringify"; import { prisma } from "@calcom/prisma"; import type { GetRecordingsResponseSchema, GetAccessLinkResponseSchema } from "@calcom/prisma/zod-utils"; import type { CalendarEvent, EventBusyDate } from "@calcom/types/Calendar"; -import type { CredentialPayload } from "@calcom/types/Credential"; +import type { CredentialPayload, CredentialForCalendarService } from "@calcom/types/Credential"; import type { EventResult, PartialReference } from "@calcom/types/EventManager"; import type { VideoCallData } from "@calcom/types/VideoApiAdapter"; @@ -26,7 +26,10 @@ const getBusyVideoTimes = async (withCredentials: CredentialPayload[]) => results.reduce((acc, availability) => acc.concat(availability), [] as (EventBusyDate | undefined)[]) ); -const createMeeting = async (credential: CredentialPayload, calEvent: CalendarEvent) => { +const createMeeting = async ( + credential: CredentialPayload | CredentialForCalendarService, + calEvent: CalendarEvent +) => { const uid: string = getUid(calEvent); log.debug( "createMeeting", @@ -54,7 +57,7 @@ const createMeeting = async (credential: CredentialPayload, calEvent: CalendarEv createdEvent: VideoCallData | undefined; credentialId: number; } = { - appName: credential.appId || "", + appName: credential.appName || credential.appId || "", type: credential.type, uid, originalEvent: calEvent, @@ -100,7 +103,7 @@ const createMeeting = async (credential: CredentialPayload, calEvent: CalendarEv }; const updateMeeting = async ( - credential: CredentialPayload, + credential: CredentialPayload | CredentialForCalendarService, calEvent: CalendarEvent, bookingRef: PartialReference | null ): Promise> => { @@ -123,7 +126,7 @@ const updateMeeting = async ( safeStringify({ bookingRef, canCallUpdateMeeting, calEvent, credential }) ); return { - appName: credential.appId || "", + appName: credential.appName || credential.appId || "", type: credential.type, success, uid, @@ -132,7 +135,7 @@ const updateMeeting = async ( } return { - appName: credential.appId || "", + appName: credential.appName || credential.appId || "", type: credential.type, success, uid, @@ -141,7 +144,10 @@ const updateMeeting = async ( }; }; -const deleteMeeting = async (credential: CredentialPayload | null, uid: string): Promise => { +const deleteMeeting = async ( + credential: CredentialPayload | CredentialForCalendarService | null, + uid: string +): Promise => { if (credential) { const videoAdapter = (await getVideoAdapters([credential]))[0]; log.debug( diff --git a/packages/types/Credential.d.ts b/packages/types/Credential.d.ts index 2abf51ec06..a6c764fc9c 100644 --- a/packages/types/Credential.d.ts +++ b/packages/types/Credential.d.ts @@ -9,6 +9,7 @@ export type CredentialPayload = Prisma.CredentialGetPayload<{ select: typeof import("@calcom/prisma/selects/credential").credentialForCalendarServiceSelect; }> & { delegatedToId?: string | null; + appName?: string; }; export type CredentialForCalendarService = CredentialPayload & {