feat: API endpoint to fetch bookings for standalone teams (#26818)
* inputs valid * modules * similar route to get bookings * removed plan * test case spec * fix: e2e tests for teams booking controller * chore: update openapi spec * added filter Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> * filter cases * test(teams-bookings): fix eventTypeIds filter test to use correct query parameter * fix: e2e tests * fix: e2e tests * fixup --------- Co-authored-by: Rajiv Sahal <sahalrajiv-extc@atharvacoe.ac.in> Co-authored-by: Ryukemeister <sahalrajiv6900@gmail.com> Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
cubic-dev-ai[bot]
Rajiv Sahal
Ryukemeister
parent
2227d990b6
commit
d137787330
@@ -12,6 +12,7 @@ import { OrganizationsTeamsBookingsModule } from "@/modules/organizations/teams/
|
||||
import { OrganizationsUsersBookingsModule } from "@/modules/organizations/users/bookings/organizations-users-bookings.module";
|
||||
import { RouterModule } from "@/modules/router/router.module";
|
||||
import { StripeModule } from "@/modules/stripe/stripe.module";
|
||||
import { TeamsBookingsModule } from "@/modules/teams/bookings/teams-bookings.module";
|
||||
import { TeamsSchedulesModule } from "@/modules/teams/schedules/teams-schedules.module";
|
||||
import { TimezoneModule } from "@/modules/timezones/timezones.module";
|
||||
import { VerifiedResourcesModule } from "@/modules/verified-resources/verified-resources.module";
|
||||
@@ -42,6 +43,7 @@ import { WebhooksModule } from "./webhooks/webhooks.module";
|
||||
VerifiedResourcesModule,
|
||||
RouterModule,
|
||||
TeamsSchedulesModule,
|
||||
TeamsBookingsModule,
|
||||
],
|
||||
})
|
||||
export class EndpointsModule implements NestModule {
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { GetOrganizationsTeamsBookingsInput_2024_08_13 } from "@/modules/organizations/teams/bookings/inputs/get-organizations-teams-bookings.input";
|
||||
|
||||
export class GetTeamsBookingsInput_2024_08_13 extends GetOrganizationsTeamsBookingsInput_2024_08_13 {}
|
||||
@@ -0,0 +1,581 @@
|
||||
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
|
||||
import type {
|
||||
BookingOutput_2024_08_13,
|
||||
CreateBookingInput_2024_08_13,
|
||||
GetBookingsOutput_2024_08_13,
|
||||
GetSeatedBookingOutput_2024_08_13,
|
||||
RecurringBookingOutput_2024_08_13,
|
||||
} from "@calcom/platform-types";
|
||||
import type { Team, User } from "@calcom/prisma/client";
|
||||
import type { INestApplication } from "@nestjs/common";
|
||||
import type { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import type { TestingModule } from "@nestjs/testing";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import request from "supertest";
|
||||
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
||||
import { HostsRepositoryFixture } from "test/fixtures/repository/hosts.repository.fixture";
|
||||
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
|
||||
import { AppModule } from "@/app.module";
|
||||
import { bootstrap } from "@/bootstrap";
|
||||
import type { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
|
||||
import type { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
|
||||
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { TeamsBookingsModule } from "@/modules/teams/bookings/teams-bookings.module";
|
||||
|
||||
type BookingData =
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13;
|
||||
|
||||
describe("Teams Bookings Endpoints 2024-08-13", () => {
|
||||
describe("Standalone team bookings", () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let schedulesService: SchedulesService_2024_04_15;
|
||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
||||
let hostsRepositoryFixture: HostsRepositoryFixture;
|
||||
|
||||
const teamAdminEmail = "team-admin-bookings@api.com";
|
||||
const teamOwnerEmail = "team-owner-bookings@api.com";
|
||||
const teamMemberEmail = "team-member-bookings@api.com";
|
||||
const nonTeamUserEmail = "non-team-user-bookings@api.com";
|
||||
|
||||
let teamAdmin: User;
|
||||
let teamOwner: User;
|
||||
let teamMember: User;
|
||||
let nonTeamUser: User;
|
||||
let standaloneTeam: Team;
|
||||
|
||||
let teamEventTypeId: number;
|
||||
let teamEventTypeId2: number;
|
||||
|
||||
// Store created booking data for filter tests
|
||||
let createdBookingUid: string;
|
||||
const attendeeEmail1 = "attendee@example.com";
|
||||
const attendeeName1 = "Test Attendee";
|
||||
const attendeeEmail2 = "another@example.com";
|
||||
const attendeeName2 = "Another Attendee";
|
||||
const bookingStart1 = new Date(Date.UTC(2030, 0, 10, 10, 0, 0));
|
||||
const bookingStart2 = new Date(Date.UTC(2030, 0, 11, 14, 0, 0));
|
||||
|
||||
async function createBookingViaApi(
|
||||
eventTypeId: number,
|
||||
start: Date,
|
||||
attendee: { name: string; email: string }
|
||||
): Promise<BookingOutput_2024_08_13> {
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: start.toISOString(),
|
||||
eventTypeId,
|
||||
attendee: {
|
||||
name: attendee.name,
|
||||
email: attendee.email,
|
||||
timeZone: "Europe/London",
|
||||
language: "en",
|
||||
},
|
||||
meetingUrl: "https://meet.google.com/test-meeting",
|
||||
};
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201);
|
||||
|
||||
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
|
||||
const responseData = responseBody.data;
|
||||
if (!Array.isArray(responseData) && "id" in responseData && !("bookingId" in responseData)) {
|
||||
return responseData as BookingOutput_2024_08_13;
|
||||
}
|
||||
throw new Error("Invalid response data - expected booking but received unexpected format");
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef: TestingModule = await withApiAuth(
|
||||
teamOwnerEmail,
|
||||
Test.createTestingModule({
|
||||
imports: [AppModule, TeamsBookingsModule],
|
||||
})
|
||||
)
|
||||
.overrideGuard(PermissionsGuard)
|
||||
.useValue({
|
||||
canActivate: () => true,
|
||||
})
|
||||
.compile();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||
hostsRepositoryFixture = new HostsRepositoryFixture(moduleRef);
|
||||
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
|
||||
|
||||
// Create a standalone team (not part of any organization)
|
||||
standaloneTeam = await teamRepositoryFixture.create({
|
||||
name: "Standalone Team Bookings",
|
||||
isOrganization: false,
|
||||
});
|
||||
|
||||
// Create users
|
||||
teamAdmin = await userRepositoryFixture.create({
|
||||
email: teamAdminEmail,
|
||||
locale: "en",
|
||||
name: "TeamAdminBookings",
|
||||
});
|
||||
|
||||
teamOwner = await userRepositoryFixture.create({
|
||||
email: teamOwnerEmail,
|
||||
locale: "en",
|
||||
name: "TeamOwnerBookings",
|
||||
});
|
||||
|
||||
teamMember = await userRepositoryFixture.create({
|
||||
email: teamMemberEmail,
|
||||
locale: "en",
|
||||
name: "TeamMemberBookings",
|
||||
});
|
||||
|
||||
nonTeamUser = await userRepositoryFixture.create({
|
||||
email: nonTeamUserEmail,
|
||||
locale: "en",
|
||||
name: "NonTeamUserBookings",
|
||||
});
|
||||
|
||||
const userSchedule: CreateScheduleInput_2024_04_15 = {
|
||||
name: "working time",
|
||||
timeZone: "Europe/London",
|
||||
isDefault: true,
|
||||
};
|
||||
await schedulesService.createUserSchedule(teamAdmin.id, userSchedule);
|
||||
await schedulesService.createUserSchedule(teamOwner.id, userSchedule);
|
||||
await schedulesService.createUserSchedule(teamMember.id, userSchedule);
|
||||
await schedulesService.createUserSchedule(nonTeamUser.id, userSchedule);
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "ADMIN",
|
||||
user: { connect: { id: teamAdmin.id } },
|
||||
team: { connect: { id: standaloneTeam.id } },
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "OWNER",
|
||||
user: { connect: { id: teamOwner.id } },
|
||||
team: { connect: { id: standaloneTeam.id } },
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "MEMBER",
|
||||
user: { connect: { id: teamMember.id } },
|
||||
team: { connect: { id: standaloneTeam.id } },
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
const teamEventType1 = await eventTypesRepositoryFixture.createTeamEventType({
|
||||
schedulingType: "COLLECTIVE",
|
||||
team: {
|
||||
connect: { id: standaloneTeam.id },
|
||||
},
|
||||
title: "Team Collective Event",
|
||||
slug: "team-collective-event",
|
||||
length: 60,
|
||||
assignAllTeamMembers: true,
|
||||
bookingFields: [],
|
||||
locations: [],
|
||||
});
|
||||
|
||||
const teamEventType2 = await eventTypesRepositoryFixture.createTeamEventType({
|
||||
schedulingType: "ROUND_ROBIN",
|
||||
team: {
|
||||
connect: { id: standaloneTeam.id },
|
||||
},
|
||||
title: "Team Round Robin Event",
|
||||
slug: "team-round-robin-event",
|
||||
length: 30,
|
||||
assignAllTeamMembers: false,
|
||||
bookingFields: [],
|
||||
locations: [],
|
||||
});
|
||||
|
||||
teamEventTypeId = teamEventType1.id;
|
||||
teamEventTypeId2 = teamEventType2.id;
|
||||
|
||||
await hostsRepositoryFixture.create({
|
||||
isFixed: true,
|
||||
user: {
|
||||
connect: {
|
||||
id: teamAdmin.id,
|
||||
},
|
||||
},
|
||||
eventType: {
|
||||
connect: {
|
||||
id: teamEventType1.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await hostsRepositoryFixture.create({
|
||||
isFixed: true,
|
||||
user: {
|
||||
connect: {
|
||||
id: teamOwner.id,
|
||||
},
|
||||
},
|
||||
eventType: {
|
||||
connect: {
|
||||
id: teamEventType1.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await hostsRepositoryFixture.create({
|
||||
isFixed: false,
|
||||
user: {
|
||||
connect: {
|
||||
id: teamMember.id,
|
||||
},
|
||||
},
|
||||
eventType: {
|
||||
connect: {
|
||||
id: teamEventType2.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
await app.init();
|
||||
|
||||
// Create bookings for filter tests
|
||||
const booking1 = await createBookingViaApi(teamEventTypeId, bookingStart1, {
|
||||
name: attendeeName1,
|
||||
email: attendeeEmail1,
|
||||
});
|
||||
createdBookingUid = booking1.uid;
|
||||
|
||||
await createBookingViaApi(teamEventTypeId2, bookingStart2, {
|
||||
name: attendeeName2,
|
||||
email: attendeeEmail2,
|
||||
});
|
||||
});
|
||||
|
||||
describe("filters", () => {
|
||||
it("should filter by single status", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/teams/${standaloneTeam.id}/bookings?status=upcoming`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
// Verify all bookings are upcoming (in the future)
|
||||
data.forEach((booking) => {
|
||||
if ("start" in booking) {
|
||||
expect(new Date(booking.start).getTime()).toBeGreaterThan(Date.now());
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should filter by multiple statuses", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/teams/${standaloneTeam.id}/bookings?status=upcoming,past`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
// Verify all bookings are either upcoming (future) or past
|
||||
const now = Date.now();
|
||||
data.forEach((booking) => {
|
||||
if (!Array.isArray(booking)) {
|
||||
const bookingEnd = new Date(booking.end).getTime();
|
||||
// Booking is either upcoming (starts in future) or past (ended in past)
|
||||
const isUpcoming = new Date(booking.start).getTime() > now;
|
||||
const isPast = bookingEnd < now;
|
||||
expect(isUpcoming || isPast).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should get team bookings with eventTypeIds filter", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/teams/${standaloneTeam.id}/bookings?eventTypeIds=${teamEventTypeId}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toBeGreaterThanOrEqual(1);
|
||||
data.forEach((booking) => {
|
||||
expect(booking.eventTypeId).toEqual(teamEventTypeId);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should filter by multiple eventTypeIds", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/teams/${standaloneTeam.id}/bookings?eventTypeIds=${teamEventTypeId},${teamEventTypeId2}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
const data: BookingData[] = responseBody.data;
|
||||
expect(data.length).toBeGreaterThanOrEqual(2);
|
||||
data.forEach((booking) => {
|
||||
expect([teamEventTypeId, teamEventTypeId2]).toContain(booking.eventTypeId);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should filter by attendeeEmail", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/teams/${standaloneTeam.id}/bookings?attendeeEmail=${attendeeEmail1}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
const data: BookingData[] = responseBody.data;
|
||||
expect(data.length).toBeGreaterThanOrEqual(1);
|
||||
data.forEach((booking) => {
|
||||
if ("attendees" in booking) {
|
||||
expect(booking.attendees.some((attendee) => attendee.email === attendeeEmail1)).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should filter by attendeeName", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/teams/${standaloneTeam.id}/bookings?attendeeName=${encodeURIComponent(attendeeName1)}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
const data: BookingData[] = responseBody.data;
|
||||
expect(data.length).toBeGreaterThanOrEqual(1);
|
||||
data.forEach((booking) => {
|
||||
if ("attendees" in booking) {
|
||||
expect(booking.attendees.some((attendee) => attendee.name === attendeeName1)).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should filter by bookingUid", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/teams/${standaloneTeam.id}/bookings?bookingUid=${createdBookingUid}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
const data: BookingData[] = responseBody.data;
|
||||
expect(data.length).toEqual(1);
|
||||
expect(data[0].uid).toEqual(createdBookingUid);
|
||||
});
|
||||
});
|
||||
|
||||
it("should filter by date range (afterStart and beforeEnd)", async () => {
|
||||
const afterStartDate = new Date(Date.UTC(2030, 0, 9, 0, 0, 0)).toISOString();
|
||||
const beforeEndDate = new Date(Date.UTC(2030, 0, 12, 0, 0, 0)).toISOString();
|
||||
return request(app.getHttpServer())
|
||||
.get(
|
||||
`/v2/teams/${standaloneTeam.id}/bookings?afterStart=${afterStartDate}&beforeEnd=${beforeEndDate}`
|
||||
)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("sorting", () => {
|
||||
it("should sort by start ascending", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/teams/${standaloneTeam.id}/bookings?sortStart=asc`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
const data: BookingData[] = responseBody.data;
|
||||
for (let i = 1; i < data.length; i++) {
|
||||
const prevBooking = data[i - 1];
|
||||
const currBooking = data[i];
|
||||
if ("start" in prevBooking && "start" in currBooking) {
|
||||
expect(new Date(prevBooking.start).getTime()).toBeLessThanOrEqual(
|
||||
new Date(currBooking.start).getTime()
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should sort by start descending", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/teams/${standaloneTeam.id}/bookings?sortStart=desc`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
const data: BookingData[] = responseBody.data;
|
||||
for (let i = 1; i < data.length; i++) {
|
||||
const prevBooking = data[i - 1];
|
||||
const currBooking = data[i];
|
||||
if ("start" in prevBooking && "start" in currBooking) {
|
||||
expect(new Date(prevBooking.start).getTime()).toBeGreaterThanOrEqual(
|
||||
new Date(currBooking.start).getTime()
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should sort by end ascending", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/teams/${standaloneTeam.id}/bookings?sortEnd=asc`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
const data: BookingData[] = responseBody.data;
|
||||
for (let i = 1; i < data.length; i++) {
|
||||
const prevBooking = data[i - 1];
|
||||
const currBooking = data[i];
|
||||
if ("end" in prevBooking && "end" in currBooking) {
|
||||
expect(new Date(prevBooking.end).getTime()).toBeLessThanOrEqual(
|
||||
new Date(currBooking.end).getTime()
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should sort by created ascending", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/teams/${standaloneTeam.id}/bookings?sortCreated=asc`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
const data: BookingData[] = responseBody.data;
|
||||
for (let i = 1; i < data.length; i++) {
|
||||
const prevBooking = data[i - 1];
|
||||
const currBooking = data[i];
|
||||
if ("createdAt" in prevBooking && "createdAt" in currBooking) {
|
||||
expect(new Date(prevBooking.createdAt).getTime()).toBeLessThanOrEqual(
|
||||
new Date(currBooking.createdAt).getTime()
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("pagination", () => {
|
||||
it("should return paginated results with take parameter", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/teams/${standaloneTeam.id}/bookings?take=1`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.length).toEqual(1);
|
||||
expect(responseBody.pagination).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("should return paginated results with skip parameter", async () => {
|
||||
// First get all bookings to know total
|
||||
const allBookingsResponse = await request(app.getHttpServer())
|
||||
.get(`/v2/teams/${standaloneTeam.id}/bookings`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200);
|
||||
|
||||
const allBookings: BookingData[] = allBookingsResponse.body.data;
|
||||
const totalCount = allBookings.length;
|
||||
|
||||
// Skip first booking
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/teams/${standaloneTeam.id}/bookings?skip=1`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.length).toEqual(totalCount - 1);
|
||||
});
|
||||
});
|
||||
|
||||
it("should return paginated results with take and skip parameters", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/teams/${standaloneTeam.id}/bookings?take=1&skip=1`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.length).toEqual(1);
|
||||
expect(responseBody.pagination).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await userRepositoryFixture.deleteByEmail(teamAdminEmail);
|
||||
await userRepositoryFixture.deleteByEmail(teamOwnerEmail);
|
||||
await userRepositoryFixture.deleteByEmail(teamMemberEmail);
|
||||
await userRepositoryFixture.deleteByEmail(nonTeamUserEmail);
|
||||
await teamRepositoryFixture.delete(standaloneTeam.id);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service";
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { API_KEY_HEADER } from "@/lib/docs/headers";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||
import { GetTeamsBookingsInput_2024_08_13 } from "@/modules/teams/bookings/inputs/get-teams-bookings.input";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { Controller, UseGuards, Get, Param, ParseIntPipe, Query, HttpStatus, HttpCode } from "@nestjs/common";
|
||||
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { GetBookingsOutput_2024_08_13 } from "@calcom/platform-types";
|
||||
|
||||
@Controller({
|
||||
path: "/v2/teams/:teamId/bookings",
|
||||
version: API_VERSIONS_VALUES,
|
||||
})
|
||||
@UseGuards(ApiAuthGuard, RolesGuard)
|
||||
@DocsTags("Teams / Bookings")
|
||||
@ApiHeader(API_KEY_HEADER)
|
||||
export class TeamsBookingsController {
|
||||
constructor(private readonly bookingsService: BookingsService_2024_08_13) {}
|
||||
|
||||
@Get("/")
|
||||
@ApiOperation({ summary: "Get team bookings" })
|
||||
@Roles("TEAM_ADMIN")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async getAllTeamBookings(
|
||||
@Query() queryParams: GetTeamsBookingsInput_2024_08_13,
|
||||
@Param("teamId", ParseIntPipe) teamId: number,
|
||||
@GetUser() user: UserWithProfile
|
||||
): Promise<GetBookingsOutput_2024_08_13> {
|
||||
const { bookings, pagination } = await this.bookingsService.getBookings(
|
||||
{ ...queryParams, teamId },
|
||||
{ email: user.email, id: user.id }
|
||||
);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: bookings,
|
||||
pagination,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { BookingsModule_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.module";
|
||||
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { RedisModule } from "@/modules/redis/redis.module";
|
||||
import { StripeModule } from "@/modules/stripe/stripe.module";
|
||||
import { TeamsBookingsController } from "@/modules/teams/bookings/teams-bookings.controller";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({
|
||||
imports: [BookingsModule_2024_08_13, PrismaModule, StripeModule, RedisModule, MembershipsModule],
|
||||
controllers: [TeamsBookingsController],
|
||||
})
|
||||
export class TeamsBookingsModule {}
|
||||
@@ -15182,6 +15182,211 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/teams/{teamId}/bookings": {
|
||||
"get": {
|
||||
"operationId": "TeamsBookingsController_getAllTeamBookings",
|
||||
"summary": "Get team bookings",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Authorization",
|
||||
"in": "header",
|
||||
"description": "value must be `Bearer <token>` where `<token>` is api key prefixed with cal_",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "status",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Filter bookings by status. If you want to filter by multiple statuses, separate them with a comma.",
|
||||
"schema": {
|
||||
"example": "?status=upcoming,past",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"upcoming",
|
||||
"recurring",
|
||||
"past",
|
||||
"cancelled",
|
||||
"unconfirmed"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "attendeeEmail",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Filter bookings by the attendee's email address.",
|
||||
"schema": {
|
||||
"example": "example@domain.com",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "attendeeName",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Filter bookings by the attendee's name.",
|
||||
"schema": {
|
||||
"example": "John Doe",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "bookingUid",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Filter bookings by the booking Uid.",
|
||||
"schema": {
|
||||
"example": "2NtaeaVcKfpmSZ4CthFdfk",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "eventTypeIds",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Filter bookings by event type ids belonging to the team. Event type ids must be separated by a comma.",
|
||||
"schema": {
|
||||
"example": "100,200",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "afterStart",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Filter bookings that start after this date-time (ISO 8601).",
|
||||
"schema": {
|
||||
"example": "2024-08-13T10:00:00.000Z",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "beforeEnd",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Filter bookings that end before this date-time (ISO 8601).",
|
||||
"schema": {
|
||||
"example": "2024-08-20T10:00:00.000Z",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "teamMemberIds",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Filter bookings by team member user ids. Multiple ids can be separated by a comma. Only works for team events.",
|
||||
"schema": {
|
||||
"example": "10,20",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "teamMemberEmails",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Filter bookings by team member emails. Multiple emails can be separated by a comma. Only works for team events.",
|
||||
"schema": {
|
||||
"example": "alice@example.com,bob@example.com",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "sortStart",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Sort results by their start time (when booking is scheduled for) in ascending or descending order.",
|
||||
"schema": {
|
||||
"example": "?sortStart=asc OR ?sortStart=desc",
|
||||
"enum": [
|
||||
"asc",
|
||||
"desc"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "sortEnd",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Sort results by their end time (when booking is scheduled to end) in ascending or descending order.",
|
||||
"schema": {
|
||||
"example": "?sortEnd=asc OR ?sortEnd=desc",
|
||||
"enum": [
|
||||
"asc",
|
||||
"desc"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "sortCreated",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Sort results by their creation time (when booking was made) in ascending or descending order.",
|
||||
"schema": {
|
||||
"example": "?sortCreated=asc OR ?sortCreated=desc",
|
||||
"enum": [
|
||||
"asc",
|
||||
"desc"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "take",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "The number of items to return",
|
||||
"schema": {
|
||||
"minimum": 1,
|
||||
"maximum": 250,
|
||||
"example": 10,
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "skip",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "The number of items to skip",
|
||||
"schema": {
|
||||
"minimum": 0,
|
||||
"example": 0,
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "teamId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/GetBookingsOutput_2024_08_13"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Teams / Bookings"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/teams/{teamId}/event-types": {
|
||||
"post": {
|
||||
"operationId": "TeamsEventTypesController_createTeamEventType",
|
||||
|
||||
Reference in New Issue
Block a user