Files
calendar/packages/features/webhooks/lib/service/WebhookNotificationHandler.ts
T
Syed Ali ShahbazandGitHub 110615b509 chore: add webhook architecture skeleton (#23247)
* add migration guide and dto

* add factory

* add notifier

* add repo

* add services

* coderabbit review --1

* coderabbit review --2

* coderabbit review --3

* further improvement

* --

* fix

* bookingWebhookFactory consideration and type fixes

* cleanup

* fix types

* DI part1

* DI --part 2

* remove migrationGuide as we're WIP

* using evyweb for DI -- 1

* DI --final

* separate func instead of private class

* adds a todo migration file

* adjust structure

* address feedback

* remove todo_migrate

* --1

* fix type

* address feedback

* add TODO comment

* address requested changes --1

* address feedback --2

* restructure as per feedback

* rename camelcase
2025-09-12 20:02:50 +00:00

136 lines
5.0 KiB
TypeScript

import { WebhookTriggerEvents } from "@calcom/prisma/enums";
import type { WebhookEventDTO } from "../dto/types";
import type { BookingPayloadBuilder } from "../factory/BookingPayloadBuilder";
import type { FormPayloadBuilder } from "../factory/FormPayloadBuilder";
import type { InstantMeetingBuilder } from "../factory/InstantMeetingBuilder";
import type { MeetingPayloadBuilder } from "../factory/MeetingPayloadBuilder";
import type { OOOPayloadBuilder } from "../factory/OOOPayloadBuilder";
import type { RecordingPayloadBuilder } from "../factory/RecordingPayloadBuilder";
import type { WebhookPayload } from "../factory/types";
import type { ILogger } from "../interface/infrastructure";
import type { IWebhookService } from "../interface/services";
import type { IWebhookNotificationHandler } from "../interface/webhook";
export class WebhookNotificationHandler implements IWebhookNotificationHandler {
private readonly log: ILogger;
constructor(
private readonly webhookService: IWebhookService,
private readonly bookingPayloadBuilder: BookingPayloadBuilder,
private readonly formPayloadBuilder: FormPayloadBuilder,
private readonly oooPayloadBuilder: OOOPayloadBuilder,
private readonly recordingPayloadBuilder: RecordingPayloadBuilder,
private readonly meetingPayloadBuilder: MeetingPayloadBuilder,
private readonly instantMeetingBuilder: InstantMeetingBuilder,
logger: ILogger
) {
this.log = logger.getSubLogger({ prefix: ["[WebhookNotificationHandler]"] });
}
async handleNotification(dto: WebhookEventDTO, isDryRun = false): Promise<void> {
const trigger = dto.triggerEvent;
try {
if (isDryRun) {
this.log.debug(`Dry run mode - skipping webhook notification for: ${trigger}`);
return;
}
const subscriptionParams = {
userId: dto.userId,
eventTypeId: dto.eventTypeId,
triggerEvent: trigger,
teamId: dto.teamId,
orgId: dto.orgId,
oAuthClientId: dto.platformClientId,
};
this.log.debug(`Querying for webhook subscribers with params:`, subscriptionParams);
const subscribers = await this.webhookService.getSubscribers(subscriptionParams);
if (subscribers.length === 0) {
this.log.debug(`No subscribers found for event: ${trigger}`, {
bookingId: dto.bookingId,
eventTypeId: dto.eventTypeId,
});
return;
}
const webhookPayload = this.createPayload(dto);
await this.webhookService.processWebhooks(trigger, webhookPayload, subscribers);
this.log.debug(`Successfully processed webhook notification: ${trigger}`, {
subscriberCount: subscribers.length,
bookingId: dto.bookingId,
});
} catch (error) {
this.log.error(`Error handling webhook notification: ${trigger}`, {
error: error instanceof Error ? error.message : String(error),
bookingId: dto.bookingId,
eventTypeId: dto.eventTypeId,
});
throw error;
}
}
private createPayload(dto: WebhookEventDTO): WebhookPayload {
switch (dto.triggerEvent) {
// Booking events
case WebhookTriggerEvents.BOOKING_CREATED:
case WebhookTriggerEvents.BOOKING_CANCELLED:
case WebhookTriggerEvents.BOOKING_REQUESTED:
case WebhookTriggerEvents.BOOKING_RESCHEDULED:
case WebhookTriggerEvents.BOOKING_REJECTED:
case WebhookTriggerEvents.BOOKING_PAID:
case WebhookTriggerEvents.BOOKING_PAYMENT_INITIATED:
case WebhookTriggerEvents.BOOKING_NO_SHOW_UPDATED:
return this.bookingPayloadBuilder.build(dto);
// Form events
case WebhookTriggerEvents.FORM_SUBMITTED:
case WebhookTriggerEvents.FORM_SUBMITTED_NO_EVENT:
return this.formPayloadBuilder.build(dto);
// OOO events
case WebhookTriggerEvents.OOO_CREATED:
return this.oooPayloadBuilder.build(dto);
// Recording events
case WebhookTriggerEvents.RECORDING_READY:
case WebhookTriggerEvents.RECORDING_TRANSCRIPTION_GENERATED:
return this.recordingPayloadBuilder.build(dto);
// Meeting events
case WebhookTriggerEvents.MEETING_STARTED:
case WebhookTriggerEvents.MEETING_ENDED:
return this.meetingPayloadBuilder.build(dto);
// Instant meeting events
case WebhookTriggerEvents.INSTANT_MEETING:
return this.instantMeetingBuilder.build(dto);
// No-show events (special handling)
case WebhookTriggerEvents.AFTER_HOSTS_CAL_VIDEO_NO_SHOW:
case WebhookTriggerEvents.AFTER_GUESTS_CAL_VIDEO_NO_SHOW: {
return {
triggerEvent: dto.triggerEvent,
createdAt: dto.createdAt,
payload: {
bookingId: dto.bookingId,
webhook: dto.webhook,
},
};
}
default: {
// TypeScript exhaustiveness check - this should never happen if all cases are covered
const _exhaustiveCheck: never = dto;
throw new Error(`Unsupported triggerEvent: ${JSON.stringify(_exhaustiveCheck)}`);
}
}
}
}