Skip to content

Commit 5d65df9

Browse files
alishaz-polymathdevin-ai-integration[bot]hariombalhara
authored
chore: migrate booking requested webhook trigger (calcom#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>
1 parent 66ce202 commit 5d65df9

39 files changed

Lines changed: 2725 additions & 1639 deletions

apps/api/v2/src/lib/modules/regular-booking.module.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ import { TaskerService } from "@/lib/services/tasker.service";
2121
import { PrismaModule } from "@/modules/prisma/prisma.module";
2222
import { Module, Scope } from "@nestjs/common";
2323

24+
import { getWebhookProducer } from "@calcom/platform-libraries/bookings";
25+
26+
import { WEBHOOK_PRODUCER } from "./regular-booking.tokens";
27+
2428
@Module({
2529
imports: [PrismaModule],
2630
providers: [
@@ -37,6 +41,10 @@ import { Module, Scope } from "@nestjs/common";
3741
},
3842
scope: Scope.TRANSIENT,
3943
},
44+
{
45+
provide: WEBHOOK_PRODUCER,
46+
useFactory: () => getWebhookProducer(),
47+
},
4048
BookingAuditProducerService,
4149
BookingEventHandlerService,
4250
CheckBookingAndDurationLimitsService,
@@ -51,6 +59,6 @@ import { Module, Scope } from "@nestjs/common";
5159
TaskerService,
5260
RegularBookingService,
5361
],
54-
exports: [RegularBookingService],
62+
exports: [RegularBookingService, WEBHOOK_PRODUCER],
5563
})
5664
export class RegularBookingModule {}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/**
2+
* Injection token for IWebhookProducerService.
3+
* Used to bridge the Cal.com DI container into NestJS.
4+
*
5+
* Defined in a separate file to avoid a circular import between
6+
* regular-booking.module.ts and regular-booking.service.ts.
7+
*/
8+
export const WEBHOOK_PRODUCER = "WEBHOOK_PRODUCER";

apps/api/v2/src/lib/services/regular-booking.service.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
import {
2+
RegularBookingService as BaseRegularBookingService,
3+
type IWebhookProducerService,
4+
} from "@calcom/platform-libraries/bookings";
5+
import type { PrismaClient } from "@calcom/prisma";
6+
import { Inject, Injectable } from "@nestjs/common";
7+
import { WEBHOOK_PRODUCER } from "@/lib/modules/regular-booking.tokens";
18
import { PrismaBookingRepository } from "@/lib/repositories/prisma-booking.repository";
29
import { PrismaFeaturesRepository } from "@/lib/repositories/prisma-features.repository";
310
import { PrismaUserRepository } from "@/lib/repositories/prisma-user.repository";
@@ -7,10 +14,6 @@ import { HashedLinkService } from "@/lib/services/hashed-link.service";
714
import { LuckyUserService } from "@/lib/services/lucky-user.service";
815
import { BookingEmailAndSmsTasker } from "@/lib/services/tasker/booking-emails-sms-tasker.service";
916
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
10-
import { Injectable } from "@nestjs/common";
11-
12-
import { RegularBookingService as BaseRegularBookingService } from "@calcom/platform-libraries/bookings";
13-
import type { PrismaClient } from "@calcom/prisma";
1417

1518
@Injectable()
1619
export class RegularBookingService extends BaseRegularBookingService {
@@ -23,7 +26,8 @@ export class RegularBookingService extends BaseRegularBookingService {
2326
userRepository: PrismaUserRepository,
2427
bookingEmailAndSmsTasker: BookingEmailAndSmsTasker,
2528
featuresRepository: PrismaFeaturesRepository,
26-
bookingEventHandler: BookingEventHandlerService
29+
bookingEventHandler: BookingEventHandlerService,
30+
@Inject(WEBHOOK_PRODUCER) webhookProducer: IWebhookProducerService
2731
) {
2832
super({
2933
checkBookingAndDurationLimitsService,
@@ -35,6 +39,7 @@ export class RegularBookingService extends BaseRegularBookingService {
3539
bookingEmailAndSmsTasker,
3640
featuresRepository,
3741
bookingEventHandler,
42+
webhookProducer,
3843
});
3944
}
4045
}

apps/api/v2/src/lib/services/tasker.service.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1+
import { getTasker, type Tasker } from "@calcom/platform-libraries/tasker";
12
import { Injectable } from "@nestjs/common";
23

3-
import { getTasker, type Tasker } from "@calcom/platform-libraries";
4-
54
@Injectable()
65
export class TaskerService {
76
private readonly tasker: Tasker;
@@ -14,5 +13,3 @@ export class TaskerService {
1413
return this.tasker;
1514
}
1615
}
17-
18-

apps/web/playwright/webhook.e2e.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
import { expect } from "@playwright/test";
2-
import { v4 as uuidv4 } from "uuid";
3-
41
import dayjs from "@calcom/dayjs";
52
import prisma from "@calcom/prisma";
63
import { BookingStatus } from "@calcom/prisma/enums";
7-
4+
import { expect } from "@playwright/test";
5+
import { v4 as uuidv4 } from "uuid";
86
import { test } from "./lib/fixtures";
97
import {
108
bookOptinEvent,

packages/app-store/_utils/payments/handlePaymentSuccess.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,7 @@ export async function handlePaymentSuccess(params: {
257257
await handleBookingRequested({
258258
evt,
259259
booking,
260+
oAuthClientId: platformClientParams?.platformClientId,
260261
});
261262
log.debug(`handling booking request for eventId ${eventType.id}`);
262263
}

packages/features/bookings/di/RegularBookingService.module.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import { moduleLoader as luckyUserServiceModuleLoader } from "@calcom/features/d
88
import { moduleLoader as prismaModuleLoader } from "@calcom/features/di/modules/Prisma";
99
import { moduleLoader as userRepositoryModuleLoader } from "@calcom/features/di/modules/User";
1010
import { DI_TOKENS } from "@calcom/features/di/tokens";
11+
import { moduleLoader as webhookProducerModuleLoader } from "@calcom/features/di/webhooks/modules/WebhookProducerService.module";
1112
import { moduleLoader as hashedLinkServiceModuleLoader } from "@calcom/features/hashedLink/di/HashedLinkService.module";
12-
1313
import { moduleLoader as bookingEmailAndSmsTaskerModuleLoader } from "./tasker/BookingEmailAndSmsTasker.module";
1414

1515
const thisModule = createModule();
@@ -31,6 +31,7 @@ const loadModule = bindModuleToClassOnToken({
3131
bookingEmailAndSmsTasker: bookingEmailAndSmsTaskerModuleLoader,
3232
featuresRepository: featuresRepositoryModuleLoader,
3333
bookingEventHandler: bookingEventHandlerModuleLoader,
34+
webhookProducer: webhookProducerModuleLoader,
3435
},
3536
});
3637

packages/features/bookings/lib/handleBookingRequested.ts

Lines changed: 24 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
11
import { sendAttendeeRequestEmailAndSMS, sendOrganizerRequestEmail } from "@calcom/emails/email-manager";
2-
import { getWebhookPayloadForBooking } from "@calcom/features/bookings/lib/getWebhookPayloadForBooking";
2+
import { getWebhookProducer } from "@calcom/features/di/webhooks/containers/webhook";
33
import { CreditService } from "@calcom/features/ee/billing/credit-service";
44
import { getAllWorkflowsFromEventType } from "@calcom/features/ee/workflows/lib/getAllWorkflowsFromEventType";
55
import { WorkflowService } from "@calcom/features/ee/workflows/lib/service/WorkflowService";
66
import type { Workflow } from "@calcom/features/ee/workflows/lib/types";
7-
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
8-
import sendPayload from "@calcom/features/webhooks/lib/sendOrSchedulePayload";
97
import getOrgIdFromMemberOrTeamId from "@calcom/lib/getOrgIdFromMemberOrTeamId";
108
import logger from "@calcom/lib/logger";
119
import { safeStringify } from "@calcom/lib/safeStringify";
1210
import type { Prisma } from "@calcom/prisma/client";
13-
import { WebhookTriggerEvents, WorkflowTriggerEvents } from "@calcom/prisma/enums";
11+
import { WorkflowTriggerEvents } from "@calcom/prisma/enums";
1412
import type { EventTypeMetadata } from "@calcom/prisma/zod-utils";
1513
import type { CalendarEvent } from "@calcom/types/Calendar";
1614

@@ -21,6 +19,8 @@ const log = logger.getSubLogger({ prefix: ["[handleBookingRequested] book:user"]
2119
*/
2220
export async function handleBookingRequested(args: {
2321
evt: CalendarEvent;
22+
/** When booking is from a platform/OAuth client, pass so platform webhook subscribers are notified */
23+
oAuthClientId?: string | null;
2424
booking: {
2525
smsReminderNumber: string | null;
2626
eventType: {
@@ -56,7 +56,7 @@ export async function handleBookingRequested(args: {
5656
id: number;
5757
};
5858
}) {
59-
const { evt, booking } = args;
59+
const { evt, booking, oAuthClientId } = args;
6060

6161
log.debug("Emails: Sending booking requested emails");
6262

@@ -73,34 +73,25 @@ export async function handleBookingRequested(args: {
7373
});
7474

7575
try {
76-
const subscribersBookingRequested = await getWebhooks({
77-
userId: booking.userId,
78-
eventTypeId: booking.eventTypeId,
79-
triggerEvent: WebhookTriggerEvents.BOOKING_REQUESTED,
80-
teamId: booking.eventType?.teamId,
81-
orgId,
82-
});
83-
84-
const webhookPayload = getWebhookPayloadForBooking({
85-
booking,
86-
evt,
87-
});
88-
89-
const promises = subscribersBookingRequested.map((sub) =>
90-
sendPayload(
91-
sub.secret,
92-
WebhookTriggerEvents.BOOKING_REQUESTED,
93-
new Date().toISOString(),
94-
sub,
95-
webhookPayload
96-
).catch((e) => {
97-
log.error(
98-
`Error executing webhook for event: ${WebhookTriggerEvents.BOOKING_REQUESTED}, URL: ${sub.subscriberUrl}, bookingId: ${evt.bookingId}, bookingUid: ${evt.uid}`,
99-
safeStringify(e)
100-
);
101-
})
102-
);
103-
await Promise.all(promises);
76+
if (!evt.uid) {
77+
log.error("Cannot queue BOOKING_REQUESTED webhook: missing booking uid");
78+
} else {
79+
try {
80+
// Keep params in sync with RegularBookingService (non-payment path) so
81+
// subscriber filtering (userId, eventTypeId, teamId, orgId, oAuthClientId) is consistent.
82+
const webhookProducer = getWebhookProducer();
83+
await webhookProducer.queueBookingRequestedWebhook({
84+
bookingUid: evt.uid,
85+
userId: booking.userId ?? undefined,
86+
eventTypeId: booking.eventTypeId ?? undefined,
87+
teamId: booking.eventType?.teamId ?? undefined,
88+
orgId,
89+
oAuthClientId: oAuthClientId ?? undefined,
90+
});
91+
} catch (error) {
92+
log.error("Error queueing BOOKING_REQUESTED webhook", safeStringify(error));
93+
}
94+
}
10495

10596
const workflows = await getAllWorkflowsFromEventType(booking.eventType, booking.userId);
10697
if (workflows.length > 0) {

0 commit comments

Comments
 (0)