From 3355fab4fb8c58b8c0d8567a9a5de8a14dc879bf Mon Sep 17 00:00:00 2001
From: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Date: Fri, 23 Jan 2026 18:50:07 -0500
Subject: [PATCH] feat: display assignment reason in organizer emails and
booking single view (#27192)
---
.../bookings/views/bookings-single-view.tsx | 26 ++++++-
.../src/templates/BaseScheduledEmail.tsx | 7 ++
.../features/CalendarEventBuilder.test.ts | 75 +++++++++++++++++++
packages/features/CalendarEventBuilder.ts | 72 +++++++++++-------
.../lib/getAssignmentReasonCategory.test.ts | 36 +++++++++
.../lib/getAssignmentReasonCategory.ts | 23 ++++++
.../features/bookings/lib/getUserBooking.ts | 10 +++
.../lib/service/RegularBookingService.ts | 15 ++++
.../repositories/BookingRepository.ts | 10 +++
packages/features/webhooks/lib/sendPayload.ts | 7 +-
.../viewer/bookings/confirm.handler.ts | 17 +++++
packages/types/Calendar.d.ts | 4 +
12 files changed, 273 insertions(+), 29 deletions(-)
create mode 100644 packages/features/bookings/lib/getAssignmentReasonCategory.test.ts
create mode 100644 packages/features/bookings/lib/getAssignmentReasonCategory.ts
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 {