diff --git a/packages/features/tasker/tasks/webhookDelivery.ts b/packages/features/tasker/tasks/webhookDelivery.ts index a5c449728a..756447a246 100644 --- a/packages/features/tasker/tasks/webhookDelivery.ts +++ b/packages/features/tasker/tasks/webhookDelivery.ts @@ -1,28 +1,26 @@ import { getWebhookTaskConsumer } from "@calcom/features/di/webhooks/containers/webhook"; import { webhookTaskPayloadSchema } from "@calcom/features/webhooks/lib/types/webhookTask"; import logger from "@calcom/lib/logger"; +import type { ILogger } from "@calcom/lib/tasker/types"; /** * Webhook Delivery Task Handler - * + * * This task is queued by WebhookTaskerProducerService and processed here. * It delegates to WebhookTaskConsumer (via DI) which handles the heavy lifting: * - Fetching webhook subscribers * - Fetching event-specific data from database * - Building versioned webhook payloads * - Sending HTTP requests to subscriber URLs - * + * * This handler can be deployed to trigger.dev for scalability. */ -const log = logger.getSubLogger({ prefix: ["webhookDelivery"] }); +const log: ILogger = logger.getSubLogger({ prefix: ["webhookDelivery"] }); export async function webhookDelivery(payload: string, taskId?: string): Promise { try { if (!taskId) { - log.error("Task ID is required for webhook delivery consumer", { - taskId, - }); throw new Error("Task ID is required for webhook delivery consumer"); } diff --git a/packages/features/webhooks/lib/service/WebhookTaskConsumer.ts b/packages/features/webhooks/lib/service/WebhookTaskConsumer.ts index 9f531e047f..c1279b5697 100644 --- a/packages/features/webhooks/lib/service/WebhookTaskConsumer.ts +++ b/packages/features/webhooks/lib/service/WebhookTaskConsumer.ts @@ -35,7 +35,7 @@ export class WebhookTaskConsumer { * Main entry point for processing webhook delivery tasks. */ async processWebhookTask(payload: WebhookTaskPayload, taskId: string): Promise { - this.log.info("Processing webhook delivery task", { + this.log.debug("Processing webhook delivery task", { operationId: payload.operationId, taskId, triggerEvent: payload.triggerEvent, @@ -58,7 +58,7 @@ export class WebhookTaskConsumer { const subscribers = await this.webhookRepository.getSubscribers(subscriberContext); if (subscribers.length === 0) { - this.log.info("No webhook subscribers found", { operationId: payload.operationId }); + this.log.debug("No webhook subscribers found", { operationId: payload.operationId }); return; } @@ -80,7 +80,7 @@ export class WebhookTaskConsumer { // Step 4: Build and send webhooks to each subscriber await this.sendWebhooksToSubscribers(subscribers, eventData, payload); - this.log.info("Webhook delivery task completed", { + this.log.debug("Webhook delivery task completed", { operationId: payload.operationId, subscriberCount: subscribers.length, }); @@ -106,25 +106,16 @@ export class WebhookTaskConsumer { /** * Build webhook payloads and send to each subscriber. * - * TODO: Implement payload building using PayloadBuilders and HTTP sending. - * For Phase 0, this is a scaffold showing the pattern. + * TODO: Implement with PayloadBuilders and HTTP client (Phase 1+) */ private async sendWebhooksToSubscribers( subscribers: unknown[], eventData: Record, payload: WebhookTaskPayload ): Promise { - // TODO: For each subscriber: - // 1. Build versioned payload using PayloadBuilders - // 2. Send HTTP request to subscriber.subscriberUrl - // 3. Handle retries/failures - // 4. Log delivery status - this.log.debug("Webhook sending not implemented yet (Phase 0 scaffold)", { subscriberCount: subscribers.length, triggerEvent: payload.triggerEvent, }); - - // This will be implemented when we add PayloadBuilders and HTTP client dependencies } } diff --git a/packages/features/webhooks/lib/service/WebhookTaskerProducerService.ts b/packages/features/webhooks/lib/service/WebhookTaskerProducerService.ts index 319993d9a5..a054bf01d6 100644 --- a/packages/features/webhooks/lib/service/WebhookTaskerProducerService.ts +++ b/packages/features/webhooks/lib/service/WebhookTaskerProducerService.ts @@ -70,7 +70,7 @@ export class WebhookTaskerProducerService implements IWebhookProducerService { async queueFormSubmittedWebhook(params: QueueFormWebhookParams): Promise { const operationId = params.operationId || uuidv4(); - this.log.info("Queueing form webhook task", { + this.log.debug("Queueing form webhook task", { operationId, triggerEvent: WebhookTriggerEvents.FORM_SUBMITTED, formId: params.formId, @@ -93,7 +93,7 @@ export class WebhookTaskerProducerService implements IWebhookProducerService { async queueRecordingReadyWebhook(params: QueueRecordingWebhookParams): Promise { const operationId = params.operationId || uuidv4(); - this.log.info("Queueing recording webhook task", { + this.log.debug("Queueing recording webhook task", { operationId, triggerEvent: WebhookTriggerEvents.RECORDING_READY, recordingId: params.recordingId, @@ -119,7 +119,7 @@ export class WebhookTaskerProducerService implements IWebhookProducerService { async queueOOOCreatedWebhook(params: QueueOOOWebhookParams): Promise { const operationId = params.operationId || uuidv4(); - this.log.info("Queueing OOO webhook task", { + this.log.debug("Queueing OOO webhook task", { operationId, triggerEvent: WebhookTriggerEvents.OOO_CREATED, oooEntryId: params.oooEntryId, @@ -152,7 +152,7 @@ export class WebhookTaskerProducerService implements IWebhookProducerService { ): Promise { const operationId = params.operationId || uuidv4(); - this.log.info("Queueing booking webhook task", { + this.log.debug("Queueing booking webhook task", { operationId, triggerEvent, bookingUid: params.bookingUid, @@ -184,7 +184,7 @@ export class WebhookTaskerProducerService implements IWebhookProducerService { ): Promise { const operationId = params.operationId || uuidv4(); - this.log.info("Queueing payment webhook task", { + this.log.debug("Queueing payment webhook task", { operationId, triggerEvent, bookingUid: params.bookingUid, @@ -212,7 +212,7 @@ export class WebhookTaskerProducerService implements IWebhookProducerService { private async queueTask(operationId: string, taskPayload: WebhookTaskPayload): Promise { try { await this.tasker.create("webhookDelivery", taskPayload); - this.log.info("Webhook delivery task queued successfully", { operationId }); + this.log.debug("Webhook delivery task queued", { operationId }); } catch (error) { this.log.error("Failed to queue webhook delivery task", { operationId, diff --git a/packages/features/webhooks/lib/service/__tests__/WebhookTaskConsumer.test.ts b/packages/features/webhooks/lib/service/__tests__/WebhookTaskConsumer.test.ts index d748ca42ef..96a668d407 100644 --- a/packages/features/webhooks/lib/service/__tests__/WebhookTaskConsumer.test.ts +++ b/packages/features/webhooks/lib/service/__tests__/WebhookTaskConsumer.test.ts @@ -98,7 +98,7 @@ describe("WebhookTaskConsumer", () => { await consumer.processWebhookTask(payload, "task-123"); - expect(mockLogger.info).toHaveBeenCalledWith( + expect(mockLogger.debug).toHaveBeenCalledWith( "Processing webhook delivery task", expect.objectContaining({ operationId: "op-123", @@ -116,7 +116,7 @@ describe("WebhookTaskConsumer", () => { oAuthClientId: undefined, }); - expect(mockLogger.info).toHaveBeenCalledWith( + expect(mockLogger.debug).toHaveBeenCalledWith( "No webhook subscribers found", expect.objectContaining({ operationId: "op-123", @@ -171,55 +171,110 @@ describe("WebhookTaskConsumer", () => { }); describe("processWebhookTask - Event Type Routing", () => { - const testCases = [ - { - trigger: WebhookTriggerEvents.BOOKING_CREATED, - requiredField: "bookingUid", - }, - { - trigger: WebhookTriggerEvents.FORM_SUBMITTED, - requiredField: "formId", - }, - { - trigger: WebhookTriggerEvents.RECORDING_READY, - requiredField: "recordingId", - }, - { - trigger: WebhookTriggerEvents.OOO_CREATED, - requiredField: "oooEntryId", - }, - ]; + it("should process BOOKING_CREATED event type (scaffold)", async () => { + const payload: WebhookTaskPayload = { + operationId: "op-test", + triggerEvent: WebhookTriggerEvents.BOOKING_CREATED, + bookingUid: "test-booking-uid", + timestamp: new Date().toISOString(), + }; - testCases.forEach(({ trigger, requiredField }) => { - it(`should process ${trigger} event type (scaffold)`, async () => { - const payload: WebhookTaskPayload = { - operationId: "op-test", - triggerEvent: trigger, - [requiredField]: "test-id", - timestamp: new Date().toISOString(), - }; + vi.mocked(mockWebhookRepository.getSubscribers).mockResolvedValueOnce([ + { + id: "sub-1", + subscriberUrl: "https://example.com/webhook", + payloadTemplate: null, + appId: null, + secret: null, + time: null, + timeUnit: null, + eventTriggers: [WebhookTriggerEvents.BOOKING_CREATED], + version: WebhookVersion.V_2021_10_20, + }, + ]); - // Mock subscriber so we reach data fetching - vi.mocked(mockWebhookRepository.getSubscribers).mockResolvedValueOnce([ - { - id: "sub-1", - subscriberUrl: "https://example.com/webhook", - payloadTemplate: null, - appId: null, - secret: null, - time: null, - timeUnit: null, - eventTriggers: [trigger], - version: WebhookVersion.V_2021_10_20, - }, - ]); + await expect(consumer.processWebhookTask(payload, "task-test")).resolves.not.toThrow(); + expect(mockWebhookRepository.getSubscribers).toHaveBeenCalled(); + }); - // Should not throw - scaffold implementation logs debug messages - await expect(consumer.processWebhookTask(payload, "task-test")).resolves.not.toThrow(); + it("should process FORM_SUBMITTED event type (scaffold)", async () => { + const payload: WebhookTaskPayload = { + operationId: "op-test", + triggerEvent: WebhookTriggerEvents.FORM_SUBMITTED, + formId: "test-form-id", + timestamp: new Date().toISOString(), + }; - // Verify subscriber fetch was called - expect(mockWebhookRepository.getSubscribers).toHaveBeenCalled(); - }); + vi.mocked(mockWebhookRepository.getSubscribers).mockResolvedValueOnce([ + { + id: "sub-1", + subscriberUrl: "https://example.com/webhook", + payloadTemplate: null, + appId: null, + secret: null, + time: null, + timeUnit: null, + eventTriggers: [WebhookTriggerEvents.FORM_SUBMITTED], + version: WebhookVersion.V_2021_10_20, + }, + ]); + + await expect(consumer.processWebhookTask(payload, "task-test")).resolves.not.toThrow(); + expect(mockWebhookRepository.getSubscribers).toHaveBeenCalled(); + }); + + it("should process RECORDING_READY event type (scaffold)", async () => { + const payload: WebhookTaskPayload = { + operationId: "op-test", + triggerEvent: WebhookTriggerEvents.RECORDING_READY, + recordingId: "test-recording-id", + bookingUid: "test-booking-uid", + timestamp: new Date().toISOString(), + }; + + vi.mocked(mockWebhookRepository.getSubscribers).mockResolvedValueOnce([ + { + id: "sub-1", + subscriberUrl: "https://example.com/webhook", + payloadTemplate: null, + appId: null, + secret: null, + time: null, + timeUnit: null, + eventTriggers: [WebhookTriggerEvents.RECORDING_READY], + version: WebhookVersion.V_2021_10_20, + }, + ]); + + await expect(consumer.processWebhookTask(payload, "task-test")).resolves.not.toThrow(); + expect(mockWebhookRepository.getSubscribers).toHaveBeenCalled(); + }); + + it("should process OOO_CREATED event type (scaffold)", async () => { + const payload: WebhookTaskPayload = { + userId: 456, + operationId: "op-test", + triggerEvent: WebhookTriggerEvents.OOO_CREATED, + oooEntryId: 123, + timestamp: new Date().toISOString(), + }; + + vi.mocked(mockWebhookRepository.getSubscribers).mockResolvedValueOnce([ + { + id: "sub-1", + subscriberUrl: "https://example.com/webhook", + payloadTemplate: null, + appId: null, + secret: null, + time: null, + timeUnit: null, + eventTriggers: [WebhookTriggerEvents.OOO_CREATED], + version: WebhookVersion.V_2021_10_20, + }, + ]); + + await expect(consumer.processWebhookTask(payload, "task-test")).resolves.not.toThrow(); + expect(mockWebhookRepository.getSubscribers).toHaveBeenCalled(); }); }); @@ -251,7 +306,7 @@ describe("WebhookTaskConsumer", () => { const payload: WebhookTaskPayload = { operationId: "op-missing", triggerEvent: WebhookTriggerEvents.BOOKING_CREATED, - // Missing bookingUid + bookingUid: "booking-123", timestamp: new Date().toISOString(), }; @@ -284,40 +339,4 @@ describe("WebhookTaskConsumer", () => { ); }); }); - - describe("Future Implementation Tests", () => { - it("TODO [When WebhookTaskConsumer.fetchBookingData() is implemented]: Test full booking data fetching", () => { - // When: BookingRepository is injected into WebhookTaskConsumer - // When: fetchBookingData() implementation is complete - // Test: Fetch booking, eventType, user, attendees from database - // Test: Verify correct data structure returned - expect(true).toBe(true); // Placeholder - }); - - it("TODO [When PayloadBuilders are integrated into sendWebhooksToSubscribers()]: Test payload building", () => { - // When: BookingPayloadBuilder is integrated (for booking events) - // When: FormPayloadBuilder is integrated (for form events) - // When: RecordingPayloadBuilder is integrated (for recording events) - // When: OOOPayloadBuilder is integrated (for OOO events) - // Test: Build versioned payloads, apply payload templates - expect(true).toBe(true); // Placeholder - }); - - it("TODO [When sendWebhooksToSubscribers() makes HTTP calls]: Test HTTP delivery", () => { - // When: HTTP client is integrated (or existing sendPayload is used) - // When: sendWebhooksToSubscribers() sends to subscriber.subscriberUrl - // Test: Mock HTTP calls, verify correct payload sent - // Test: Handle retries, timeouts, errors - expect(true).toBe(true); // Placeholder - }); - - it("TODO [When all services are wired]: Integration test for full Producer→Consumer flow", () => { - // When: All webhook services use Producer/Consumer pattern - // Test: Full flow - Producer → Tasker → Consumer → HTTP delivery - // Test: Verify webhook received by mock HTTP server - // Test: Retry logic with task processor - // Test: E2E with real database and task queue - expect(true).toBe(true); // Placeholder - }); - }); }); diff --git a/packages/features/webhooks/lib/service/__tests__/WebhookTaskerProducerService.test.ts b/packages/features/webhooks/lib/service/__tests__/WebhookTaskerProducerService.test.ts index 3c7a6f2a93..36f9f23db4 100644 --- a/packages/features/webhooks/lib/service/__tests__/WebhookTaskerProducerService.test.ts +++ b/packages/features/webhooks/lib/service/__tests__/WebhookTaskerProducerService.test.ts @@ -95,12 +95,12 @@ describe("WebhookTaskerProducerService", () => { expect(payload.operationId).toBe("custom-op-id"); }); - it("should log info messages", async () => { + it("should log debug messages", async () => { await producer.queueBookingCreatedWebhook({ bookingUid: "booking-123", }); - expect(mockLogger.info).toHaveBeenCalledWith( + expect(mockLogger.debug).toHaveBeenCalledWith( "Queueing booking webhook task", expect.objectContaining({ operationId: expect.any(String), @@ -109,8 +109,8 @@ describe("WebhookTaskerProducerService", () => { }) ); - expect(mockLogger.info).toHaveBeenCalledWith( - "Webhook delivery task queued successfully", + expect(mockLogger.debug).toHaveBeenCalledWith( + "Webhook delivery task queued", expect.objectContaining({ operationId: expect.any(String), }) diff --git a/packages/features/webhooks/lib/service/data-fetchers/BookingWebhookDataFetcher.ts b/packages/features/webhooks/lib/service/data-fetchers/BookingWebhookDataFetcher.ts index bc42a386c2..ae2ed8db74 100644 --- a/packages/features/webhooks/lib/service/data-fetchers/BookingWebhookDataFetcher.ts +++ b/packages/features/webhooks/lib/service/data-fetchers/BookingWebhookDataFetcher.ts @@ -27,12 +27,7 @@ export class BookingWebhookDataFetcher implements IWebhookDataFetcher { return null; } - // TODO [Phase 1+]: Implement using BookingRepository (to be injected) - // const booking = await this.bookingRepository.findByUid(bookingUid); - // const eventType = await this.eventTypeRepository.findById(booking.eventTypeId); - // const organizer = await this.userRepository.findById(booking.userId); - // return { booking, eventType, organizer, attendees: booking.attendees }; - + // TODO: Implement using BookingRepository/EventTypeRepository/UserRepository (Phase 1+) this.logger.debug("Booking data fetch not implemented yet (Phase 0 scaffold)", { bookingUid }); return { bookingUid, _scaffold: true }; } diff --git a/packages/features/webhooks/lib/service/data-fetchers/FormWebhookDataFetcher.ts b/packages/features/webhooks/lib/service/data-fetchers/FormWebhookDataFetcher.ts index e4387a3272..79245fcbbd 100644 --- a/packages/features/webhooks/lib/service/data-fetchers/FormWebhookDataFetcher.ts +++ b/packages/features/webhooks/lib/service/data-fetchers/FormWebhookDataFetcher.ts @@ -18,11 +18,7 @@ export class FormWebhookDataFetcher implements IWebhookDataFetcher { return null; } - // TODO [Phase 1+]: Implement using FormRepository - // const form = await this.formRepository.findById(formId); - // const responses = await this.formRepository.getResponses(formId); - // return { form, responses }; - + // TODO: Implement using FormRepository (Phase 1+) this.logger.debug("Form data fetch not implemented yet (Phase 0 scaffold)", { formId }); return { formId, _scaffold: true }; } diff --git a/packages/features/webhooks/lib/service/data-fetchers/OOOWebhookDataFetcher.ts b/packages/features/webhooks/lib/service/data-fetchers/OOOWebhookDataFetcher.ts index 82cc58ff30..c6a6b9188b 100644 --- a/packages/features/webhooks/lib/service/data-fetchers/OOOWebhookDataFetcher.ts +++ b/packages/features/webhooks/lib/service/data-fetchers/OOOWebhookDataFetcher.ts @@ -18,11 +18,7 @@ export class OOOWebhookDataFetcher implements IWebhookDataFetcher { return null; } - // TODO [Phase 1+]: Implement using OOORepository - // const oooEntry = await this.oooRepository.findById(oooEntryId); - // const user = await this.userRepository.findById(oooEntry.userId); - // return { oooEntry, user }; - + // TODO: Implement using OOORepository (Phase 1+) this.logger.debug("OOO data fetch not implemented yet (Phase 0 scaffold)", { oooEntryId }); return { oooEntryId, _scaffold: true }; } diff --git a/packages/features/webhooks/lib/service/data-fetchers/PaymentWebhookDataFetcher.ts b/packages/features/webhooks/lib/service/data-fetchers/PaymentWebhookDataFetcher.ts index 0ec485038f..ef680d5fa6 100644 --- a/packages/features/webhooks/lib/service/data-fetchers/PaymentWebhookDataFetcher.ts +++ b/packages/features/webhooks/lib/service/data-fetchers/PaymentWebhookDataFetcher.ts @@ -23,11 +23,7 @@ export class PaymentWebhookDataFetcher implements IWebhookDataFetcher { return null; } - // TODO [Phase 1+]: Implement using BookingRepository and PaymentRepository - // const booking = await this.bookingRepository.findByUid(bookingUid); - // const payment = await this.paymentRepository.findByBookingId(booking.id); - // return { booking, payment, eventType, organizer, attendees }; - + // TODO: Implement using BookingRepository/PaymentRepository (Phase 1+) this.logger.debug("Payment data fetch not implemented yet (Phase 0 scaffold)", { bookingUid }); return { bookingUid, _scaffold: true }; } diff --git a/packages/features/webhooks/lib/service/data-fetchers/RecordingWebhookDataFetcher.ts b/packages/features/webhooks/lib/service/data-fetchers/RecordingWebhookDataFetcher.ts index effe58ba0f..665867241c 100644 --- a/packages/features/webhooks/lib/service/data-fetchers/RecordingWebhookDataFetcher.ts +++ b/packages/features/webhooks/lib/service/data-fetchers/RecordingWebhookDataFetcher.ts @@ -23,22 +23,7 @@ export class RecordingWebhookDataFetcher implements IWebhookDataFetcher { return null; } - // TODO [Phase 1+]: Implement recording data fetching - // Note: Recording files are stored by video providers. We receive recording_id from their webhook - // and generate our own proxy download link (using generateVideoToken + our API endpoint). - // This method fetches booking/event data from DB needed to build the webhook payload. - // - // Pattern: recordingId → booking → eventType → user → attendees - // Then generate downloadLink: `${WEBAPP_URL}/api/video/recording?token=${generateVideoToken(recordingId)}` - // - // const booking = await this.bookingRepository.findByUid(payload.bookingUid); - // const eventType = await this.eventTypeRepository.findById(booking.eventTypeId); - // const user = await this.userRepository.findById(booking.userId); - // const attendees = booking.attendees; - // const token = generateVideoToken(recordingId); - // const downloadLink = `${WEBAPP_URL}/api/video/recording?token=${token}`; - // return { booking, eventType, user, attendees, downloadLink }; - + // TODO: Fetch booking data and generate download link (Phase 1+) this.logger.debug("Recording data fetch not implemented yet (Phase 0 scaffold)", { recordingId }); return { recordingId, _scaffold: true }; }