Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/features/bookings/lib/dto/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,8 @@ export type BookingHandlerInput = {
export type RegularBookingCreateResult = Awaited<ReturnType<RegularBookingService["createBooking"]>>;

export type InstantBookingCreateResult = Awaited<ReturnType<InstantBookingCreateService["create"]>>;

// More properties to be added to this config in followup PRs
export type BookingFlowConfig = {
isDryRun: boolean;
};
23 changes: 23 additions & 0 deletions packages/features/bookings/lib/messageBus/BookingMessageBus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { BaseMessageBus } from "@calcom/lib/messageBus/MessageBus";
import type { IMessageHandler, Message } from "@calcom/lib/messageBus/types";

import type { BookingMessage, BookingMessagePayloadMap } from "./types.d";

export type { BookingCreatedMessagePayload, BookingRescheduledMessagePayload } from "./types.d";
export { BOOKING_MESSAGES } from "./types.d";

export type { BookingMessage };

// Booking-specific message handler interface
export interface IBookingMessageHandler<T extends BookingMessage>
extends IMessageHandler<T, BookingMessagePayloadMap> {
subscribedMessage: T;
handle(message: Message<BookingMessagePayloadMap[T]>): Promise<void>;
isEnabled?(message: Message<BookingMessagePayloadMap[T]>): boolean;
}

export class BookingMessageBus extends BaseMessageBus<BookingMessage, BookingMessagePayloadMap> {
constructor() {
super(["BookingMessageBus"]);
}
}
14 changes: 14 additions & 0 deletions packages/features/bookings/lib/messageBus/registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { subscriptions as privateLinkSubscriptions } from "@calcom/features/privateLink/messages/bookingMessageBusRegistration";
import { subscriptions as jobsSubscriptions } from "@calcom/jobs/messages/messageBusRegistration";

import type { BookingMessageBus } from "./BookingMessageBus";

export function registerBookingMessageHandlers(messageBus: BookingMessageBus): void {
privateLinkSubscriptions.forEach((subscription) => {
messageBus.subscribe(subscription);
});

jobsSubscriptions.forEach((subscription) => {
messageBus.subscribe(subscription);
});
}
126 changes: 126 additions & 0 deletions packages/features/bookings/lib/messageBus/types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import type { Message } from "@calcom/lib/messageBus/MessageBus";
import type { BookingStatus, SchedulingType } from "@calcom/prisma/enums";

import type { BookingFlowConfig } from "../dto/types";

type CreatedBooking = {
uid: string;
userId: number | null;
status: BookingStatus;
startTime: Date;
endTime: Date;
location: string | null;
};

type BookingEventType = {
id: number;
slug: string;
schedulingType: SchedulingType | null;
metadata: Record<string, unknown> | null;
hashedLink?: string;
};

export interface BookingCreationPayload {
booking: CreatedBooking;
eventType: BookingEventType;
config: BookingFlowConfig;
}

export type BookingCreatedMessagePayload = BookingCreationPayload;

export interface BookingRescheduledMessagePayload extends BookingCreatedMessagePayload {
reschedule: {
originalBooking: {
uid: string;
};
rescheduleReason: string | null;
rescheduledBy: string | null;
};
}

export type BookingCreatedMessage = Message<BookingCreatedMessagePayload>;

export type BookingRescheduledMessage = Message<BookingRescheduledMessagePayload>;

// export interface BookingRequestedMessagePayload extends BookingCreationPayload {
// // Core booking information
// booking: {
// id: number;
// uid: string;
// userId: number;
// status: BookingStatus;
// startTime: Date;
// endTime: Date;
// location: string | null;
// metadata: Record<string, any> | null;
// eventTypeId: number | null;
// };

// eventType: {
// id: number;
// slug: string;
// schedulingType: string | null;
// hosts: any[];
// metadata: Record<string, any> | null;
// isTeamEventType?: boolean;
// teamId?: number | null;
// hashedLink?: string;
// seatsPerTimeSlot?: number | null;
// };

// calendarEvent: CalendarEvent;

// // Platform and execution context
// context: {
// platformClientId?: string;
// noEmail?: boolean;
// isDryRun: boolean;
// };

// // User and organization context
// organizer: {
// id: number;
// };

// // Integration and platform data
// integrations?: {
// credentials?: any[];
// };

// // Form submission and user input data
// formData?: any;

// // Calendar and conferencing app sync data from EventManager
// appSync?: {
// additionalInformation?: any;
// };

// // Routing and assignment specific data
// routing?: {
// contactOwnerEmail?: string | null;
// routingFormResponseId?: number;
// crmRecordId?: string;
// reroutingFormResponses?: any;
// assignmentReason?: {
// reasonEnum: AssignmentReasonEnum;
// reasonString: string;
// };
// };
// }

export const BOOKING_MESSAGES = {
BOOKING_CREATED: "booking.created",
BOOKING_RESCHEDULED: "booking.rescheduled",
BOOKING_REQUESTED: "booking.requested",
} as const;

export type BookingMessage = (typeof BOOKING_MESSAGES)[keyof typeof BOOKING_MESSAGES];

// Placeholder type for booking requested - to be implemented
export type BookingRequestedMessagePayload = BookingCreationPayload;

export interface BookingMessagePayloadMap {
[BOOKING_MESSAGES.BOOKING_CREATED]: BookingCreatedMessagePayload;
[BOOKING_MESSAGES.BOOKING_RESCHEDULED]: BookingRescheduledMessagePayload;
[BOOKING_MESSAGES.BOOKING_REQUESTED]: BookingRequestedMessagePayload;
}
65 changes: 38 additions & 27 deletions packages/features/bookings/lib/service/RegularBookingService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ import type { CheckBookingAndDurationLimitsService } from "@calcom/features/book
import { handlePayment } from "@calcom/features/bookings/lib/handlePayment";
import { handleWebhookTrigger } from "@calcom/features/bookings/lib/handleWebhookTrigger";
import { isEventTypeLoggingEnabled } from "@calcom/features/bookings/lib/isEventTypeLoggingEnabled";
// TODO: Must be injected from DI
import { BOOKING_MESSAGES } from "@calcom/features/bookings/lib/messageBus/BookingMessageBus";
import type { CacheService } from "@calcom/features/calendar-cache/lib/getShouldServeCache";
import AssignmentReasonRecorder from "@calcom/features/ee/round-robin/assignmentReason/AssignmentReasonRecorder";
import { getUsernameList } from "@calcom/features/eventtypes/lib/defaultEvents";
Expand Down Expand Up @@ -67,13 +69,8 @@ import { getPiiFreeCalendarEvent, getPiiFreeEventType } from "@calcom/lib/piiFre
import { safeStringify } from "@calcom/lib/safeStringify";
import type { LuckyUserService } from "@calcom/lib/server/getLuckyUser";
import { getTranslation } from "@calcom/lib/server/i18n";
import type { PrismaAttributeRepository as AttributeRepository } from "@calcom/lib/server/repository/PrismaAttributeRepository";
import type { BookingRepository } from "@calcom/lib/server/repository/booking";
import type { HostRepository } from "@calcom/lib/server/repository/host";
import type { PrismaOOORepository as OooRepository } from "@calcom/lib/server/repository/ooo";
import type { UserRepository } from "@calcom/lib/server/repository/user";
import { WorkflowRepository } from "@calcom/lib/server/repository/workflow";
import { HashedLinkService } from "@calcom/lib/server/service/hashedLinkService";
import { WorkflowService } from "@calcom/lib/server/service/workflows";
import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
import type { PrismaClient } from "@calcom/prisma";
Expand Down Expand Up @@ -126,6 +123,7 @@ import { validateBookingTimeIsNotOutOfBounds } from "../handleNewBooking/validat
import { validateEventLength } from "../handleNewBooking/validateEventLength";
import handleSeats from "../handleSeats/handleSeats";
import type { IBookingService } from "../interfaces/IBookingService";
import { registerBookingMessageHandlers } from "../messageBus/registry";

const translator = short();
const log = logger.getSubLogger({ prefix: ["[api] book:user"] });
Expand Down Expand Up @@ -419,10 +417,7 @@ export interface IBookingServiceDependencies {
featuresRepository: FeaturesRepository;
checkBookingLimitsService: CheckBookingLimitsService;
luckyUserService: LuckyUserService;
hostRepository: HostRepository;
oooRepository: OooRepository;
userRepository: UserRepository;
attributeRepository: AttributeRepository;
bookingMessageBus: BookingMessageBus;
}

async function handler(
Expand All @@ -449,6 +444,7 @@ async function handler(
cacheService,
checkBookingAndDurationLimitsService,
luckyUserService,
bookingMessageBus,
} = deps;

const isPlatformBooking = !!platformClientId;
Expand Down Expand Up @@ -2045,6 +2041,36 @@ async function handler(
}
: undefined;

const bookingFlowConfig = {
isDryRun,
};

const bookingCreationPayload = {
booking,
eventType,
config: bookingFlowConfig,
};

if (originalRescheduledBooking) {
await bookingMessageBus.emit(BOOKING_MESSAGES.BOOKING_RESCHEDULED, {
...bookingCreationPayload,
reschedule: {
originalBooking: {
uid: originalRescheduledBooking.uid,
},
rescheduleReason,
rescheduledBy: reqBody.rescheduledBy,
},
});
} else {
console.log("RegularBookingService emit BOOKING_CREATED", {
bookingCreationPayload,
});
await bookingMessageBus.emit(BOOKING_MESSAGES.BOOKING_CREATED, {
...bookingCreationPayload,
});
}

const webhookData: EventPayloadType = {
...evt,
...eventTypeInfo,
Expand Down Expand Up @@ -2282,23 +2308,6 @@ async function handler(
});
}

try {
const hashedLinkService = new HashedLinkService();
if (hasHashedBookingLink && reqBody.hashedLink && !isDryRun) {
await hashedLinkService.validateAndIncrementUsage(reqBody.hashedLink as string);
}
} catch (error) {
loggerWithEventDetails.error("Error while updating hashed link", JSON.stringify({ error }));

// Handle repository errors and convert to HttpErrors
if (error instanceof Error) {
throw new HttpError({ statusCode: 410, message: error.message });
}

// For unexpected errors, provide a generic message
throw new HttpError({ statusCode: 500, message: "Failed to process booking link" });
}

if (!booking) throw new HttpError({ statusCode: 400, message: "Booking failed" });

try {
Expand Down Expand Up @@ -2420,7 +2429,9 @@ async function handler(
* We are open to renaming it to something more descriptive.
*/
export class RegularBookingService implements IBookingService {
constructor(private readonly deps: IBookingServiceDependencies) {}
constructor(private readonly deps: IBookingServiceDependencies) {
registerBookingMessageHandlers(this.deps.bookingMessageBus);
}

async createBooking(input: { bookingData: CreateRegularBookingData; bookingMeta?: CreateBookingMeta }) {
return handler({ bookingData: input.bookingData, ...input.bookingMeta }, this.deps);
Expand Down
43 changes: 43 additions & 0 deletions packages/features/privateLink/lib/updateHashedLinkUsage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { Logger } from "tslog";

import { safeStringify } from "@calcom/lib/safeStringify";
import { HashedLinkService } from "@calcom/lib/server/service/hashedLinkService";

// I am passing deps as an argument instead of having this module as a class and passing constructor dependencies because this is a very simple fn and having a class would be overkill.
/**
* Updates the usage of a hashed link for a booking
* @param hashedLink - The hashed link to update the usage of
* @param bookingUid - The UID of the booking to update the usage of
* @param deps - The dependencies to use
* @returns void
*/
export const updateHashedLinkUsage = async (
{
hashedLink,
bookingUid,
}: {
hashedLink: string;
bookingUid: string;
},
deps: {
log: Logger<unknown>;
}
) => {
const { log } = deps;
try {
const hashedLinkService = new HashedLinkService();
await hashedLinkService.validateAndIncrementUsage(hashedLink);

log.debug(`Successfully updated hashed link usage for booking ${bookingUid}`);
} catch (error) {
log.error(
"Error while updating hashed link",
safeStringify(error),
safeStringify({
error,
bookingUid,
hashedLink,
})
);
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { BOOKING_MESSAGES } from "@calcom/features/bookings/lib/messageBus/BookingMessageBus";
import type {
IBookingMessageHandler,
BookingCreatedMessagePayload,
} from "@calcom/features/bookings/lib/messageBus/BookingMessageBus";
import logger from "@calcom/lib/logger";
import type { Message } from "@calcom/lib/messageBus/types";

import { updateHashedLinkUsage } from "../../lib/updateHashedLinkUsage";

const log = logger.getSubLogger({ prefix: ["HashedLinkHandler"] });
export class MessageBookingCreatedPrivateLinkHandler
implements IBookingMessageHandler<typeof BOOKING_MESSAGES.BOOKING_CREATED>
{
subscribedMessage = BOOKING_MESSAGES.BOOKING_CREATED;

isEnabled(message: Message<BookingCreatedMessagePayload>): boolean {
console.log("MessageBookingCreatedPrivateLinkHandler isEnabled - Checking", {
config: message.payload.config,
});
return !message.payload.config.isDryRun;
}

async handle(message: Message<BookingCreatedMessagePayload>): Promise<void> {
console.log("MessageBookingCreatedPrivateLinkHandler handle - Handling", {
payload: message.payload,
});
const { payload } = message;
if (!payload.eventType.hashedLink) {
return;
}
await updateHashedLinkUsage(
{
hashedLink: payload.eventType.hashedLink,
bookingUid: payload.booking.uid,
},
{
log,
}
);
}
}
Loading