diff --git a/apps/web/modules/bookings/views/bookings-single-view.tsx b/apps/web/modules/bookings/views/bookings-single-view.tsx index 26fd0a3d4d..d623b36fd0 100644 --- a/apps/web/modules/bookings/views/bookings-single-view.tsx +++ b/apps/web/modules/bookings/views/bookings-single-view.tsx @@ -42,7 +42,9 @@ import { getIs24hClockFromLocalStorage, isBrowserLocale24h } from "@calcom/lib/t import { getTimeShiftFlags, getFirstShiftFlags } from "@calcom/lib/timeShift"; import { CURRENT_TIMEZONE } from "@calcom/lib/timezoneConstants"; import { localStorage } from "@calcom/lib/webstorage"; -import { BookingStatus, SchedulingType } from "@calcom/prisma/enums"; +import { AssignmentReasonEnum, BookingStatus, SchedulingType } from "@calcom/prisma/enums"; + +import assignmentReasonBadgeTitleMap from "@calcom/web/lib/booking/assignmentReasonBadgeTitleMap"; import { bookingMetadataSchema } from "@calcom/prisma/zod-utils"; import { trpc } from "@calcom/trpc/react"; import { Alert } from "@calcom/ui/components/alert"; @@ -764,6 +766,28 @@ export default function Success(props: PageProps) { )} + {canViewHiddenData && + bookingInfo.assignmentReason && + bookingInfo.assignmentReason.length > 0 && + bookingInfo.assignmentReason[0].reasonEnum && ( + <> +
{t("assignment_reason")}
+
+ + {t( + assignmentReasonBadgeTitleMap( + bookingInfo.assignmentReason[0].reasonEnum as AssignmentReasonEnum + ) + )} + + {bookingInfo.assignmentReason[0].reasonString && ( +

+ {bookingInfo.assignmentReason[0].reasonString} +

+ )} +
+ + )}
{eventType.bookingFields.map((field) => { diff --git a/packages/emails/src/templates/BaseScheduledEmail.tsx b/packages/emails/src/templates/BaseScheduledEmail.tsx index 585702c87b..306481e4a1 100644 --- a/packages/emails/src/templates/BaseScheduledEmail.tsx +++ b/packages/emails/src/templates/BaseScheduledEmail.tsx @@ -129,6 +129,13 @@ export const BaseScheduledEmail = ( {props.includeAppsStatus && } + {props.isOrganizer && props.calEvent.assignmentReason && ( + + )} {props.calEvent.paymentInfo?.amount && ( { } }); + it("should create an event with assignment reason", () => { + const event = new CalendarEventBuilder() + .withBasicDetails({ + bookerUrl: "https://cal.com/user/test-slug", + title: "Test Event", + startTime: mockStartTime, + endTime: mockEndTime, + }) + .withEventType({ + slug: "test-slug", + id: 123, + }) + .withAssignmentReason({ + category: "routed", + details: "Language: English, Region: US", + }) + .build(); + + expect(event).not.toBeNull(); + if (event) { + expect(event.assignmentReason).toEqual({ + category: "routed", + details: "Language: English, Region: US", + }); + } + }); + + it("should create an event with assignment reason without details", () => { + const event = new CalendarEventBuilder() + .withBasicDetails({ + bookerUrl: "https://cal.com/user/test-slug", + title: "Test Event", + startTime: mockStartTime, + endTime: mockEndTime, + }) + .withEventType({ + slug: "test-slug", + id: 123, + }) + .withAssignmentReason({ + category: "reassigned", + details: null, + }) + .build(); + + expect(event).not.toBeNull(); + if (event) { + expect(event.assignmentReason).toEqual({ + category: "reassigned", + details: null, + }); + } + }); + + it("should create an event with null assignment reason", () => { + const event = new CalendarEventBuilder() + .withBasicDetails({ + bookerUrl: "https://cal.com/user/test-slug", + title: "Test Event", + startTime: mockStartTime, + endTime: mockEndTime, + }) + .withEventType({ + slug: "test-slug", + id: 123, + }) + .withAssignmentReason(null) + .build(); + + expect(event).not.toBeNull(); + if (event) { + expect(event.assignmentReason).toBeNull(); + } + }); + it("should create a complete calendar event with all properties", () => { const event = new CalendarEventBuilder() .withBasicDetails({ diff --git a/packages/features/CalendarEventBuilder.ts b/packages/features/CalendarEventBuilder.ts index abb168236e..cdf8269926 100644 --- a/packages/features/CalendarEventBuilder.ts +++ b/packages/features/CalendarEventBuilder.ts @@ -1,6 +1,7 @@ import type { TFunction } from "i18next"; import { ALL_APPS } from "@calcom/app-store/utils"; +import { getAssignmentReasonCategory } from "@calcom/features/bookings/lib/getAssignmentReasonCategory"; 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"; @@ -85,21 +86,22 @@ export class CalendarEventBuilder { if (!eventType) throw new Error(`Booking ${uid} is missing eventType — it may have been deleted.`); const builder = new CalendarEventBuilder(); - const { - description, - attendees, - references, - title, - startTime, - endTime, - location, - responses, - customInputs, - iCalUID, - iCalSequence, - oneTimePassword, - seatsReferences, - } = booking; + const { + description, + attendees, + references, + title, + startTime, + endTime, + location, + responses, + customInputs, + iCalUID, + iCalSequence, + oneTimePassword, + seatsReferences, + assignmentReason, + } = booking; const { conferenceCredentialId, @@ -185,10 +187,18 @@ export class CalendarEventBuilder { platformCancelUrl, platformBookingUrl, }) - .withRecurring(recurring) - .withUid(uid) - .withOneTimePassword(oneTimePassword) - .withOrganization(organizationId); + .withRecurring(recurring) + .withUid(uid) + .withOneTimePassword(oneTimePassword) + .withOrganization(organizationId) + .withAssignmentReason( + assignmentReason?.[0]?.reasonEnum + ? { + category: getAssignmentReasonCategory(assignmentReason[0].reasonEnum), + details: assignmentReason[0].reasonString ?? null, + } + : null + ); // Seats if (seatsReferences?.length && bookingResponses) { @@ -523,15 +533,23 @@ export class CalendarEventBuilder { return this; } - withHashedLink(hashedLink?: string | null) { - this.event = { - ...this.event, - hashedLink, - }; - return this; - } + withHashedLink(hashedLink?: string | null) { + this.event = { + ...this.event, + hashedLink, + }; + return this; + } - build(): CalendarEvent | null { + withAssignmentReason(assignmentReason?: { category: string; details?: string | null } | null) { + this.event = { + ...this.event, + assignmentReason, + }; + return this; + } + + build(): CalendarEvent | null { // Validate required fields if ( !this.event.startTime || diff --git a/packages/features/bookings/lib/getAssignmentReasonCategory.test.ts b/packages/features/bookings/lib/getAssignmentReasonCategory.test.ts new file mode 100644 index 0000000000..423de32c05 --- /dev/null +++ b/packages/features/bookings/lib/getAssignmentReasonCategory.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it } from "vitest"; + +import { AssignmentReasonEnum } from "@calcom/prisma/enums"; + +import { getAssignmentReasonCategory } from "./getAssignmentReasonCategory"; + +describe("getAssignmentReasonCategory", () => { + it("returns 'routed' for ROUTING_FORM_ROUTING", () => { + expect(getAssignmentReasonCategory(AssignmentReasonEnum.ROUTING_FORM_ROUTING)).toBe("routed"); + }); + + it("returns 'routed' for ROUTING_FORM_ROUTING_FALLBACK", () => { + expect(getAssignmentReasonCategory(AssignmentReasonEnum.ROUTING_FORM_ROUTING_FALLBACK)).toBe("routed"); + }); + + it("returns 'reassigned' for REASSIGNED", () => { + expect(getAssignmentReasonCategory(AssignmentReasonEnum.REASSIGNED)).toBe("reassigned"); + }); + + it("returns 'reassigned' for RR_REASSIGNED", () => { + expect(getAssignmentReasonCategory(AssignmentReasonEnum.RR_REASSIGNED)).toBe("reassigned"); + }); + + it("returns 'rerouted' for REROUTED", () => { + expect(getAssignmentReasonCategory(AssignmentReasonEnum.REROUTED)).toBe("rerouted"); + }); + + it("returns 'salesforce_assigned' for SALESFORCE_ASSIGNMENT", () => { + expect(getAssignmentReasonCategory(AssignmentReasonEnum.SALESFORCE_ASSIGNMENT)).toBe("salesforce_assigned"); + }); + + it("returns 'routed' for unknown enum values (default case)", () => { + const unknownValue = "UNKNOWN_VALUE" as AssignmentReasonEnum; + expect(getAssignmentReasonCategory(unknownValue)).toBe("routed"); + }); +}); diff --git a/packages/features/bookings/lib/getAssignmentReasonCategory.ts b/packages/features/bookings/lib/getAssignmentReasonCategory.ts new file mode 100644 index 0000000000..86dc6a8723 --- /dev/null +++ b/packages/features/bookings/lib/getAssignmentReasonCategory.ts @@ -0,0 +1,23 @@ +import { AssignmentReasonEnum } from "@calcom/prisma/enums"; + +/** + * Maps an AssignmentReasonEnum to a translation key for the category label. + * Returns translation keys like "routed", "reassigned", "rerouted", "salesforce_assigned" + * that can be used with the t() function to get translated labels. + */ +export const getAssignmentReasonCategory = (assignmentReason: AssignmentReasonEnum): string => { + switch (assignmentReason) { + case AssignmentReasonEnum.ROUTING_FORM_ROUTING: + case AssignmentReasonEnum.ROUTING_FORM_ROUTING_FALLBACK: + return "routed"; + case AssignmentReasonEnum.REASSIGNED: + case AssignmentReasonEnum.RR_REASSIGNED: + return "reassigned"; + case AssignmentReasonEnum.REROUTED: + return "rerouted"; + case AssignmentReasonEnum.SALESFORCE_ASSIGNMENT: + return "salesforce_assigned"; + default: + return "routed"; + } +}; diff --git a/packages/features/bookings/lib/getUserBooking.ts b/packages/features/bookings/lib/getUserBooking.ts index a337df39b4..e1c53dc67e 100644 --- a/packages/features/bookings/lib/getUserBooking.ts +++ b/packages/features/bookings/lib/getUserBooking.ts @@ -71,6 +71,16 @@ const getUserBooking = async (uid: string) => { utm_content: true, }, }, + assignmentReason: { + select: { + reasonEnum: true, + reasonString: true, + }, + orderBy: { + createdAt: "desc", + }, + take: 1, + }, }, }); diff --git a/packages/features/bookings/lib/service/RegularBookingService.ts b/packages/features/bookings/lib/service/RegularBookingService.ts index d997069c78..f6506bbd0e 100644 --- a/packages/features/bookings/lib/service/RegularBookingService.ts +++ b/packages/features/bookings/lib/service/RegularBookingService.ts @@ -28,6 +28,7 @@ import { scheduleMandatoryReminder } from "@calcom/ee/workflows/lib/reminders/sc import getICalUID from "@calcom/emails/lib/getICalUID"; import { CalendarEventBuilder } from "@calcom/features/CalendarEventBuilder"; import { verifyCodeUnAuthenticated } from "@calcom/features/auth/lib/verifyCodeUnAuthenticated"; +import { getAssignmentReasonCategory } from "@calcom/features/bookings/lib/getAssignmentReasonCategory"; import EventManager, { placeholderCreatedEvent } from "@calcom/features/bookings/lib/EventManager"; import type { BookingDataSchemaGetter } from "@calcom/features/bookings/lib/dto/types"; import type { @@ -1992,6 +1993,20 @@ async function handler( evt = updatedEvtWithPassword; + // Add assignment reason to evt for emails + if (assignmentReason) { + const updatedEvtWithAssignmentReason = CalendarEventBuilder.fromEvent(evt) + ?.withAssignmentReason({ + category: getAssignmentReasonCategory(assignmentReason.reasonEnum), + details: assignmentReason.reasonString ?? null, + }) + .build(); + + if (updatedEvtWithAssignmentReason) { + evt = updatedEvtWithAssignmentReason; + } + } + if (booking && booking.id && eventType.seatsPerTimeSlot) { const currentAttendee = booking.attendees.find( (attendee) => diff --git a/packages/features/bookings/repositories/BookingRepository.ts b/packages/features/bookings/repositories/BookingRepository.ts index 5af27616c1..041a017486 100644 --- a/packages/features/bookings/repositories/BookingRepository.ts +++ b/packages/features/bookings/repositories/BookingRepository.ts @@ -342,6 +342,16 @@ const selectStatementToGetBookingForCalEventBuilder = { attendee: { select: { id: true, email: true, phoneNumber: true } }, }, }, + assignmentReason: { + select: { + reasonEnum: true, + reasonString: true, + }, + orderBy: { + createdAt: "desc" as const, + }, + take: 1, + }, }; export class BookingRepository implements IBookingRepository { diff --git a/packages/features/webhooks/lib/sendPayload.ts b/packages/features/webhooks/lib/sendPayload.ts index 106f0cc06e..1bd642a690 100644 --- a/packages/features/webhooks/lib/sendPayload.ts +++ b/packages/features/webhooks/lib/sendPayload.ts @@ -79,7 +79,7 @@ export type OOOEntryPayloadType = { }; }; -export type EventPayloadType = CalendarEvent & +export type EventPayloadType = Omit & TranscriptionGeneratedPayload & EventTypeInfo & { uid?: string | null; @@ -97,6 +97,11 @@ export type EventPayloadType = CalendarEvent & cancelledBy?: string; paymentData?: PaymentData; requestReschedule?: boolean; + assignmentReason?: + | string + | { reasonEnum: string; reasonString: string }[] + | { category: string; details?: string | null } + | null; }; export type WebhookPayloadType = diff --git a/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts b/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts index c91411dc2e..f2887c4f6e 100644 --- a/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts @@ -4,6 +4,7 @@ import { getLocationValueForDB } from "@calcom/app-store/locations"; import { sendDeclinedEmailsAndSMS } from "@calcom/emails/email-manager"; import { getBookingEventHandlerService } from "@calcom/features/bookings/di/BookingEventHandlerService.container"; import { getAllCredentialsIncludeServiceAccountKey } from "@calcom/features/bookings/lib/getAllCredentialsForUsersOnEvent/getAllCredentials"; +import { getAssignmentReasonCategory } from "@calcom/features/bookings/lib/getAssignmentReasonCategory"; import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses"; import { handleConfirmation } from "@calcom/features/bookings/lib/handleConfirmation"; import { handleWebhookTrigger } from "@calcom/features/bookings/lib/handleWebhookTrigger"; @@ -197,6 +198,16 @@ export const confirmHandler = async ({ ctx, input }: ConfirmOptions) => { recurringEventId: true, status: true, smsReminderNumber: true, + assignmentReason: { + select: { + reasonEnum: true, + reasonString: true, + }, + orderBy: { + createdAt: "desc" as const, + }, + take: 1, + }, }, }); @@ -321,6 +332,12 @@ export const confirmHandler = async ({ ctx, input }: ConfirmOptions) => { ...(platformClientParams ? platformClientParams : {}), organizationId: organizerOrganizationId ?? booking.eventType?.team?.parentId ?? null, additionalNotes: booking.description, + assignmentReason: booking.assignmentReason?.[0]?.reasonEnum + ? { + category: getAssignmentReasonCategory(booking.assignmentReason[0].reasonEnum), + details: booking.assignmentReason[0].reasonString ?? null, + } + : null, }; const recurringEvent = parseRecurringEvent(booking.eventType?.recurringEvent); diff --git a/packages/types/Calendar.d.ts b/packages/types/Calendar.d.ts index 8d88ad9679..b9cb0ebe88 100644 --- a/packages/types/Calendar.d.ts +++ b/packages/types/Calendar.d.ts @@ -226,6 +226,10 @@ export interface CalendarEvent { rescheduledBy?: string; organizationId?: number | null; hasOrganizerChanged?: boolean; + assignmentReason?: { + category: string; // Translated label like "Routed", "Reassigned", etc. + details?: string | null; // The detailed reason string + } | null; } export interface EntryPoint {