diff --git a/apps/web/lib/daily-webhook/triggerWebhooks.ts b/apps/web/lib/daily-webhook/triggerWebhooks.ts index 3aa7ab7502..6474b6c098 100644 --- a/apps/web/lib/daily-webhook/triggerWebhooks.ts +++ b/apps/web/lib/daily-webhook/triggerWebhooks.ts @@ -54,8 +54,9 @@ export const triggerRecordingReadyWebhook = async ({ }) ); + const { assignmentReason: _emailAssignmentReason, ...evtWithoutAssignmentReason } = evt; const payload: EventPayloadType = { - ...evt, + ...evtWithoutAssignmentReason, downloadLink, }; @@ -94,8 +95,9 @@ export const triggerTranscriptionGeneratedWebhook = async ({ }) ); + const { assignmentReason: _emailAssignmentReason2, ...evtWithoutAssignmentReason2 } = evt; const payload: EventPayloadType = { - ...evt, + ...evtWithoutAssignmentReason2, downloadLinks, }; diff --git a/packages/app-store/_utils/payments/handlePaymentSuccess.ts b/packages/app-store/_utils/payments/handlePaymentSuccess.ts index f55da25499..fb7c327d93 100644 --- a/packages/app-store/_utils/payments/handlePaymentSuccess.ts +++ b/packages/app-store/_utils/payments/handlePaymentSuccess.ts @@ -160,8 +160,9 @@ export async function handlePaymentSuccess(params: { length: booking.eventType?.length, }; + const { assignmentReason: _emailAssignmentReason, ...evtWithoutAssignmentReason } = evt; const payload: EventPayloadType = { - ...evt, + ...evtWithoutAssignmentReason, ...eventTypeInfo, bookingId, eventTypeId: booking.eventType?.id, diff --git a/packages/features/bookings/lib/getWebhookPayloadForBooking.ts b/packages/features/bookings/lib/getWebhookPayloadForBooking.ts index 8e75040fd8..2ccee97307 100644 --- a/packages/features/bookings/lib/getWebhookPayloadForBooking.ts +++ b/packages/features/bookings/lib/getWebhookPayloadForBooking.ts @@ -30,8 +30,9 @@ export const getWebhookPayloadForBooking = ({ length: booking.eventType?.length, }; + const { assignmentReason: _emailAssignmentReason, ...evtWithoutAssignmentReason } = evt; const payload: EventPayloadType = { - ...evt, + ...evtWithoutAssignmentReason, ...eventTypeInfo, bookingId: booking.id, }; diff --git a/packages/features/bookings/lib/handleCancelBooking.ts b/packages/features/bookings/lib/handleCancelBooking.ts index 92f278c543..f64ed8d7e8 100644 --- a/packages/features/bookings/lib/handleCancelBooking.ts +++ b/packages/features/bookings/lib/handleCancelBooking.ts @@ -13,11 +13,11 @@ import { import type { ActionSource } from "@calcom/features/booking-audit/lib/types/actionSource"; import { BookingReferenceRepository } from "@calcom/features/bookingReference/repositories/BookingReferenceRepository"; import { getBookingEventHandlerService } from "@calcom/features/bookings/di/BookingEventHandlerService.container"; -import { getFeaturesRepository } from "@calcom/features/di/containers/FeaturesRepository"; import EventManager from "@calcom/features/bookings/lib/EventManager"; import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses"; import { processNoShowFeeOnCancellation } from "@calcom/features/bookings/lib/payment/processNoShowFeeOnCancellation"; import { processPaymentRefund } from "@calcom/features/bookings/lib/payment/processPaymentRefund"; +import { getFeaturesRepository } from "@calcom/features/di/containers/FeaturesRepository"; import { CreditService } from "@calcom/features/ee/billing/credit-service"; import { getBookerBaseUrl } from "@calcom/features/ee/organizations/lib/getBookerUrlServer"; import { getAllWorkflowsFromEventType } from "@calcom/features/ee/workflows/lib/getAllWorkflowsFromEventType"; @@ -429,9 +429,10 @@ async function handler(input: CancelBookingInput, dependencies?: Dependencies) { isPlatformManagedUserBooking: bookingToDelete.user.isPlatformManaged, } satisfies HandleCancelBookingResponse; + const { assignmentReason: _emailAssignmentReason, ...evtWithoutAssignmentReason } = evt; const promises = webhooks.map((webhook) => sendPayload(webhook.secret, eventTrigger, new Date().toISOString(), webhook, { - ...evt, + ...evtWithoutAssignmentReason, ...eventTypeInfo, status: "CANCELLED", smsReminderNumber: bookingToDelete.smsReminderNumber || undefined, diff --git a/packages/features/bookings/lib/handleConfirmation.ts b/packages/features/bookings/lib/handleConfirmation.ts index 74112ff8a5..22e6bbf993 100644 --- a/packages/features/bookings/lib/handleConfirmation.ts +++ b/packages/features/bookings/lib/handleConfirmation.ts @@ -4,9 +4,10 @@ import { sendScheduledEmailsAndSMS } from "@calcom/emails/email-manager"; import type { Actor } from "@calcom/features/booking-audit/lib/dto/types"; import type { ActionSource } from "@calcom/features/booking-audit/lib/types/actionSource"; import { getBookingEventHandlerService } from "@calcom/features/bookings/di/BookingEventHandlerService.container"; -import { getFeaturesRepository } from "@calcom/features/di/containers/FeaturesRepository"; import type { EventManagerUser } from "@calcom/features/bookings/lib/EventManager"; import EventManager, { placeholderCreatedEvent } from "@calcom/features/bookings/lib/EventManager"; +import { getFeaturesRepository } from "@calcom/features/di/containers/FeaturesRepository"; +import type { ISimpleLogger } from "@calcom/features/di/shared/services/logger.service"; import { CreditService } from "@calcom/features/ee/billing/credit-service"; import { getBookerBaseUrl } from "@calcom/features/ee/organizations/lib/getBookerUrlServer"; import { @@ -34,10 +35,8 @@ import type { PlatformClientParams } from "@calcom/prisma/zod-utils"; import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; import type { AdditionalInformation, CalendarEvent } from "@calcom/types/Calendar"; import { v4 as uuidv4 } from "uuid"; - import { getCalEventResponses } from "./getCalEventResponses"; import { scheduleNoShowTriggers } from "./handleNewBooking/scheduleNoShowTriggers"; -import type { ISimpleLogger } from "@calcom/features/di/shared/services/logger.service"; async function fireBookingAcceptedEvent({ actor, @@ -570,8 +569,9 @@ export async function handleConfirmation(args: { length: eventType?.length, }; + const { assignmentReason: _emailAssignmentReason, ...evtWithoutAssignmentReason } = evt; const payload: EventPayloadType = { - ...evt, + ...evtWithoutAssignmentReason, ...eventTypeInfo, bookingId, eventTypeId: eventType?.id, diff --git a/packages/features/bookings/lib/handleSeats/cancel/cancelAttendeeSeat.ts b/packages/features/bookings/lib/handleSeats/cancel/cancelAttendeeSeat.ts index 7616b6b448..1d3d14f818 100644 --- a/packages/features/bookings/lib/handleSeats/cancel/cancelAttendeeSeat.ts +++ b/packages/features/bookings/lib/handleSeats/cancel/cancelAttendeeSeat.ts @@ -1,6 +1,8 @@ import { getCalendar } from "@calcom/app-store/_utils/getCalendar"; -import { getAllDelegationCredentialsForUserIncludeServiceAccountKey } from "@calcom/app-store/delegationCredential"; -import { getDelegationCredentialOrFindRegularCredential } from "@calcom/app-store/delegationCredential"; +import { + getAllDelegationCredentialsForUserIncludeServiceAccountKey, + getDelegationCredentialOrFindRegularCredential, +} from "@calcom/app-store/delegationCredential"; import { sendCancelledSeatEmailsAndSMS } from "@calcom/emails/email-manager"; import { updateMeeting } from "@calcom/features/conferencing/lib/videoClient"; import { WorkflowRepository } from "@calcom/features/ee/workflows/repositories/WorkflowRepository"; @@ -14,10 +16,9 @@ import { safeStringify } from "@calcom/lib/safeStringify"; import { getTranslation } from "@calcom/lib/server/i18n"; import prisma from "@calcom/prisma"; import { WebhookTriggerEvents } from "@calcom/prisma/enums"; -import { bookingCancelAttendeeSeatSchema } from "@calcom/prisma/zod-utils"; import type { EventTypeMetadata } from "@calcom/prisma/zod-utils"; +import { bookingCancelAttendeeSeatSchema } from "@calcom/prisma/zod-utils"; import type { CalendarEvent } from "@calcom/types/Calendar"; - import type { BookingToDelete } from "../../handleCancelBooking"; async function cancelAttendeeSeat( @@ -159,8 +160,9 @@ async function cancelAttendeeSeat( ] : []; + const { assignmentReason: _emailAssignmentReason, ...evtWithoutAssignmentReason } = evt; const payload: EventPayloadType = { - ...evt, + ...evtWithoutAssignmentReason, ...eventTypeInfo, status: "CANCELLED", smsReminderNumber: bookingToDelete.smsReminderNumber || undefined, diff --git a/packages/features/bookings/lib/handleSeats/handleSeats.ts b/packages/features/bookings/lib/handleSeats/handleSeats.ts index 7c1153a449..0d02d9d69b 100644 --- a/packages/features/bookings/lib/handleSeats/handleSeats.ts +++ b/packages/features/bookings/lib/handleSeats/handleSeats.ts @@ -284,8 +284,9 @@ const handleSeats = async ( loggerWithEventDetails.error("Error while scheduling workflow reminders", JSON.stringify({ error })); } + const { assignmentReason: _emailAssignmentReason, ...evtWithoutAssignmentReason } = evt; const webhookData: EventPayloadType = { - ...evt, + ...evtWithoutAssignmentReason, ...eventTypeInfo, uid: resultBooking?.uid || uid, bookingId: seatedBooking?.id, diff --git a/packages/features/bookings/lib/service/RegularBookingService.ts b/packages/features/bookings/lib/service/RegularBookingService.ts index 3892eabdd6..2aa40761ad 100644 --- a/packages/features/bookings/lib/service/RegularBookingService.ts +++ b/packages/features/bookings/lib/service/RegularBookingService.ts @@ -1,11 +1,4 @@ -import short, { uuid } from "short-uuid"; -import { v5 as uuidv5 } from "uuid"; -import { getAuditActionSource } from "../handleNewBooking/getAuditActionSource"; -import { - buildBookingCreatedAuditData, - buildBookingRescheduledAuditData, -} from "../handleNewBooking/buildBookingEventAuditData"; -import type { ActionSource } from "@calcom/features/booking-audit/lib/types/actionSource"; +import process from "node:process"; import processExternalId from "@calcom/app-store/_utils/calendars/processExternalId"; import { getPaymentAppData } from "@calcom/app-store/_utils/payments/getPaymentAppData"; import { @@ -20,33 +13,33 @@ import { } from "@calcom/app-store/locations"; import { getAppFromSlug } from "@calcom/app-store/utils"; import { - eventTypeMetaDataSchemaWithTypedApps, eventTypeAppMetadataOptionalSchema, + eventTypeMetaDataSchemaWithTypedApps, } from "@calcom/app-store/zod-utils"; import dayjs from "@calcom/dayjs"; import { scheduleMandatoryReminder } from "@calcom/ee/workflows/lib/reminders/scheduleMandatoryReminder"; 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 { ActionSource } from "@calcom/features/booking-audit/lib/types/actionSource"; import type { - CreateRegularBookingData, - CreateBookingMeta, + BookingDataSchemaGetter, BookingHandlerInput, + CreateBookingMeta, + CreateRegularBookingData, } from "@calcom/features/bookings/lib/dto/types"; +import EventManager, { placeholderCreatedEvent } from "@calcom/features/bookings/lib/EventManager"; +import { getAssignmentReasonCategory } from "@calcom/features/bookings/lib/getAssignmentReasonCategory"; import type { CheckBookingAndDurationLimitsService } from "@calcom/features/bookings/lib/handleNewBooking/checkBookingAndDurationLimits"; import { handlePayment } from "@calcom/features/bookings/lib/handlePayment"; import { handleWebhookTrigger } from "@calcom/features/bookings/lib/handleWebhookTrigger"; import { isEventTypeLoggingEnabled } from "@calcom/features/bookings/lib/isEventTypeLoggingEnabled"; -import { BookingEventHandlerService } from "@calcom/features/bookings/lib/onBookingEvents/BookingEventHandlerService"; +import type { BookingEventHandlerService } from "@calcom/features/bookings/lib/onBookingEvents/BookingEventHandlerService"; import type { BookingRescheduledPayload } from "@calcom/features/bookings/lib/onBookingEvents/types.d"; -import { BookingEmailAndSmsTasker } from "@calcom/features/bookings/lib/tasker/BookingEmailAndSmsTasker"; +import type { BookingEmailAndSmsTasker } from "@calcom/features/bookings/lib/tasker/BookingEmailAndSmsTasker"; +import { CalendarEventBuilder } from "@calcom/features/CalendarEventBuilder"; import { getSpamCheckService } from "@calcom/features/di/watchlist/containers/SpamCheckService.container"; import { CreditService } from "@calcom/features/ee/billing/credit-service"; import { getBookerBaseUrl } from "@calcom/features/ee/organizations/lib/getBookerUrlServer"; -import { getRoutingTraceService } from "@calcom/features/routing-trace/di/RoutingTraceService.container"; import AssignmentReasonRecorder from "@calcom/features/ee/round-robin/assignmentReason/AssignmentReasonRecorder"; import { BookingLocationService } from "@calcom/features/ee/round-robin/lib/bookingLocationService"; import { getAllWorkflowsFromEventType } from "@calcom/features/ee/workflows/lib/getAllWorkflowsFromEventType"; @@ -54,24 +47,25 @@ import { WorkflowService } from "@calcom/features/ee/workflows/lib/service/Workf import { WorkflowRepository } from "@calcom/features/ee/workflows/repositories/WorkflowRepository"; import { getUsernameList } from "@calcom/features/eventtypes/lib/defaultEvents"; import { getEventName, updateHostInEventName } from "@calcom/features/eventtypes/lib/eventNaming"; -import { FeaturesRepository } from "@calcom/features/flags/features.repository"; +import type { FeaturesRepository } from "@calcom/features/flags/features.repository"; import { getFullName } from "@calcom/features/form-builder/utils"; import type { HashedLinkService } from "@calcom/features/hashedLink/lib/service/HashedLinkService"; import { ProfileRepository } from "@calcom/features/profile/repositories/ProfileRepository"; +import { getRoutingTraceService } from "@calcom/features/routing-trace/di/RoutingTraceService.container"; import { handleAnalyticsEvents } from "@calcom/features/tasker/tasks/analytics/handleAnalyticsEvents"; import type { UserRepository } from "@calcom/features/users/repositories/UserRepository"; import { UsersRepository } from "@calcom/features/users/users.repository"; import type { GetSubscriberOptions } from "@calcom/features/webhooks/lib/getWebhooks"; import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks"; import { - deleteWebhookScheduledTriggers, cancelNoShowTasksForBooking, + deleteWebhookScheduledTriggers, scheduleTrigger, } from "@calcom/features/webhooks/lib/scheduleTrigger"; import type { EventPayloadType, EventTypeInfo } from "@calcom/features/webhooks/lib/sendPayload"; -import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser"; import { groupHostsByGroupId } from "@calcom/lib/bookings/hostGroupUtils"; import { shouldIgnoreContactOwner } from "@calcom/lib/bookings/routing/utils"; +import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser"; import { DEFAULT_GROUP_ID, ENABLE_ASYNC_TASKER } from "@calcom/lib/constants"; import { ErrorCode } from "@calcom/lib/errorCodes"; import { ErrorWithCode } from "@calcom/lib/errors"; @@ -87,36 +81,43 @@ import { getTranslation } from "@calcom/lib/server/i18n"; import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat"; import { distributedTracing } from "@calcom/lib/tracing/factory"; import type { PrismaClient } from "@calcom/prisma"; -import type { DestinationCalendar, Prisma, User, AssignmentReasonEnum } from "@calcom/prisma/client"; +import type { AssignmentReasonEnum, DestinationCalendar, Prisma, User } from "@calcom/prisma/client"; import { BookingStatus, + CreationSource, SchedulingType, WebhookTriggerEvents, WorkflowTriggerEvents, - CreationSource, } from "@calcom/prisma/enums"; import { userMetadata as userMetadataSchema } from "@calcom/prisma/zod-utils"; import type { AdditionalInformation, AppsStatus, - CalendarEvent, CalEventResponses, + CalendarEvent, } from "@calcom/types/Calendar"; import type { CredentialForCalendarService } from "@calcom/types/Credential"; import type { EventResult, PartialReference } from "@calcom/types/EventManager"; - +import short, { uuid } from "short-uuid"; +import { v5 as uuidv5 } from "uuid"; import type { BookingRepository } from "../../repositories/BookingRepository"; -import { BookingActionMap, BookingEmailSmsHandler, type BookingActionType } from "../BookingEmailSmsHandler"; +import { BookingActionMap, type BookingActionType, BookingEmailSmsHandler } from "../BookingEmailSmsHandler"; import { getAllCredentialsIncludeServiceAccountKey } from "../getAllCredentialsForUsersOnEvent/getAllCredentials"; import { refreshCredentials } from "../getAllCredentialsForUsersOnEvent/refreshCredentials"; import getBookingDataSchema from "../getBookingDataSchema"; -import { LuckyUserService } from "../getLuckyUser"; +import type { LuckyUserService } from "../getLuckyUser"; import { addVideoCallDataToEvent } from "../handleNewBooking/addVideoCallDataToEvent"; +import { + buildBookingCreatedAuditData, + buildBookingRescheduledAuditData, +} from "../handleNewBooking/buildBookingEventAuditData"; import { checkActiveBookingsLimitForBooker } from "../handleNewBooking/checkActiveBookingsLimitForBooker"; import { checkIfBookerEmailIsBlocked } from "../handleNewBooking/checkIfBookerEmailIsBlocked"; -import { createBooking } from "../handleNewBooking/createBooking"; import type { Booking } from "../handleNewBooking/createBooking"; +import { createBooking } from "../handleNewBooking/createBooking"; import { ensureAvailableUsers } from "../handleNewBooking/ensureAvailableUsers"; +import { getAuditActionSource } from "../handleNewBooking/getAuditActionSource"; +import { getBookingAuditActorForNewBooking } from "../handleNewBooking/getBookingAuditActorForNewBooking"; import { getBookingData } from "../handleNewBooking/getBookingData"; import { getCustomInputsResponses } from "../handleNewBooking/getCustomInputsResponses"; import { getEventType } from "../handleNewBooking/getEventType"; @@ -127,15 +128,14 @@ import { getSeatedBooking } from "../handleNewBooking/getSeatedBooking"; import { getVideoCallDetails } from "../handleNewBooking/getVideoCallDetails"; import { handleAppsStatus } from "../handleNewBooking/handleAppsStatus"; import { loadAndValidateUsers } from "../handleNewBooking/loadAndValidateUsers"; -import { getOriginalRescheduledBooking } from "../handleNewBooking/originalRescheduledBookingUtils"; import type { BookingType } from "../handleNewBooking/originalRescheduledBookingUtils"; +import { getOriginalRescheduledBooking } from "../handleNewBooking/originalRescheduledBookingUtils"; import { scheduleNoShowTriggers } from "../handleNewBooking/scheduleNoShowTriggers"; import type { IEventTypePaymentCredentialType, Invitee, IsFixedAwareUser } from "../handleNewBooking/types"; import { validateBookingTimeIsNotOutOfBounds } from "../handleNewBooking/validateBookingTimeIsNotOutOfBounds"; import { validateEventLength } from "../handleNewBooking/validateEventLength"; import handleSeats from "../handleSeats/handleSeats"; import type { IBookingService } from "../interfaces/IBookingService"; -import { getBookingAuditActorForNewBooking } from "../handleNewBooking/getBookingAuditActorForNewBooking"; import { isWithinMinimumRescheduleNotice } from "../reschedule/isWithinMinimumRescheduleNotice"; const translator = short(); @@ -1305,7 +1305,7 @@ async function handler( const isManagedEventType = !!eventType.parentId; // Track credential ID for per-host locations - let perHostCredentialId: number | undefined = undefined; + let perHostCredentialId: number | undefined; // Handle per-host custom locations for round-robin events if ( @@ -2498,8 +2498,9 @@ async function handler( const webhookLocation = metadata?.videoCallUrl || evt.location; + const { assignmentReason: _emailAssignmentReason, ...evtWithoutAssignmentReason } = evt; const webhookData: EventPayloadType = { - ...evt, + ...evtWithoutAssignmentReason, ...eventTypeInfo, bookingId: booking?.id, rescheduleId: originalRescheduledBooking?.id || undefined, diff --git a/packages/features/webhooks/lib/sendPayload.test.ts b/packages/features/webhooks/lib/sendPayload.test.ts index 788d5b658c..5cc2ebf1d9 100644 --- a/packages/features/webhooks/lib/sendPayload.test.ts +++ b/packages/features/webhooks/lib/sendPayload.test.ts @@ -1,7 +1,7 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; - +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { WebhookVersion } from "./interface/IWebhookRepository"; -import sendPayload from "./sendPayload"; +import type { EventPayloadType } from "./sendPayload"; +import sendPayload, { sanitizeAssignmentReasonForWebhook } from "./sendPayload"; describe("sendPayload", () => { const mockFetch = vi.fn(); @@ -108,4 +108,138 @@ describe("sendPayload", () => { expect(options.headers["X-Cal-Webhook-Version"]).toBe("2021-10-20"); }); }); + + describe("sanitizeAssignmentReasonForWebhook", () => { + const basePayload = { + title: "Test Booking", + startTime: "2024-01-01T10:00:00Z", + endTime: "2024-01-01T11:00:00Z", + organizer: { + email: "organizer@example.com", + name: "Organizer", + timeZone: "UTC", + language: { locale: "en" }, + }, + attendees: [], + type: "test-event", + } as unknown as EventPayloadType; + + it("should preserve string assignmentReason", () => { + const data = { ...basePayload, assignmentReason: "Salesforce contact owner: user@example.com" }; + const result = sanitizeAssignmentReasonForWebhook(data); + expect(result.assignmentReason).toBe("Salesforce contact owner: user@example.com"); + }); + + it("should preserve array assignmentReason with reasonEnum and reasonString", () => { + const reasons = [{ reasonEnum: "ROUTED", reasonString: "Language: English" }]; + const data = { ...basePayload, assignmentReason: reasons }; + const result = sanitizeAssignmentReasonForWebhook(data); + expect(result.assignmentReason).toEqual(reasons); + }); + + it("should preserve null assignmentReason", () => { + const data = { ...basePayload, assignmentReason: null }; + const result = sanitizeAssignmentReasonForWebhook(data); + expect(result.assignmentReason).toBeNull(); + }); + + it("should preserve undefined assignmentReason", () => { + const data = { ...basePayload }; + const result = sanitizeAssignmentReasonForWebhook(data); + expect(result.assignmentReason).toBeUndefined(); + }); + + it("should strip CalendarEvent email format { category, details }", () => { + const data = { + ...basePayload, + assignmentReason: { category: "routed", details: "Language: English" }, + } as unknown as EventPayloadType; + const result = sanitizeAssignmentReasonForWebhook(data); + expect(result.assignmentReason).toBeUndefined(); + }); + + it("should strip CalendarEvent email format with null details", () => { + const data = { + ...basePayload, + assignmentReason: { category: "reassigned", details: null }, + } as unknown as EventPayloadType; + const result = sanitizeAssignmentReasonForWebhook(data); + expect(result.assignmentReason).toBeUndefined(); + }); + + it("should not strip other fields when stripping assignmentReason", () => { + const data = { + ...basePayload, + bookingId: 123, + status: "ACCEPTED", + assignmentReason: { category: "routed", details: "test" }, + } as unknown as EventPayloadType; + const result = sanitizeAssignmentReasonForWebhook(data); + expect(result.assignmentReason).toBeUndefined(); + expect(result.bookingId).toBe(123); + expect(result.status).toBe("ACCEPTED"); + expect(result.title).toBe("Test Booking"); + }); + }); + + describe("sendPayload strips CalendarEvent assignmentReason format", () => { + it("should not include { category, details } format in webhook payload", async () => { + const webhook = { + subscriberUrl: "https://example.com/webhook", + appId: null, + payloadTemplate: null, + version: WebhookVersion.V_2021_10_20, + }; + + await sendPayload("test-secret", "BOOKING_CREATED", new Date().toISOString(), webhook, { + title: "Test Booking", + startTime: "2024-01-01T10:00:00Z", + endTime: "2024-01-01T11:00:00Z", + organizer: { + email: "organizer@example.com", + name: "Organizer", + timeZone: "UTC", + language: { locale: "en" }, + }, + attendees: [], + type: "test-event", + description: "", + assignmentReason: { category: "routed", details: "Language: English" }, + } as unknown as Parameters[4]); + + const [, options] = mockFetch.mock.calls[0]; + const body = JSON.parse(options.body); + expect(body.payload.assignmentReason).toBeUndefined(); + }); + + it("should preserve array assignmentReason format in webhook payload", async () => { + const webhook = { + subscriberUrl: "https://example.com/webhook", + appId: null, + payloadTemplate: null, + version: WebhookVersion.V_2021_10_20, + }; + + const reasons = [{ reasonEnum: "ROUTED", reasonString: "Language: English" }]; + await sendPayload("test-secret", "BOOKING_CREATED", new Date().toISOString(), webhook, { + title: "Test Booking", + startTime: "2024-01-01T10:00:00Z", + endTime: "2024-01-01T11:00:00Z", + organizer: { + email: "organizer@example.com", + name: "Organizer", + timeZone: "UTC", + language: { locale: "en" }, + }, + attendees: [], + type: "test-event", + description: "", + assignmentReason: reasons, + } as unknown as Parameters[4]); + + const [, options] = mockFetch.mock.calls[0]; + const body = JSON.parse(options.body); + expect(body.payload.assignmentReason).toEqual(reasons); + }); + }); }); diff --git a/packages/features/webhooks/lib/sendPayload.ts b/packages/features/webhooks/lib/sendPayload.ts index 1bd642a690..baddd3cbac 100644 --- a/packages/features/webhooks/lib/sendPayload.ts +++ b/packages/features/webhooks/lib/sendPayload.ts @@ -1,12 +1,15 @@ import { createHmac } from "node:crypto"; -import { compile } from "handlebars"; - import type { TGetTranscriptAccessLink } from "@calcom/app-store/dailyvideo/zod"; import { getHumanReadableLocationValue } from "@calcom/app-store/locations"; -import type { WebhookSubscriber, PaymentData } from "@calcom/features/webhooks/lib/dto/types"; -import { DelegationCredentialErrorPayloadType } from "@calcom/features/webhooks/lib/dto/types"; +import type { + DelegationCredentialErrorPayloadType, + PaymentData, + WebhookSubscriber, +} from "@calcom/features/webhooks/lib/dto/types"; import { getUTCOffsetByTimezone } from "@calcom/lib/dayjs"; import type { CalendarEvent, Person } from "@calcom/types/Calendar"; +import { compile } from "handlebars"; +import { z } from "zod"; // Minimal webhook shape for sending payloads (subset of WebhookSubscriber) type WebhookForPayload = Pick; @@ -97,11 +100,7 @@ export type EventPayloadType = Omit & cancelledBy?: string; paymentData?: PaymentData; requestReschedule?: boolean; - assignmentReason?: - | string - | { reasonEnum: string; reasonString: string }[] - | { category: string; details?: string | null } - | null; + assignmentReason?: string | { reasonEnum: string; reasonString: string }[] | null; }; export type WebhookPayloadType = @@ -222,6 +221,19 @@ export function isEventPayload(data: WebhookPayloadType): data is EventPayloadTy return !isNoShowPayload(data) && !isOOOEntryPayload(data) && !isDelegationCredentialErrorPayload(data); } +const webhookAssignmentReasonSchema = z.union([ + z.string(), + z.array(z.object({ reasonEnum: z.string(), reasonString: z.string() })), + z.null(), + z.undefined(), +]); + +export function sanitizeAssignmentReasonForWebhook(data: EventPayloadType): EventPayloadType { + const result = webhookAssignmentReasonSchema.safeParse(data.assignmentReason); + if (result.success) return data; + return { ...data, assignmentReason: undefined }; +} + const sendPayload = async ( secretKey: string | null, triggerEvent: string, @@ -239,6 +251,7 @@ const sendPayload = async ( let body; /* Zapier id is hardcoded in the DB, we send the raw data for this case */ if (isEventPayload(data)) { + data = sanitizeAssignmentReasonForWebhook(data); data.description = data.description || data.additionalNotes; if (appId === "zapier") { body = getZapierPayload({ ...data, createdAt }); diff --git a/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts b/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts index e4f6a4a979..c97c4526ff 100644 --- a/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts @@ -2,8 +2,9 @@ import { getUsersCredentialsIncludeServiceAccountKey } from "@calcom/app-store/d import type { LocationObject } from "@calcom/app-store/locations"; import { getLocationValueForDB } from "@calcom/app-store/locations"; import { sendDeclinedEmailsAndSMS } from "@calcom/emails/email-manager"; +import type { Actor } from "@calcom/features/booking-audit/lib/dto/types"; +import type { ValidActionSource } from "@calcom/features/booking-audit/lib/types/actionSource"; import { getBookingEventHandlerService } from "@calcom/features/bookings/di/BookingEventHandlerService.container"; -import { getFeaturesRepository } from "@calcom/features/di/containers/FeaturesRepository"; 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"; @@ -11,6 +12,8 @@ import { handleConfirmation } from "@calcom/features/bookings/lib/handleConfirma import { handleWebhookTrigger } from "@calcom/features/bookings/lib/handleWebhookTrigger"; import { processPaymentRefund } from "@calcom/features/bookings/lib/payment/processPaymentRefund"; import { BookingAccessService } from "@calcom/features/bookings/services/BookingAccessService"; +import { getFeaturesRepository } from "@calcom/features/di/containers/FeaturesRepository"; +import type { ISimpleLogger } from "@calcom/features/di/shared/services/logger.service"; import { CreditService } from "@calcom/features/ee/billing/credit-service"; import { getBookerBaseUrl } from "@calcom/features/ee/organizations/lib/getBookerUrlServer"; import { workflowSelect } from "@calcom/features/ee/workflows/lib/getAllWorkflows"; @@ -22,6 +25,8 @@ import getOrgIdFromMemberOrTeamId from "@calcom/lib/getOrgIdFromMemberOrTeamId"; import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType"; import { isPrismaObjOrUndefined } from "@calcom/lib/isPrismaObj"; import { parseRecurringEvent } from "@calcom/lib/isRecurringEvent"; +import logger from "@calcom/lib/logger"; +import { safeStringify } from "@calcom/lib/safeStringify"; import { getTranslation } from "@calcom/lib/server/i18n"; import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat"; import type { TraceContext } from "@calcom/lib/tracing"; @@ -34,11 +39,7 @@ import { TRPCError } from "@trpc/server"; import { v4 as uuidv4 } from "uuid"; import type { TrpcSessionUser } from "../../../types"; import type { TConfirmInputSchema } from "./confirm.schema"; -import type { ValidActionSource } from "@calcom/features/booking-audit/lib/types/actionSource"; -import type { Actor } from "@calcom/features/booking-audit/lib/dto/types"; -import type { ISimpleLogger } from "@calcom/features/di/shared/services/logger.service"; -import { safeStringify } from "@calcom/lib/safeStringify"; -import logger from "@calcom/lib/logger"; + type ConfirmOptions = { ctx: { user: Pick< @@ -537,8 +538,9 @@ export const confirmHandler = async ({ ctx, input }: ConfirmOptions) => { currency: booking.eventType?.currency, length: booking.eventType?.length, }; + const { assignmentReason: _emailAssignmentReason, ...evtWithoutAssignmentReason } = evt; const webhookData: EventPayloadType = { - ...evt, + ...evtWithoutAssignmentReason, ...eventTypeInfo, bookingId, eventTypeId: booking.eventType?.id,