From 3a7122d613b00a1d425ce2d61ff09f3238801a34 Mon Sep 17 00:00:00 2001 From: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com> Date: Tue, 17 Feb 2026 04:55:10 +0400 Subject: [PATCH] fix: revert assignmentReason breaking change in webhook payloads (#27891) * fix: revert assignmentReason breaking change in webhook payloads Remove the new { category, details } format from EventPayloadType to maintain backward compatibility for webhook consumers. The new format is stripped at all webhook payload construction sites by destructuring assignmentReason out of CalendarEvent before spreading into the payload. A sanitizeAssignmentReasonForWebhook function provides an additional safety net in sendPayload itself. Emails and booking single view continue to use the new format via CalendarEvent. Co-Authored-By: ali@cal.com * fix: strip assignmentReason from handlePaymentSuccess webhook payload Co-Authored-By: ali@cal.com * fix: strip assignmentReason from triggerWebhooks and handleSeats webhook payloads Co-Authored-By: ali@cal.com * refactor: use zod safeParse instead of type assertion in sanitizeAssignmentReasonForWebhook Co-Authored-By: ali@cal.com --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- apps/web/lib/daily-webhook/triggerWebhooks.ts | 6 +- .../_utils/payments/handlePaymentSuccess.ts | 3 +- .../lib/getWebhookPayloadForBooking.ts | 3 +- .../bookings/lib/handleCancelBooking.ts | 5 +- .../bookings/lib/handleConfirmation.ts | 8 +- .../handleSeats/cancel/cancelAttendeeSeat.ts | 12 +- .../bookings/lib/handleSeats/handleSeats.ts | 3 +- .../lib/service/RegularBookingService.ts | 65 ++++---- .../features/webhooks/lib/sendPayload.test.ts | 140 +++++++++++++++++- packages/features/webhooks/lib/sendPayload.ts | 31 ++-- .../viewer/bookings/confirm.handler.ts | 16 +- 11 files changed, 225 insertions(+), 67 deletions(-) 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,