2ec9e903ce
* feat: implement webhook tasker with async/sync fallback This PR implements a webhook tasker with async/sync fallback architecture to fix failing E2E tests. The solution follows the existing proration tasker pattern and uses Dependency Injection with @evyweb/ioctopus. Key changes: - Create IWebhookTasker interface for webhook delivery - Implement WebhookSyncTasker for immediate execution (E2E tests) - Implement WebhookAsyncTasker for queued execution (production) - Create main WebhookTasker class extending Tasker<IWebhookTasker> - Add DI modules and tokens for all tasker components - Update WebhookTaskerProducerService to use new WebhookTasker - Add unit tests for sync and async taskers The ENABLE_ASYNC_TASKER flag automatically selects the appropriate mode: - Production: Uses WebhookAsyncTasker to queue tasks - E2E Tests: Uses WebhookSyncTasker for immediate execution This ensures webhooks are delivered immediately in E2E tests without requiring the cron job that processes queued tasks. Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com> * fix: update WebhookTaskerProducerService tests for new interface Update tests to use the new deps-based constructor and mockWebhookTasker.deliverWebhook instead of mockTasker.create Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com> * fix: use moduleLoader pattern for WebhookProducerService in container Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com> * refactor: replace InternalTasker with Trigger.dev for webhook delivery This commit refactors the WebhookTasker to use Trigger.dev instead of InternalTasker, following the pattern established in BookingEmailAndSmsTasker and PlatformOrganizationBillingTasker (PR #26803). Changes: - Replace WebhookAsyncTasker with WebhookTriggerTasker that uses trigger.dev - Create trigger.dev task files (deliver-webhook.ts, config.ts, schema.ts) - Update DI modules to use WebhookTriggerTasker - Remove old InternalTasker-based implementation - Update unit tests for new implementation The WebhookSyncTasker continues to execute webhooks immediately for E2E tests where ENABLE_ASYNC_TASKER is automatically false. Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com> * fix: update comments to reflect Trigger.dev usage instead of InternalTasker Co-Authored-By: unknown <> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
235 lines
7.6 KiB
TypeScript
235 lines
7.6 KiB
TypeScript
import { WebhookTriggerEvents } from "@calcom/prisma/enums";
|
|
import { v4 as uuidv4 } from "uuid";
|
|
import type { BookingTriggerEvents, PaymentTriggerEvents } from "../factory/versioned/PayloadBuilderFactory";
|
|
import type { ILogger } from "../interface/infrastructure";
|
|
import type {
|
|
IWebhookProducerService,
|
|
QueueBookingWebhookParams,
|
|
QueueFormWebhookParams,
|
|
QueueOOOWebhookParams,
|
|
QueuePaymentWebhookParams,
|
|
QueueRecordingWebhookParams,
|
|
} from "../interface/WebhookProducerService";
|
|
import type { WebhookTasker } from "../tasker/WebhookTasker";
|
|
import type { WebhookTaskPayload } from "../types/webhookTask";
|
|
|
|
/**
|
|
* Lightweight Producer Service for webhook delivery.
|
|
*
|
|
* DEPENDENCIES: Only Tasker and Logger (no Prisma, no repositories)
|
|
*
|
|
* This service queues minimal webhook tasks to be processed by WebhookTaskConsumer.
|
|
* The consumer handles the heavy lifting (DB queries, payload building, HTTP delivery).
|
|
*/
|
|
/**
|
|
* Dependencies for WebhookTaskerProducerService
|
|
*/
|
|
export interface IWebhookTaskerProducerServiceDeps {
|
|
webhookTasker: WebhookTasker;
|
|
logger: ILogger;
|
|
}
|
|
|
|
export class WebhookTaskerProducerService implements IWebhookProducerService {
|
|
private readonly log: ILogger;
|
|
|
|
constructor(private readonly deps: IWebhookTaskerProducerServiceDeps) {
|
|
this.log = deps.logger.getSubLogger({ prefix: ["[WebhookTaskerProducerService]"] });
|
|
}
|
|
|
|
async queueBookingCreatedWebhook(params: QueueBookingWebhookParams): Promise<void> {
|
|
await this.queueBookingWebhook(WebhookTriggerEvents.BOOKING_CREATED, params);
|
|
}
|
|
|
|
async queueBookingCancelledWebhook(params: QueueBookingWebhookParams): Promise<void> {
|
|
await this.queueBookingWebhook(WebhookTriggerEvents.BOOKING_CANCELLED, params);
|
|
}
|
|
|
|
async queueBookingRescheduledWebhook(params: QueueBookingWebhookParams): Promise<void> {
|
|
await this.queueBookingWebhook(WebhookTriggerEvents.BOOKING_RESCHEDULED, params);
|
|
}
|
|
|
|
/**
|
|
* Queue a webhook for requested bookings.
|
|
*
|
|
* This fires when bookings require confirmation (status = PENDING).
|
|
*/
|
|
async queueBookingRequestedWebhook(params: QueueBookingWebhookParams): Promise<void> {
|
|
await this.queueBookingWebhook(WebhookTriggerEvents.BOOKING_REQUESTED, params);
|
|
}
|
|
|
|
async queueBookingRejectedWebhook(params: QueueBookingWebhookParams): Promise<void> {
|
|
await this.queueBookingWebhook(WebhookTriggerEvents.BOOKING_REJECTED, params);
|
|
}
|
|
|
|
async queueBookingPaymentInitiatedWebhook(params: QueuePaymentWebhookParams): Promise<void> {
|
|
await this.queuePaymentWebhook(WebhookTriggerEvents.BOOKING_PAYMENT_INITIATED, params);
|
|
}
|
|
|
|
async queueBookingPaidWebhook(params: QueuePaymentWebhookParams): Promise<void> {
|
|
await this.queuePaymentWebhook(WebhookTriggerEvents.BOOKING_PAID, params);
|
|
}
|
|
|
|
async queueBookingNoShowUpdatedWebhook(params: QueueBookingWebhookParams): Promise<void> {
|
|
await this.queueBookingWebhook(WebhookTriggerEvents.BOOKING_NO_SHOW_UPDATED, params);
|
|
}
|
|
|
|
async queueFormSubmittedWebhook(params: QueueFormWebhookParams): Promise<void> {
|
|
const operationId = params.operationId || uuidv4();
|
|
|
|
this.log.debug("Queueing form webhook task", {
|
|
operationId,
|
|
triggerEvent: WebhookTriggerEvents.FORM_SUBMITTED,
|
|
formId: params.formId,
|
|
});
|
|
|
|
const taskPayload: WebhookTaskPayload = {
|
|
operationId,
|
|
triggerEvent: WebhookTriggerEvents.FORM_SUBMITTED,
|
|
formId: params.formId,
|
|
teamId: params.teamId,
|
|
userId: params.userId,
|
|
oAuthClientId: params.oAuthClientId,
|
|
metadata: params.metadata,
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
|
|
await this.queueTask(operationId, taskPayload);
|
|
}
|
|
|
|
async queueRecordingReadyWebhook(params: QueueRecordingWebhookParams): Promise<void> {
|
|
const operationId = params.operationId || uuidv4();
|
|
|
|
this.log.debug("Queueing recording webhook task", {
|
|
operationId,
|
|
triggerEvent: WebhookTriggerEvents.RECORDING_READY,
|
|
recordingId: params.recordingId,
|
|
bookingUid: params.bookingUid,
|
|
});
|
|
|
|
const taskPayload: WebhookTaskPayload = {
|
|
operationId,
|
|
triggerEvent: WebhookTriggerEvents.RECORDING_READY,
|
|
recordingId: params.recordingId,
|
|
bookingUid: params.bookingUid,
|
|
eventTypeId: params.eventTypeId,
|
|
teamId: params.teamId,
|
|
userId: params.userId,
|
|
oAuthClientId: params.oAuthClientId,
|
|
metadata: params.metadata,
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
|
|
await this.queueTask(operationId, taskPayload);
|
|
}
|
|
|
|
async queueOOOCreatedWebhook(params: QueueOOOWebhookParams): Promise<void> {
|
|
const operationId = params.operationId || uuidv4();
|
|
|
|
this.log.debug("Queueing OOO webhook task", {
|
|
operationId,
|
|
triggerEvent: WebhookTriggerEvents.OOO_CREATED,
|
|
oooEntryId: params.oooEntryId,
|
|
userId: params.userId,
|
|
});
|
|
|
|
const taskPayload: WebhookTaskPayload = {
|
|
operationId,
|
|
triggerEvent: WebhookTriggerEvents.OOO_CREATED,
|
|
oooEntryId: params.oooEntryId,
|
|
userId: params.userId,
|
|
teamId: params.teamId,
|
|
oAuthClientId: params.oAuthClientId,
|
|
metadata: params.metadata,
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
|
|
await this.queueTask(operationId, taskPayload);
|
|
}
|
|
|
|
/**
|
|
* Internal helper to queue booking-related webhooks
|
|
*/
|
|
private async queueBookingWebhook(
|
|
triggerEvent: Exclude<
|
|
BookingTriggerEvents,
|
|
typeof WebhookTriggerEvents.BOOKING_PAYMENT_INITIATED | typeof WebhookTriggerEvents.BOOKING_PAID
|
|
>,
|
|
params: QueueBookingWebhookParams
|
|
): Promise<void> {
|
|
const operationId = params.operationId || uuidv4();
|
|
|
|
this.log.debug("Queueing booking webhook task", {
|
|
operationId,
|
|
triggerEvent,
|
|
bookingUid: params.bookingUid,
|
|
eventTypeId: params.eventTypeId,
|
|
});
|
|
|
|
const taskPayload: WebhookTaskPayload = {
|
|
operationId,
|
|
triggerEvent,
|
|
bookingUid: params.bookingUid,
|
|
eventTypeId: params.eventTypeId,
|
|
teamId: params.teamId,
|
|
userId: params.userId,
|
|
orgId: params.orgId,
|
|
oAuthClientId: params.oAuthClientId,
|
|
metadata: params.metadata,
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
|
|
await this.queueTask(operationId, taskPayload);
|
|
}
|
|
|
|
/**
|
|
* Internal helper to queue payment-related webhooks
|
|
*/
|
|
private async queuePaymentWebhook(
|
|
triggerEvent: PaymentTriggerEvents,
|
|
params: QueuePaymentWebhookParams
|
|
): Promise<void> {
|
|
const operationId = params.operationId || uuidv4();
|
|
|
|
this.log.debug("Queueing payment webhook task", {
|
|
operationId,
|
|
triggerEvent,
|
|
bookingUid: params.bookingUid,
|
|
});
|
|
|
|
const taskPayload: WebhookTaskPayload = {
|
|
operationId,
|
|
triggerEvent,
|
|
bookingUid: params.bookingUid,
|
|
eventTypeId: params.eventTypeId,
|
|
teamId: params.teamId,
|
|
userId: params.userId,
|
|
orgId: params.orgId,
|
|
oAuthClientId: params.oAuthClientId,
|
|
metadata: params.metadata,
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
|
|
await this.queueTask(operationId, taskPayload);
|
|
}
|
|
|
|
/**
|
|
* Internal helper to queue task via WebhookTasker
|
|
*
|
|
* The WebhookTasker automatically selects the appropriate execution mode:
|
|
* - Production: Queues to Trigger.dev for background processing
|
|
* - E2E Tests: Executes immediately via WebhookSyncTasker
|
|
*/
|
|
private async queueTask(operationId: string, taskPayload: WebhookTaskPayload): Promise<void> {
|
|
try {
|
|
const result = await this.deps.webhookTasker.deliverWebhook(taskPayload);
|
|
this.log.debug("Webhook delivery task queued", { operationId, taskId: result.taskId });
|
|
} catch (error) {
|
|
this.log.error("Failed to queue webhook delivery task", {
|
|
operationId,
|
|
error: error instanceof Error ? error.message : String(error),
|
|
});
|
|
throw error;
|
|
}
|
|
}
|
|
}
|