Files
calendar/packages/features/bookings/lib/bookingCreateBodySchema.ts
T
MorganGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
6b62557a92 feat: add hostSubsetIds parameter for round robin host filtering (#25627)
* feat: add hostSubsetIds parameter for round robin host filtering

Add support for filtering round robin event type hosts via API v2.
When hostSubsetIds is provided, only the specified hosts are considered
for availability calculation and booking assignment.

Changes:
- Add hostSubsetIds to slots API input (GET /slots/available)
- Add hostSubsetIds to booking API input (POST /bookings)
- Update _findQualifiedHostsWithDelegationCredentials to filter by hostSubsetIds
- Pass hostSubsetIds through all layers: API -> tRPC -> slots/booking services

This allows API consumers to request availability and create bookings
for a subset of hosts within a round robin event type.

Co-Authored-By: morgan@cal.com <morgan@cal.com>

* chore: add e2e tests

* chore: add enableHostSubset team event-type setting

* fixup! chore: add enableHostSubset team event-type setting

* fix tests

* fix tests

* improve isWithinRRHostSubset

* rename to rrHost subset

* fix ai review

* fix: add booker platform wrapper rrHostSubsetIds prop

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-12-10 10:37:39 +00:00

117 lines
4.2 KiB
TypeScript

// `responses` is merged with it during handleNewBooking call because `responses` schema is dynamic and depends on eventType
import z from "zod";
import { routingFormResponseInDbSchema } from "@calcom/app-store/routing-forms/zod";
import { timeZoneSchema } from "@calcom/lib/dayjs/timeZone.schema";
import { CreationSource } from "@calcom/prisma/enums";
export const bookingCreateBodySchema = z.object({
end: z.string().optional(),
eventTypeId: z.number(),
eventTypeSlug: z.string().optional(),
rescheduleUid: z.string().optional(),
recurringEventId: z.string().optional(),
rescheduledBy: z.string().email({ message: "Invalid email" }).optional(),
start: z.string(),
timeZone: timeZoneSchema,
user: z.union([z.string(), z.array(z.string())]).optional(),
language: z.string(),
bookingUid: z.string().optional(),
metadata: z.record(z.string()),
hasHashedBookingLink: z.boolean().optional(),
hashedLink: z.string().nullish(),
seatReferenceUid: z.string().optional(),
orgSlug: z.string().optional(),
teamMemberEmail: z.string().nullish(),
crmOwnerRecordType: z.string().nullish(),
crmRecordId: z.string().nullish(),
routedTeamMemberIds: z.array(z.number()).nullish(),
routingFormResponseId: z.number().optional(),
skipContactOwner: z.boolean().optional(),
rrHostSubsetIds: z.array(z.number()).nullish(),
crmAppSlug: z.string().nullish().optional(),
cfToken: z.string().nullish().optional(),
/**
* Holds the corrected responses of the Form for a booking, provided during rerouting
*/
reroutingFormResponses: routingFormResponseInDbSchema.optional(),
/**
* Used to identify if the booking is a dry run.
*/
_isDryRun: z.boolean().optional(),
tracking: z
.object({
utm_source: z.string().optional(),
utm_medium: z.string().optional(),
utm_campaign: z.string().optional(),
utm_term: z.string().optional(),
utm_content: z.string().optional(),
})
.optional(),
dub_id: z.string().nullish(),
creationSource: z.nativeEnum(CreationSource).optional(),
verificationCode: z.string().optional(),
});
export type BookingCreateBody = z.input<typeof bookingCreateBodySchema>;
// TODO: Plan to create different schemas for Recurring Bookings, Instant Bookings and Regular Bookings.
// We later want to make these properties required. They were optional because they are part of general schema which is used for all types of bookings.
const recurringBookingCreateBodyPartialSchema = z.object({
recurringCount: z.number().optional(),
isFirstRecurringSlot: z.boolean().optional(),
thirdPartyRecurringEventId: z.string().nullish(),
numSlotsToCheckForAvailability: z.number().optional(),
allRecurringDates: z
.array(
z.object({
start: z.string(),
end: z.string().optional(),
})
)
.optional(),
currentRecurringIndex: z.number().optional(),
});
export const extendedBookingCreateBody = bookingCreateBodySchema.merge(
z
.object({
noEmail: z.boolean().optional(),
appsStatus: z
.array(
z.object({
appName: z.string(),
success: z.number(),
failures: z.number(),
type: z.string(),
errors: z.string().array(),
warnings: z.string().array().optional(),
})
)
.optional(),
luckyUsers: z.array(z.number()).optional(),
customInputs: z.undefined().optional(),
})
.merge(recurringBookingCreateBodyPartialSchema)
);
export type ExtendedBookingCreateBody = z.input<typeof extendedBookingCreateBody>;
// It has only the legacy props that are part of `responses` now. The API can still hit old props
export const bookingCreateSchemaLegacyPropsForApi = z.object({
email: z.string(),
name: z.string(),
guests: z.array(z.string()).optional(),
notes: z.string().optional(),
location: z.string(),
smsReminderNumber: z.string().optional().nullable(),
rescheduleReason: z.string().optional(),
customInputs: z.array(z.object({ label: z.string(), value: z.union([z.string(), z.boolean()]) })),
});
// This is the schema that is used for the API. It has all the legacy props that are part of `responses` now.
export const bookingCreateBodySchemaForApi = extendedBookingCreateBody.merge(
bookingCreateSchemaLegacyPropsForApi.partial()
);