Skip to content

Commit 857b3da

Browse files
chore: Webhook Scaffolding cleanup (calcom#27159)
* quick cleanup * more fixes
1 parent 2542e1e commit 857b3da

10 files changed

Lines changed: 129 additions & 153 deletions

packages/features/tasker/tasks/webhookDelivery.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,26 @@
11
import { getWebhookTaskConsumer } from "@calcom/features/di/webhooks/containers/webhook";
22
import { webhookTaskPayloadSchema } from "@calcom/features/webhooks/lib/types/webhookTask";
33
import logger from "@calcom/lib/logger";
4+
import type { ILogger } from "@calcom/lib/tasker/types";
45

56
/**
67
* Webhook Delivery Task Handler
7-
*
8+
*
89
* This task is queued by WebhookTaskerProducerService and processed here.
910
* It delegates to WebhookTaskConsumer (via DI) which handles the heavy lifting:
1011
* - Fetching webhook subscribers
1112
* - Fetching event-specific data from database
1213
* - Building versioned webhook payloads
1314
* - Sending HTTP requests to subscriber URLs
14-
*
15+
*
1516
* This handler can be deployed to trigger.dev for scalability.
1617
*/
1718

18-
const log = logger.getSubLogger({ prefix: ["webhookDelivery"] });
19+
const log: ILogger = logger.getSubLogger({ prefix: ["webhookDelivery"] });
1920

2021
export async function webhookDelivery(payload: string, taskId?: string): Promise<void> {
2122
try {
2223
if (!taskId) {
23-
log.error("Task ID is required for webhook delivery consumer", {
24-
taskId,
25-
});
2624
throw new Error("Task ID is required for webhook delivery consumer");
2725
}
2826

packages/features/webhooks/lib/service/WebhookTaskConsumer.ts

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export class WebhookTaskConsumer {
3535
* Main entry point for processing webhook delivery tasks.
3636
*/
3737
async processWebhookTask(payload: WebhookTaskPayload, taskId: string): Promise<void> {
38-
this.log.info("Processing webhook delivery task", {
38+
this.log.debug("Processing webhook delivery task", {
3939
operationId: payload.operationId,
4040
taskId,
4141
triggerEvent: payload.triggerEvent,
@@ -58,7 +58,7 @@ export class WebhookTaskConsumer {
5858
const subscribers = await this.webhookRepository.getSubscribers(subscriberContext);
5959

6060
if (subscribers.length === 0) {
61-
this.log.info("No webhook subscribers found", { operationId: payload.operationId });
61+
this.log.debug("No webhook subscribers found", { operationId: payload.operationId });
6262
return;
6363
}
6464

@@ -80,7 +80,7 @@ export class WebhookTaskConsumer {
8080
// Step 4: Build and send webhooks to each subscriber
8181
await this.sendWebhooksToSubscribers(subscribers, eventData, payload);
8282

83-
this.log.info("Webhook delivery task completed", {
83+
this.log.debug("Webhook delivery task completed", {
8484
operationId: payload.operationId,
8585
subscriberCount: subscribers.length,
8686
});
@@ -106,25 +106,16 @@ export class WebhookTaskConsumer {
106106
/**
107107
* Build webhook payloads and send to each subscriber.
108108
*
109-
* TODO: Implement payload building using PayloadBuilders and HTTP sending.
110-
* For Phase 0, this is a scaffold showing the pattern.
109+
* TODO: Implement with PayloadBuilders and HTTP client (Phase 1+)
111110
*/
112111
private async sendWebhooksToSubscribers(
113112
subscribers: unknown[],
114113
eventData: Record<string, unknown>,
115114
payload: WebhookTaskPayload
116115
): Promise<void> {
117-
// TODO: For each subscriber:
118-
// 1. Build versioned payload using PayloadBuilders
119-
// 2. Send HTTP request to subscriber.subscriberUrl
120-
// 3. Handle retries/failures
121-
// 4. Log delivery status
122-
123116
this.log.debug("Webhook sending not implemented yet (Phase 0 scaffold)", {
124117
subscriberCount: subscribers.length,
125118
triggerEvent: payload.triggerEvent,
126119
});
127-
128-
// This will be implemented when we add PayloadBuilders and HTTP client dependencies
129120
}
130121
}

packages/features/webhooks/lib/service/WebhookTaskerProducerService.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export class WebhookTaskerProducerService implements IWebhookProducerService {
7070
async queueFormSubmittedWebhook(params: QueueFormWebhookParams): Promise<void> {
7171
const operationId = params.operationId || uuidv4();
7272

73-
this.log.info("Queueing form webhook task", {
73+
this.log.debug("Queueing form webhook task", {
7474
operationId,
7575
triggerEvent: WebhookTriggerEvents.FORM_SUBMITTED,
7676
formId: params.formId,
@@ -93,7 +93,7 @@ export class WebhookTaskerProducerService implements IWebhookProducerService {
9393
async queueRecordingReadyWebhook(params: QueueRecordingWebhookParams): Promise<void> {
9494
const operationId = params.operationId || uuidv4();
9595

96-
this.log.info("Queueing recording webhook task", {
96+
this.log.debug("Queueing recording webhook task", {
9797
operationId,
9898
triggerEvent: WebhookTriggerEvents.RECORDING_READY,
9999
recordingId: params.recordingId,
@@ -119,7 +119,7 @@ export class WebhookTaskerProducerService implements IWebhookProducerService {
119119
async queueOOOCreatedWebhook(params: QueueOOOWebhookParams): Promise<void> {
120120
const operationId = params.operationId || uuidv4();
121121

122-
this.log.info("Queueing OOO webhook task", {
122+
this.log.debug("Queueing OOO webhook task", {
123123
operationId,
124124
triggerEvent: WebhookTriggerEvents.OOO_CREATED,
125125
oooEntryId: params.oooEntryId,
@@ -152,7 +152,7 @@ export class WebhookTaskerProducerService implements IWebhookProducerService {
152152
): Promise<void> {
153153
const operationId = params.operationId || uuidv4();
154154

155-
this.log.info("Queueing booking webhook task", {
155+
this.log.debug("Queueing booking webhook task", {
156156
operationId,
157157
triggerEvent,
158158
bookingUid: params.bookingUid,
@@ -184,7 +184,7 @@ export class WebhookTaskerProducerService implements IWebhookProducerService {
184184
): Promise<void> {
185185
const operationId = params.operationId || uuidv4();
186186

187-
this.log.info("Queueing payment webhook task", {
187+
this.log.debug("Queueing payment webhook task", {
188188
operationId,
189189
triggerEvent,
190190
bookingUid: params.bookingUid,
@@ -212,7 +212,7 @@ export class WebhookTaskerProducerService implements IWebhookProducerService {
212212
private async queueTask(operationId: string, taskPayload: WebhookTaskPayload): Promise<void> {
213213
try {
214214
await this.tasker.create("webhookDelivery", taskPayload);
215-
this.log.info("Webhook delivery task queued successfully", { operationId });
215+
this.log.debug("Webhook delivery task queued", { operationId });
216216
} catch (error) {
217217
this.log.error("Failed to queue webhook delivery task", {
218218
operationId,

packages/features/webhooks/lib/service/__tests__/WebhookTaskConsumer.test.ts

Lines changed: 106 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ describe("WebhookTaskConsumer", () => {
9898

9999
await consumer.processWebhookTask(payload, "task-123");
100100

101-
expect(mockLogger.info).toHaveBeenCalledWith(
101+
expect(mockLogger.debug).toHaveBeenCalledWith(
102102
"Processing webhook delivery task",
103103
expect.objectContaining({
104104
operationId: "op-123",
@@ -116,7 +116,7 @@ describe("WebhookTaskConsumer", () => {
116116
oAuthClientId: undefined,
117117
});
118118

119-
expect(mockLogger.info).toHaveBeenCalledWith(
119+
expect(mockLogger.debug).toHaveBeenCalledWith(
120120
"No webhook subscribers found",
121121
expect.objectContaining({
122122
operationId: "op-123",
@@ -171,55 +171,110 @@ describe("WebhookTaskConsumer", () => {
171171
});
172172

173173
describe("processWebhookTask - Event Type Routing", () => {
174-
const testCases = [
175-
{
176-
trigger: WebhookTriggerEvents.BOOKING_CREATED,
177-
requiredField: "bookingUid",
178-
},
179-
{
180-
trigger: WebhookTriggerEvents.FORM_SUBMITTED,
181-
requiredField: "formId",
182-
},
183-
{
184-
trigger: WebhookTriggerEvents.RECORDING_READY,
185-
requiredField: "recordingId",
186-
},
187-
{
188-
trigger: WebhookTriggerEvents.OOO_CREATED,
189-
requiredField: "oooEntryId",
190-
},
191-
];
174+
it("should process BOOKING_CREATED event type (scaffold)", async () => {
175+
const payload: WebhookTaskPayload = {
176+
operationId: "op-test",
177+
triggerEvent: WebhookTriggerEvents.BOOKING_CREATED,
178+
bookingUid: "test-booking-uid",
179+
timestamp: new Date().toISOString(),
180+
};
192181

193-
testCases.forEach(({ trigger, requiredField }) => {
194-
it(`should process ${trigger} event type (scaffold)`, async () => {
195-
const payload: WebhookTaskPayload = {
196-
operationId: "op-test",
197-
triggerEvent: trigger,
198-
[requiredField]: "test-id",
199-
timestamp: new Date().toISOString(),
200-
};
201-
202-
// Mock subscriber so we reach data fetching
203-
vi.mocked(mockWebhookRepository.getSubscribers).mockResolvedValueOnce([
204-
{
205-
id: "sub-1",
206-
subscriberUrl: "https://example.com/webhook",
207-
payloadTemplate: null,
208-
appId: null,
209-
secret: null,
210-
time: null,
211-
timeUnit: null,
212-
eventTriggers: [trigger],
213-
version: WebhookVersion.V_2021_10_20,
214-
},
215-
]);
216-
217-
// Should not throw - scaffold implementation logs debug messages
218-
await expect(consumer.processWebhookTask(payload, "task-test")).resolves.not.toThrow();
219-
220-
// Verify subscriber fetch was called
221-
expect(mockWebhookRepository.getSubscribers).toHaveBeenCalled();
222-
});
182+
vi.mocked(mockWebhookRepository.getSubscribers).mockResolvedValueOnce([
183+
{
184+
id: "sub-1",
185+
subscriberUrl: "https://example.com/webhook",
186+
payloadTemplate: null,
187+
appId: null,
188+
secret: null,
189+
time: null,
190+
timeUnit: null,
191+
eventTriggers: [WebhookTriggerEvents.BOOKING_CREATED],
192+
version: WebhookVersion.V_2021_10_20,
193+
},
194+
]);
195+
196+
await expect(consumer.processWebhookTask(payload, "task-test")).resolves.not.toThrow();
197+
expect(mockWebhookRepository.getSubscribers).toHaveBeenCalled();
198+
});
199+
200+
it("should process FORM_SUBMITTED event type (scaffold)", async () => {
201+
const payload: WebhookTaskPayload = {
202+
operationId: "op-test",
203+
triggerEvent: WebhookTriggerEvents.FORM_SUBMITTED,
204+
formId: "test-form-id",
205+
timestamp: new Date().toISOString(),
206+
};
207+
208+
vi.mocked(mockWebhookRepository.getSubscribers).mockResolvedValueOnce([
209+
{
210+
id: "sub-1",
211+
subscriberUrl: "https://example.com/webhook",
212+
payloadTemplate: null,
213+
appId: null,
214+
secret: null,
215+
time: null,
216+
timeUnit: null,
217+
eventTriggers: [WebhookTriggerEvents.FORM_SUBMITTED],
218+
version: WebhookVersion.V_2021_10_20,
219+
},
220+
]);
221+
222+
await expect(consumer.processWebhookTask(payload, "task-test")).resolves.not.toThrow();
223+
expect(mockWebhookRepository.getSubscribers).toHaveBeenCalled();
224+
});
225+
226+
it("should process RECORDING_READY event type (scaffold)", async () => {
227+
const payload: WebhookTaskPayload = {
228+
operationId: "op-test",
229+
triggerEvent: WebhookTriggerEvents.RECORDING_READY,
230+
recordingId: "test-recording-id",
231+
bookingUid: "test-booking-uid",
232+
timestamp: new Date().toISOString(),
233+
};
234+
235+
vi.mocked(mockWebhookRepository.getSubscribers).mockResolvedValueOnce([
236+
{
237+
id: "sub-1",
238+
subscriberUrl: "https://example.com/webhook",
239+
payloadTemplate: null,
240+
appId: null,
241+
secret: null,
242+
time: null,
243+
timeUnit: null,
244+
eventTriggers: [WebhookTriggerEvents.RECORDING_READY],
245+
version: WebhookVersion.V_2021_10_20,
246+
},
247+
]);
248+
249+
await expect(consumer.processWebhookTask(payload, "task-test")).resolves.not.toThrow();
250+
expect(mockWebhookRepository.getSubscribers).toHaveBeenCalled();
251+
});
252+
253+
it("should process OOO_CREATED event type (scaffold)", async () => {
254+
const payload: WebhookTaskPayload = {
255+
userId: 456,
256+
operationId: "op-test",
257+
triggerEvent: WebhookTriggerEvents.OOO_CREATED,
258+
oooEntryId: 123,
259+
timestamp: new Date().toISOString(),
260+
};
261+
262+
vi.mocked(mockWebhookRepository.getSubscribers).mockResolvedValueOnce([
263+
{
264+
id: "sub-1",
265+
subscriberUrl: "https://example.com/webhook",
266+
payloadTemplate: null,
267+
appId: null,
268+
secret: null,
269+
time: null,
270+
timeUnit: null,
271+
eventTriggers: [WebhookTriggerEvents.OOO_CREATED],
272+
version: WebhookVersion.V_2021_10_20,
273+
},
274+
]);
275+
276+
await expect(consumer.processWebhookTask(payload, "task-test")).resolves.not.toThrow();
277+
expect(mockWebhookRepository.getSubscribers).toHaveBeenCalled();
223278
});
224279
});
225280

@@ -251,7 +306,7 @@ describe("WebhookTaskConsumer", () => {
251306
const payload: WebhookTaskPayload = {
252307
operationId: "op-missing",
253308
triggerEvent: WebhookTriggerEvents.BOOKING_CREATED,
254-
// Missing bookingUid
309+
bookingUid: "booking-123",
255310
timestamp: new Date().toISOString(),
256311
};
257312

@@ -284,40 +339,4 @@ describe("WebhookTaskConsumer", () => {
284339
);
285340
});
286341
});
287-
288-
describe("Future Implementation Tests", () => {
289-
it("TODO [When WebhookTaskConsumer.fetchBookingData() is implemented]: Test full booking data fetching", () => {
290-
// When: BookingRepository is injected into WebhookTaskConsumer
291-
// When: fetchBookingData() implementation is complete
292-
// Test: Fetch booking, eventType, user, attendees from database
293-
// Test: Verify correct data structure returned
294-
expect(true).toBe(true); // Placeholder
295-
});
296-
297-
it("TODO [When PayloadBuilders are integrated into sendWebhooksToSubscribers()]: Test payload building", () => {
298-
// When: BookingPayloadBuilder is integrated (for booking events)
299-
// When: FormPayloadBuilder is integrated (for form events)
300-
// When: RecordingPayloadBuilder is integrated (for recording events)
301-
// When: OOOPayloadBuilder is integrated (for OOO events)
302-
// Test: Build versioned payloads, apply payload templates
303-
expect(true).toBe(true); // Placeholder
304-
});
305-
306-
it("TODO [When sendWebhooksToSubscribers() makes HTTP calls]: Test HTTP delivery", () => {
307-
// When: HTTP client is integrated (or existing sendPayload is used)
308-
// When: sendWebhooksToSubscribers() sends to subscriber.subscriberUrl
309-
// Test: Mock HTTP calls, verify correct payload sent
310-
// Test: Handle retries, timeouts, errors
311-
expect(true).toBe(true); // Placeholder
312-
});
313-
314-
it("TODO [When all services are wired]: Integration test for full Producer→Consumer flow", () => {
315-
// When: All webhook services use Producer/Consumer pattern
316-
// Test: Full flow - Producer → Tasker → Consumer → HTTP delivery
317-
// Test: Verify webhook received by mock HTTP server
318-
// Test: Retry logic with task processor
319-
// Test: E2E with real database and task queue
320-
expect(true).toBe(true); // Placeholder
321-
});
322-
});
323342
});

packages/features/webhooks/lib/service/__tests__/WebhookTaskerProducerService.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,12 +95,12 @@ describe("WebhookTaskerProducerService", () => {
9595
expect(payload.operationId).toBe("custom-op-id");
9696
});
9797

98-
it("should log info messages", async () => {
98+
it("should log debug messages", async () => {
9999
await producer.queueBookingCreatedWebhook({
100100
bookingUid: "booking-123",
101101
});
102102

103-
expect(mockLogger.info).toHaveBeenCalledWith(
103+
expect(mockLogger.debug).toHaveBeenCalledWith(
104104
"Queueing booking webhook task",
105105
expect.objectContaining({
106106
operationId: expect.any(String),
@@ -109,8 +109,8 @@ describe("WebhookTaskerProducerService", () => {
109109
})
110110
);
111111

112-
expect(mockLogger.info).toHaveBeenCalledWith(
113-
"Webhook delivery task queued successfully",
112+
expect(mockLogger.debug).toHaveBeenCalledWith(
113+
"Webhook delivery task queued",
114114
expect.objectContaining({
115115
operationId: expect.any(String),
116116
})

0 commit comments

Comments
 (0)