Files
calendar/packages/features/di/webhooks/containers/webhook.ts
T
Syed Ali ShahbazGitHubali@cal.com <alishahbaz7@gmail.com>ali@cal.com <alishahbaz7@gmail.com>Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>Hariom Balhara
5d65df9c05 chore: migrate booking requested webhook trigger (#27546)
* init

* wiring up

* fix type

* feat: implement DI pattern for webhook producer in API v2

- Export IWebhookProducerService and getWebhookProducer from platform-libraries
- Add WEBHOOK_PRODUCER token and useFactory provider in RegularBookingModule
- Inject webhookProducer in RegularBookingService and pass to base class

This follows the composition root pattern where only the NestJS module
knows about getWebhookProducer(), and all consumers depend only on the
IWebhookProducerService interface via constructor injection.

Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com>

* test: migrate BOOKING_REQUESTED tests to new webhook architecture

- Remove failing BOOKING_REQUESTED tests from fresh-booking.test.ts (4 tests)
- Remove failing BOOKING_REQUESTED tests from reschedule.test.ts (2 tests)
- Remove failing BOOKING_REQUESTED test from collective-scheduling.test.ts (1 test)
- Replace WebhookTaskConsumer.test.ts with placeholder (constructor changed)
- Create new webhook architecture test suite:
  - producer/WebhookTaskerProducerService.test.ts (14 tests)
  - consumer/WebhookTaskConsumer.test.ts (8 tests)
  - consumer/triggers/booking-requested.test.ts (8 tests)

The new test suite is organized by trigger type for extensibility as more
triggers are migrated to the producer/consumer pattern.

Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com>

* test: remove paid events BOOKING_REQUESTED test (moved to new architecture)

Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com>

* wrap webhook in own try-catch

* wire datafetcher

* fix

* fix v2

* fix circular dependency

* --

* merge-conflict-resolve

* mreg-conflict-resolve

* remove early return

* test: add integration tests for BOOKING_REQUESTED webhook producer invocation

Cover all 8 scenarios verifying the booking flow correctly invokes
the webhook producer for BOOKING_REQUESTED:

1. Basic confirmation → queueBookingRequestedWebhook called
2. Booker-is-organizer + confirmation → still called
3. Confirmation threshold NOT met → not called (BOOKING_CREATED instead)
4. Confirmation threshold IS met → called
5. Paid event + confirmation → called after payment succeeds
6. Reschedule + confirmation (non-organizer) → called (not BOOKING_RESCHEDULED)
7. Reschedule + confirmation (organizer) → not called (BOOKING_RESCHEDULED instead)
8. Collective scheduling + confirmation → called

Adds reusable MockWebhookProducer helper in @calcom/testing for
extendable use as more webhook triggers migrate to the new architecture.

Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com>

* fix bug

* fix conditional check

* remove unnecessary comment

* add missing expect

* remove empty test

* clean up

* tasker config

* --

* fix missing metadata

* remove faulty if else

* test: add payload content verification tests for BOOKING_REQUESTED webhook

Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com>

* remove unnecessary tests

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Hariom Balhara <1780212+hariombalhara@users.noreply.github.com>
2026-02-19 20:21:26 +04:00

135 lines
6.6 KiB
TypeScript

import type { WebhookFeature } from "@calcom/features/webhooks/lib/facade/WebhookFeature";
import type { IWebhookRepository } from "@calcom/features/webhooks/lib/interface/IWebhookRepository";
import type {
IBookingWebhookService,
IFormWebhookService,
IOOOWebhookService,
IRecordingWebhookService,
IWebhookService,
} from "@calcom/features/webhooks/lib/interface/services";
import type { IWebhookProducerService } from "@calcom/features/webhooks/lib/interface/WebhookProducerService";
import type { IWebhookNotifier } from "@calcom/features/webhooks/lib/interface/webhook";
import type { WebhookTaskConsumer } from "@calcom/features/webhooks/lib/service/WebhookTaskConsumer";
import { type Container, createContainer } from "@evyweb/ioctopus";
import { moduleLoader as bookingRepositoryModuleLoader } from "../../modules/Booking";
import { moduleLoader as prismaModuleLoader } from "../../modules/Prisma";
import { moduleLoader as loggerModuleLoader } from "../../shared/services/logger.service";
import { taskerServiceModule } from "../../shared/services/tasker.service";
import { SHARED_TOKENS } from "../../shared/shared.tokens";
import { bookingWebhookDataFetcherModule } from "../modules/BookingWebhookDataFetcher.module";
import { formWebhookDataFetcherModule } from "../modules/FormWebhookDataFetcher.module";
import { oooWebhookDataFetcherModule } from "../modules/OOOWebhookDataFetcher.module";
import { paymentWebhookDataFetcherModule } from "../modules/PaymentWebhookDataFetcher.module";
import { recordingWebhookDataFetcherModule } from "../modules/RecordingWebhookDataFetcher.module";
import { webhookModule } from "../modules/Webhook.module";
import { moduleLoader as webhookProducerServiceModuleLoader } from "../modules/WebhookProducerService.module";
import { webhookTaskConsumerModule } from "../modules/WebhookTaskConsumer.module";
import { WEBHOOK_TOKENS } from "../Webhooks.tokens";
const webhookContainer: Container = createContainer();
// Load shared infrastructure
loggerModuleLoader.loadModule(webhookContainer);
prismaModuleLoader.loadModule(webhookContainer);
bookingRepositoryModuleLoader.loadModule(webhookContainer);
webhookContainer.load(SHARED_TOKENS.TASKER, taskerServiceModule);
// Load webhook module (includes cross-table repositories + all webhook services)
webhookContainer.load(WEBHOOK_TOKENS.WEBHOOK_EVENT_TYPE_REPOSITORY, webhookModule);
webhookContainer.load(WEBHOOK_TOKENS.WEBHOOK_USER_REPOSITORY, webhookModule);
webhookContainer.load(WEBHOOK_TOKENS.WEBHOOK_REPOSITORY, webhookModule);
webhookContainer.load(WEBHOOK_TOKENS.WEBHOOK_SERVICE, webhookModule);
webhookContainer.load(WEBHOOK_TOKENS.BOOKING_WEBHOOK_SERVICE, webhookModule);
webhookContainer.load(WEBHOOK_TOKENS.FORM_WEBHOOK_SERVICE, webhookModule);
webhookContainer.load(WEBHOOK_TOKENS.RECORDING_WEBHOOK_SERVICE, webhookModule);
webhookContainer.load(WEBHOOK_TOKENS.OOO_WEBHOOK_SERVICE, webhookModule);
webhookContainer.load(WEBHOOK_TOKENS.PAYLOAD_BUILDER_FACTORY, webhookModule);
webhookContainer.load(WEBHOOK_TOKENS.WEBHOOK_NOTIFICATION_HANDLER, webhookModule);
webhookContainer.load(WEBHOOK_TOKENS.WEBHOOK_NOTIFIER, webhookModule);
// Load Data Fetchers (Strategy Pattern implementations)
webhookContainer.load(WEBHOOK_TOKENS.BOOKING_DATA_FETCHER, bookingWebhookDataFetcherModule);
webhookContainer.load(WEBHOOK_TOKENS.PAYMENT_DATA_FETCHER, paymentWebhookDataFetcherModule);
webhookContainer.load(WEBHOOK_TOKENS.FORM_DATA_FETCHER, formWebhookDataFetcherModule);
webhookContainer.load(WEBHOOK_TOKENS.RECORDING_DATA_FETCHER, recordingWebhookDataFetcherModule);
webhookContainer.load(WEBHOOK_TOKENS.OOO_DATA_FETCHER, oooWebhookDataFetcherModule);
// Load Producer/Consumer modules
// Use moduleLoader pattern for producer service to load WebhookTasker dependencies
webhookProducerServiceModuleLoader.loadModule(webhookContainer);
webhookContainer.load(WEBHOOK_TOKENS.WEBHOOK_TASK_CONSUMER, webhookTaskConsumerModule);
export { webhookContainer };
/**
* Get the Webhook Task Consumer.
*
* This is used internally by the tasker handler (`webhookDelivery.ts`).
* For application code, use `getWebhookFeature().consumer` instead.
*/
export function getWebhookTaskConsumer(): WebhookTaskConsumer {
return webhookContainer.get<WebhookTaskConsumer>(WEBHOOK_TOKENS.WEBHOOK_TASK_CONSUMER);
}
/**
* Get the complete Webhook Feature facade (RECOMMENDED).
*
* This is the primary interface for webhook functionality.
* It provides access to all webhook services through a unified, type-safe API.
*
* Usage:
* ```typescript
* import { getWebhookFeature } from "@calcom/features/di/webhooks/containers/webhook";
*
* const webhooks = getWebhookFeature();
*
* // Queue a webhook (lightweight - Producer pattern)
* await webhooks.producer.queueBookingCreatedWebhook({ ... });
*
* // Or use event-specific services (legacy - will be deprecated in Phase 6)
* await webhooks.booking.emitBookingCreated({ ... });
* ```
*/
export function getWebhookFeature(): WebhookFeature {
return {
producer: webhookContainer.get<IWebhookProducerService>(WEBHOOK_TOKENS.WEBHOOK_PRODUCER_SERVICE),
consumer: webhookContainer.get<WebhookTaskConsumer>(WEBHOOK_TOKENS.WEBHOOK_TASK_CONSUMER),
core: webhookContainer.get<IWebhookService>(WEBHOOK_TOKENS.WEBHOOK_SERVICE),
booking: webhookContainer.get<IBookingWebhookService>(WEBHOOK_TOKENS.BOOKING_WEBHOOK_SERVICE),
form: webhookContainer.get<IFormWebhookService>(WEBHOOK_TOKENS.FORM_WEBHOOK_SERVICE),
recording: webhookContainer.get<IRecordingWebhookService>(WEBHOOK_TOKENS.RECORDING_WEBHOOK_SERVICE),
ooo: webhookContainer.get<IOOOWebhookService>(WEBHOOK_TOKENS.OOO_WEBHOOK_SERVICE),
notifier: webhookContainer.get<IWebhookNotifier>(WEBHOOK_TOKENS.WEBHOOK_NOTIFIER),
repository: webhookContainer.get<IWebhookRepository>(WEBHOOK_TOKENS.WEBHOOK_REPOSITORY),
};
}
/**
* Get only the webhook producer service
*
* Use this when you only need to queue webhooks, not consume or manage them.
* Lighter import footprint for better tree-shaking and faster module loading.
*
* Benefits:
* - Interface Segregation Principle: Import only what you need
* - Smaller bundle size: Avoid loading entire facade
* - Clearer intent: "I'm only queueing webhooks"
*
* Usage:
* ```typescript
* import { getWebhookProducer } from "@calcom/features/di/webhooks";
*
* const producer = getWebhookProducer();
* await producer.queueBookingCreatedWebhook({
* bookingUid: booking.uid,
* eventTypeId: eventType.id,
* userId: user.id,
* });
* ```
*
* @returns Lightweight webhook producer service (no heavy dependencies)
*/
export function getWebhookProducer(): IWebhookProducerService {
return webhookContainer.get<IWebhookProducerService>(WEBHOOK_TOKENS.WEBHOOK_PRODUCER_SERVICE);
}