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>
This commit is contained in:
co-authored by
morgan@cal.com <morgan@cal.com>
Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent
300b9c916d
commit
6b62557a92
+4
@@ -389,6 +389,7 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
expect(data.hideOrganizerEmail).toEqual(body.hideOrganizerEmail);
|
||||
expect(data.lockTimeZoneToggleOnBookingPage).toEqual(body.lockTimeZoneToggleOnBookingPage);
|
||||
expect(data.color).toEqual(body.color);
|
||||
expect(data.rrHostSubsetEnabled).toEqual(false);
|
||||
expect(data.successRedirectUrl).toEqual("https://masterchief.com/argentina/flan/video/1234");
|
||||
expect(data.emailSettings).toEqual(body.emailSettings);
|
||||
collectiveEventType = responseBody.data;
|
||||
@@ -537,6 +538,7 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
const data = responseBody.data;
|
||||
expect(data.title).toEqual(collectiveEventType.title);
|
||||
expect(data.hosts.length).toEqual(2);
|
||||
expect(data.rrHostSubsetEnabled).toEqual(false);
|
||||
evaluateHost(collectiveEventType.hosts[0], data.hosts[0]);
|
||||
evaluateHost(collectiveEventType.hosts[1], data.hosts[1]);
|
||||
|
||||
@@ -1142,6 +1144,7 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
hideCalendarEventDetails: true,
|
||||
hideOrganizerEmail: true,
|
||||
lockTimeZoneToggleOnBookingPage: true,
|
||||
rrHostSubsetEnabled: true,
|
||||
color: {
|
||||
darkThemeHex: "#292929",
|
||||
lightThemeHex: "#fafafa",
|
||||
@@ -1175,6 +1178,7 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
expect(data.hideOrganizerEmail).toEqual(body.hideOrganizerEmail);
|
||||
expect(data.lockTimeZoneToggleOnBookingPage).toEqual(body.lockTimeZoneToggleOnBookingPage);
|
||||
expect(data.color).toEqual(body.color);
|
||||
expect(data.rrHostSubsetEnabled).toEqual(true);
|
||||
expect(data.successRedirectUrl).toEqual("https://masterchief.com/argentina/flan/video/1234");
|
||||
collectiveEventType = responseBody.data;
|
||||
});
|
||||
|
||||
@@ -88,6 +88,7 @@ type Input = Pick<
|
||||
| "rescheduleWithSameRoundRobinHost"
|
||||
| "maxActiveBookingPerBookerOfferReschedule"
|
||||
| "maxActiveBookingsPerBooker"
|
||||
| "rrHostSubsetEnabled"
|
||||
>;
|
||||
|
||||
@Injectable()
|
||||
@@ -103,8 +104,14 @@ export class OutputOrganizationsEventTypesService {
|
||||
|
||||
const emailSettings = this.transformEmailSettings(metadata);
|
||||
|
||||
const { teamId, userId, parentId, assignAllTeamMembers, rescheduleWithSameRoundRobinHost } =
|
||||
databaseEventType;
|
||||
const {
|
||||
teamId,
|
||||
userId,
|
||||
parentId,
|
||||
assignAllTeamMembers,
|
||||
rescheduleWithSameRoundRobinHost,
|
||||
rrHostSubsetEnabled,
|
||||
} = databaseEventType;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { ownerId, users, ...rest } = this.outputEventTypesService.getResponseEventType(
|
||||
0,
|
||||
@@ -139,6 +146,7 @@ export class OutputOrganizationsEventTypesService {
|
||||
theme: databaseEventType?.team?.theme,
|
||||
},
|
||||
rescheduleWithSameRoundRobinHost,
|
||||
rrHostSubsetEnabled,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+112
-2
@@ -53,6 +53,7 @@ describe("Slots 2024-09-04 Endpoints", () => {
|
||||
const teammateEmailOne = `slots-2024-09-04-user-1-team-slots-${randomString()}`;
|
||||
let teammateApiKeyString: string;
|
||||
const teammateEmailTwo = `slots-2024-09-04-user-2-team-slots-${randomString()}`;
|
||||
const teammateEmailThree = `slots-2024-09-04-user-3-team-slots-${randomString()}`;
|
||||
let teammateTwoApiKeyString: string;
|
||||
|
||||
const outsiderEmail = `slots-2024-09-04-unrelated-team-slots-${randomString()}`;
|
||||
@@ -63,6 +64,7 @@ describe("Slots 2024-09-04 Endpoints", () => {
|
||||
let team: Team;
|
||||
let teammateOne: User;
|
||||
let teammateTwo: User;
|
||||
let teammateThree: User;
|
||||
let collectiveEventTypeId: number;
|
||||
let collectiveEventTypeSlug: string;
|
||||
let collectiveEventTypeWithoutHostsId: number;
|
||||
@@ -114,6 +116,12 @@ describe("Slots 2024-09-04 Endpoints", () => {
|
||||
username: teammateEmailTwo,
|
||||
});
|
||||
|
||||
teammateThree = await userRepositoryFixture.create({
|
||||
email: teammateEmailThree,
|
||||
name: teammateEmailThree,
|
||||
username: teammateEmailThree,
|
||||
});
|
||||
|
||||
outsider = await userRepositoryFixture.create({
|
||||
email: outsiderEmail,
|
||||
name: outsiderEmail,
|
||||
@@ -155,6 +163,13 @@ describe("Slots 2024-09-04 Endpoints", () => {
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "MEMBER",
|
||||
user: { connect: { id: teammateThree.id } },
|
||||
team: { connect: { id: team.id } },
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
const collectiveEventType = await eventTypesRepositoryFixture.createTeamEventType({
|
||||
schedulingType: "COLLECTIVE",
|
||||
team: {
|
||||
@@ -243,7 +258,7 @@ describe("Slots 2024-09-04 Endpoints", () => {
|
||||
bookingFields: [],
|
||||
locations: [],
|
||||
users: {
|
||||
connect: [{ id: teammateOne.id }, { id: teammateTwo.id }],
|
||||
connect: [{ id: teammateOne.id }, { id: teammateTwo.id }, { id: teammateThree.id }],
|
||||
},
|
||||
hosts: {
|
||||
create: [
|
||||
@@ -255,8 +270,13 @@ describe("Slots 2024-09-04 Endpoints", () => {
|
||||
userId: teammateTwo.id,
|
||||
isFixed: false,
|
||||
},
|
||||
{
|
||||
userId: teammateThree.id,
|
||||
isFixed: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
rrHostSubsetEnabled: true,
|
||||
});
|
||||
|
||||
roundRobinEventTypeWithoutFixedHostsId = roundRobinEventTypeWithoutFixedHosts.id;
|
||||
@@ -274,7 +294,7 @@ describe("Slots 2024-09-04 Endpoints", () => {
|
||||
bookingFields: [],
|
||||
locations: [],
|
||||
users: {
|
||||
connect: [{ id: teammateOne.id }, { id: teammateTwo.id }],
|
||||
connect: [{ id: teammateOne.id }, { id: teammateTwo.id }, { id: teammateThree.id }],
|
||||
},
|
||||
hosts: {
|
||||
create: [
|
||||
@@ -286,8 +306,13 @@ describe("Slots 2024-09-04 Endpoints", () => {
|
||||
userId: teammateTwo.id,
|
||||
isFixed: false,
|
||||
},
|
||||
{
|
||||
userId: teammateThree.id,
|
||||
isFixed: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
rrHostSubsetEnabled: true,
|
||||
});
|
||||
|
||||
roundRobinEventTypeWithFixedAndNonFixedHostsId = roundRobinEventTypeWithFixedAndNonFixedHosts.id;
|
||||
@@ -301,6 +326,17 @@ describe("Slots 2024-09-04 Endpoints", () => {
|
||||
await schedulesService.createUserSchedule(teammateOne.id, userSchedule);
|
||||
await schedulesService.createUserSchedule(teammateTwo.id, userSchedule);
|
||||
|
||||
await schedulesService.createUserSchedule(teammateThree.id, {
|
||||
...userSchedule,
|
||||
availability: [
|
||||
{
|
||||
days: ["Monday", "Friday"],
|
||||
startTime: "09:00",
|
||||
endTime: "17:00",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
@@ -360,6 +396,79 @@ describe("Slots 2024-09-04 Endpoints", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should get round robin team event without fixed hosts slots in UTC with subsetIds for teammateThree who has a smaller schedule", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(
|
||||
`/v2/slots?eventTypeId=${roundRobinEventTypeWithoutFixedHostsId}&start=2050-09-05&end=2050-09-09&rrHostSubsetIds[]=${teammateThree.id}`
|
||||
)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_09_04)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetSlotsOutput_2024_09_04 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
const slots = responseBody.data;
|
||||
expect(slots).toBeDefined();
|
||||
const days = Object.keys(slots);
|
||||
expect(days.length).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
it("should get round robin team event without fixed hosts slots in UTC with subsetIds for teammateOne", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(
|
||||
`/v2/slots?eventTypeId=${roundRobinEventTypeWithoutFixedHostsId}&start=2050-09-05&end=2050-09-09&rrHostSubsetIds[]=${teammateOne.id}`
|
||||
)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_09_04)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetSlotsOutput_2024_09_04 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
const slots = responseBody.data;
|
||||
|
||||
expect(slots).toBeDefined();
|
||||
const days = Object.keys(slots);
|
||||
expect(days.length).toEqual(5);
|
||||
expect(slots).toEqual(expectedSlotsUTC);
|
||||
});
|
||||
});
|
||||
|
||||
it("should get round robin team event with and without fixed hosts slots in UTC with subsetIds for teammateOne(fixed) and teammateThree(not fixed) ", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(
|
||||
`/v2/slots?eventTypeId=${roundRobinEventTypeWithFixedAndNonFixedHostsId}&start=2050-09-05&end=2050-09-09&rrHostSubsetIds[]=${teammateOne.id}&rrHostSubsetIds[]=${teammateThree.id}`
|
||||
)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_09_04)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetSlotsOutput_2024_09_04 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
const slots = responseBody.data;
|
||||
|
||||
expect(slots).toBeDefined();
|
||||
const days = Object.keys(slots);
|
||||
expect(days.length).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
it("should get round robin team event with and without fixed hosts slots in UTC with subsetIds for teammateOne(fixed) and teammateTwo(not fixed) ", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(
|
||||
`/v2/slots?eventTypeId=${roundRobinEventTypeWithFixedAndNonFixedHostsId}&start=2050-09-05&end=2050-09-09&rrHostSubsetIds[]=${teammateOne.id}&rrHostSubsetIds[]=${teammateTwo.id}`
|
||||
)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_09_04)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetSlotsOutput_2024_09_04 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
const slots = responseBody.data;
|
||||
|
||||
expect(slots).toBeDefined();
|
||||
const days = Object.keys(slots);
|
||||
expect(days.length).toEqual(5);
|
||||
expect(slots).toEqual(expectedSlotsUTC);
|
||||
});
|
||||
});
|
||||
|
||||
it("should not be able reserve a team event type slot with custom duration if no auth is provided", async () => {
|
||||
await request(app.getHttpServer())
|
||||
.post(`/v2/slots/reservations`)
|
||||
@@ -748,6 +857,7 @@ describe("Slots 2024-09-04 Endpoints", () => {
|
||||
afterAll(async () => {
|
||||
await userRepositoryFixture.deleteByEmail(teammateOne.email);
|
||||
await userRepositoryFixture.deleteByEmail(teammateTwo.email);
|
||||
await userRepositoryFixture.deleteByEmail(teammateThree.email);
|
||||
await userRepositoryFixture.deleteByEmail(outsiderEmail);
|
||||
await teamRepositoryFixture.delete(team.id);
|
||||
await bookingsRepositoryFixture.deleteById(collectiveBookingId);
|
||||
|
||||
@@ -30,6 +30,7 @@ export type InternalGetSlotsQuery = {
|
||||
timeZone: string | undefined;
|
||||
orgSlug: string | null | undefined;
|
||||
rescheduleUid: string | null;
|
||||
rrHostSubsetIds?: number[];
|
||||
};
|
||||
|
||||
export type InternalGetSlotsQueryWithRouting = InternalGetSlotsQuery & {
|
||||
@@ -79,6 +80,7 @@ export class SlotsInputService_2024_09_04 {
|
||||
timeZone,
|
||||
orgSlug,
|
||||
rescheduleUid,
|
||||
rrHostSubsetIds: query.rrHostSubsetIds,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user