fix: v2 seated bookings (#17045)
* fix: rely on booking uid instead of id from handleNewBooking * docs: managed user create input timeZone * fix: make bookingResponsesSchema nullable * Revert "fix: make bookingResponsesSchema nullable" This reverts commit f3127b31dd259d23cae520c994d8e712fc0fa17e. * refactor: define inputs * refactor: define outputs * refactor: bookings controller * test: first seated event test * test: recurring seated event booking * test: multiple bookings for seated event * test: fetching individual seated bookings * test: fetch all bookings * test: make reschedule seated work and test it * wip: cancel seated booking * test: cancel seated booking * refactor: dont return attendee email in seat booking responses * refactor: order of properties in response * refactor: test cleanup * refactor: simplify output booking service * refactor: flaky test * refactor: try to fix test by splitting them * try to fix test * try to fix tests
This commit is contained in:
@@ -6,6 +6,7 @@ import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/servi
|
||||
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
||||
import { ApiKeyRepository } from "@/modules/api-key/api-key-repository";
|
||||
import { BillingModule } from "@/modules/billing/billing.module";
|
||||
import { BookingSeatModule } from "@/modules/booking-seat/booking-seat.module";
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
@@ -16,7 +17,7 @@ import { UsersModule } from "@/modules/users/users.module";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, RedisModule, TokensModule, BillingModule, UsersModule],
|
||||
imports: [PrismaModule, RedisModule, TokensModule, BillingModule, UsersModule, BookingSeatModule],
|
||||
providers: [
|
||||
TokensRepository,
|
||||
OAuthFlowService,
|
||||
|
||||
@@ -29,6 +29,25 @@ export class BookingsRepository_2024_08_13 {
|
||||
});
|
||||
}
|
||||
|
||||
async getByIdsWithAttendeesWithBookingSeatAndUserAndEvent(ids: number[]) {
|
||||
return this.dbRead.prisma.booking.findMany({
|
||||
where: {
|
||||
id: {
|
||||
in: ids,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
attendees: {
|
||||
include: {
|
||||
bookingSeat: true,
|
||||
},
|
||||
},
|
||||
user: true,
|
||||
eventType: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getByUid(bookingUid: string) {
|
||||
return this.dbRead.prisma.booking.findUnique({
|
||||
where: {
|
||||
@@ -50,6 +69,23 @@ export class BookingsRepository_2024_08_13 {
|
||||
});
|
||||
}
|
||||
|
||||
async getByIdWithAttendeesWithBookingSeatAndUserAndEvent(id: number) {
|
||||
return this.dbRead.prisma.booking.findUnique({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
include: {
|
||||
attendees: {
|
||||
include: {
|
||||
bookingSeat: true,
|
||||
},
|
||||
},
|
||||
user: true,
|
||||
eventType: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getByUidWithAttendeesAndUserAndEvent(uid: string) {
|
||||
return this.dbRead.prisma.booking.findUnique({
|
||||
where: {
|
||||
@@ -63,6 +99,31 @@ export class BookingsRepository_2024_08_13 {
|
||||
});
|
||||
}
|
||||
|
||||
async getByUidWithAttendeesWithBookingSeatAndUserAndEvent(uid: string) {
|
||||
return this.dbRead.prisma.booking.findUnique({
|
||||
where: {
|
||||
uid,
|
||||
},
|
||||
include: {
|
||||
attendees: {
|
||||
include: {
|
||||
bookingSeat: true,
|
||||
},
|
||||
},
|
||||
user: true,
|
||||
eventType: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getRecurringByUid(uid: string) {
|
||||
return this.dbRead.prisma.booking.findMany({
|
||||
where: {
|
||||
recurringEventId: uid,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getRecurringByUidWithAttendeesAndUserAndEvent(uid: string) {
|
||||
return this.dbRead.prisma.booking.findMany({
|
||||
where: {
|
||||
|
||||
@@ -34,7 +34,14 @@ import { User } from "@prisma/client";
|
||||
import { Request } from "express";
|
||||
|
||||
import { BOOKING_READ, BOOKING_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { GetBookingOutput_2024_08_13, GetBookingsOutput_2024_08_13 } from "@calcom/platform-types";
|
||||
import {
|
||||
CancelBookingInput,
|
||||
CancelBookingInputPipe,
|
||||
GetBookingOutput_2024_08_13,
|
||||
GetBookingsOutput_2024_08_13,
|
||||
RescheduleBookingInput,
|
||||
RescheduleBookingInputPipe,
|
||||
} from "@calcom/platform-types";
|
||||
import {
|
||||
CreateBookingInputPipe,
|
||||
CreateBookingInput,
|
||||
@@ -166,7 +173,8 @@ export class BookingsController_2024_08_13 {
|
||||
})
|
||||
async rescheduleBooking(
|
||||
@Param("bookingUid") bookingUid: string,
|
||||
@Body() body: RescheduleBookingInput_2024_08_13,
|
||||
@Body(new RescheduleBookingInputPipe())
|
||||
body: RescheduleBookingInput,
|
||||
@Req() request: Request
|
||||
): Promise<RescheduleBookingOutput_2024_08_13> {
|
||||
const newBooking = await this.bookingsService.rescheduleBooking(request, bookingUid, body);
|
||||
@@ -185,7 +193,8 @@ export class BookingsController_2024_08_13 {
|
||||
async cancelBooking(
|
||||
@Req() request: Request,
|
||||
@Param("bookingUid") bookingUid: string,
|
||||
@Body() body: CancelBookingInput_2024_08_13
|
||||
@Body(new CancelBookingInputPipe())
|
||||
body: CancelBookingInput
|
||||
): Promise<CancelBookingOutput_2024_08_13> {
|
||||
const cancelledBooking = await this.bookingsService.cancelBooking(request, bookingUid, body);
|
||||
|
||||
|
||||
@@ -0,0 +1,645 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
|
||||
import { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reschedule-booking.output";
|
||||
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
|
||||
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||
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 { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import { User } from "@prisma/client";
|
||||
import { DateTime } from "luxon";
|
||||
import * as request from "supertest";
|
||||
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
|
||||
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.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 { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
|
||||
import {
|
||||
CancelSeatedBookingInput_2024_08_13,
|
||||
CreateRecurringSeatedBookingOutput_2024_08_13,
|
||||
CreateSeatedBookingOutput_2024_08_13,
|
||||
GetBookingOutput_2024_08_13,
|
||||
GetBookingsOutput_2024_08_13,
|
||||
GetRecurringSeatedBookingOutput_2024_08_13,
|
||||
GetSeatedBookingOutput_2024_08_13,
|
||||
RescheduleSeatedBookingInput_2024_08_13,
|
||||
} from "@calcom/platform-types";
|
||||
import {
|
||||
CreateBookingInput_2024_08_13,
|
||||
CreateRecurringBookingInput_2024_08_13,
|
||||
} from "@calcom/platform-types";
|
||||
import { PlatformOAuthClient, Team } from "@calcom/prisma/client";
|
||||
|
||||
describe("Bookings Endpoints 2024-08-13", () => {
|
||||
describe("Seated bookings", () => {
|
||||
let app: INestApplication;
|
||||
let organization: Team;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let bookingsRepositoryFixture: BookingsRepositoryFixture;
|
||||
let schedulesService: SchedulesService_2024_04_15;
|
||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||
let oAuthClient: PlatformOAuthClient;
|
||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||
|
||||
const userEmail = "seated-bookings-controller-e2e@api.com";
|
||||
let user: User;
|
||||
|
||||
const seatedTventTypeSlug = "peer-coding-seated";
|
||||
const recurringSeatedTventTypeSlug = "peer-coding-recurring-seated";
|
||||
let seatedEventTypeId: number;
|
||||
let recurringSeatedEventTypeId: number;
|
||||
const maxRecurrenceCount = 3;
|
||||
|
||||
let createdSeatedBooking: CreateSeatedBookingOutput_2024_08_13;
|
||||
let createdRecurringSeatedBooking: CreateRecurringSeatedBookingOutput_2024_08_13[];
|
||||
|
||||
const emailAttendeeOne = "mr_proper_seated@gmail.com";
|
||||
const nameAttendeeOne = "Mr Proper Seated";
|
||||
const emailAttendeeTwo = "mr_proper_friend_seated@gmail.com";
|
||||
const nameAttendeeTwo = "Mr Proper Friend Seated";
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
userEmail,
|
||||
Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
|
||||
})
|
||||
)
|
||||
.overrideGuard(PermissionsGuard)
|
||||
.useValue({
|
||||
canActivate: () => true,
|
||||
})
|
||||
.compile();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
|
||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
||||
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
|
||||
|
||||
organization = await teamRepositoryFixture.create({ name: "organization bookings" });
|
||||
oAuthClient = await createOAuthClient(organization.id);
|
||||
|
||||
user = await userRepositoryFixture.create({
|
||||
email: userEmail,
|
||||
});
|
||||
|
||||
const userSchedule: CreateScheduleInput_2024_04_15 = {
|
||||
name: "working time",
|
||||
timeZone: "Europe/Rome",
|
||||
isDefault: true,
|
||||
};
|
||||
await schedulesService.createUserSchedule(user.id, userSchedule);
|
||||
const seatedEvent = await eventTypesRepositoryFixture.create(
|
||||
{
|
||||
title: "peer coding",
|
||||
slug: seatedTventTypeSlug,
|
||||
length: 60,
|
||||
seatsPerTimeSlot: 5,
|
||||
seatsShowAttendees: true,
|
||||
seatsShowAvailabilityCount: true,
|
||||
locations: [{ type: "inPerson", address: "via 10, rome, italy" }],
|
||||
},
|
||||
user.id
|
||||
);
|
||||
seatedEventTypeId = seatedEvent.id;
|
||||
|
||||
const recurringSeatedEvent = await eventTypesRepositoryFixture.create(
|
||||
// note(Lauris): freq 2 means weekly, interval 1 means every week and count 3 means 3 weeks in a row
|
||||
{
|
||||
title: "peer coding recurring",
|
||||
slug: recurringSeatedTventTypeSlug,
|
||||
length: 60,
|
||||
recurringEvent: { freq: 2, count: maxRecurrenceCount, interval: 1 },
|
||||
seatsPerTimeSlot: 5,
|
||||
seatsShowAttendees: true,
|
||||
seatsShowAvailabilityCount: true,
|
||||
locations: [{ type: "inPerson", address: "via 10, rome, italy" }],
|
||||
},
|
||||
user.id
|
||||
);
|
||||
recurringSeatedEventTypeId = recurringSeatedEvent.id;
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
async function createOAuthClient(organizationId: number) {
|
||||
const data = {
|
||||
logo: "logo-url",
|
||||
name: "name",
|
||||
redirectUris: ["http://localhost:5555"],
|
||||
permissions: 32,
|
||||
};
|
||||
const secret = "secret";
|
||||
|
||||
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
|
||||
return client;
|
||||
}
|
||||
|
||||
it("should book an event type with seats for the first time", async () => {
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
|
||||
eventTypeId: seatedEventTypeId,
|
||||
attendee: {
|
||||
name: nameAttendeeOne,
|
||||
email: emailAttendeeOne,
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
bookingFieldsResponses: {
|
||||
codingLanguage: "TypeScript",
|
||||
},
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseDataIsCreateSeatedBooking(responseBody.data)).toBe(true);
|
||||
|
||||
if (responseDataIsCreateSeatedBooking(responseBody.data)) {
|
||||
const data: CreateSeatedBookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.seatUid).toBeDefined();
|
||||
const seatUid = data.seatUid;
|
||||
expect(data.id).toBeDefined();
|
||||
expect(data.uid).toBeDefined();
|
||||
expect(data.hosts[0].id).toEqual(user.id);
|
||||
expect(data.status).toEqual("accepted");
|
||||
expect(data.start).toEqual(body.start);
|
||||
expect(data.end).toEqual(
|
||||
DateTime.fromISO(body.start, { zone: "utc" }).plus({ hours: 1 }).toISO()
|
||||
);
|
||||
expect(data.duration).toEqual(60);
|
||||
expect(data.eventTypeId).toEqual(seatedEventTypeId);
|
||||
expect(data.eventType).toEqual({
|
||||
id: seatedEventTypeId,
|
||||
slug: seatedTventTypeSlug,
|
||||
});
|
||||
expect(data.attendees.length).toEqual(1);
|
||||
expect(data.attendees[0]).toEqual({
|
||||
name: body.attendee.name,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
seatUid,
|
||||
bookingFieldsResponses: {
|
||||
name: body.attendee.name,
|
||||
...body.bookingFieldsResponses,
|
||||
},
|
||||
});
|
||||
expect(data.location).toBeDefined();
|
||||
expect(data.absentHost).toEqual(false);
|
||||
createdSeatedBooking = data;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected recurring booking but received non array response"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should book an event type with seats for the second time", async () => {
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
|
||||
eventTypeId: seatedEventTypeId,
|
||||
attendee: {
|
||||
name: nameAttendeeTwo,
|
||||
email: emailAttendeeTwo,
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
bookingFieldsResponses: {
|
||||
codingLanguage: "Rust",
|
||||
},
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseDataIsCreateSeatedBooking(responseBody.data)).toBe(true);
|
||||
|
||||
if (responseDataIsCreateSeatedBooking(responseBody.data)) {
|
||||
const data: CreateSeatedBookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.seatUid).toBeDefined();
|
||||
const seatUid = data.seatUid;
|
||||
expect(data.id).toBeDefined();
|
||||
expect(data.uid).toBeDefined();
|
||||
expect(data.hosts[0].id).toEqual(user.id);
|
||||
expect(data.status).toEqual("accepted");
|
||||
expect(data.start).toEqual(body.start);
|
||||
expect(data.end).toEqual(
|
||||
DateTime.fromISO(body.start, { zone: "utc" }).plus({ hours: 1 }).toISO()
|
||||
);
|
||||
expect(data.duration).toEqual(60);
|
||||
expect(data.eventTypeId).toEqual(seatedEventTypeId);
|
||||
expect(data.eventType).toEqual({
|
||||
id: seatedEventTypeId,
|
||||
slug: seatedTventTypeSlug,
|
||||
});
|
||||
expect(data.attendees.length).toEqual(2);
|
||||
// note(Lauris): first attendee is from previous test request
|
||||
const firstAttendee = data.attendees.find((attendee) => attendee.name === nameAttendeeOne);
|
||||
expect(firstAttendee).toEqual({
|
||||
name: createdSeatedBooking.attendees[0].name,
|
||||
timeZone: createdSeatedBooking.attendees[0].timeZone,
|
||||
language: createdSeatedBooking.attendees[0].language,
|
||||
absent: false,
|
||||
seatUid: createdSeatedBooking.seatUid,
|
||||
bookingFieldsResponses: {
|
||||
name: createdSeatedBooking.attendees[0].name,
|
||||
...createdSeatedBooking.attendees[0].bookingFieldsResponses,
|
||||
},
|
||||
});
|
||||
const secondAttendee = data.attendees.find((attendee) => attendee.name === nameAttendeeTwo);
|
||||
expect(secondAttendee).toEqual({
|
||||
name: body.attendee.name,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
seatUid,
|
||||
bookingFieldsResponses: {
|
||||
name: body.attendee.name,
|
||||
...body.bookingFieldsResponses,
|
||||
},
|
||||
});
|
||||
expect(data.location).toBeDefined();
|
||||
expect(data.absentHost).toEqual(false);
|
||||
createdSeatedBooking = data;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected recurring booking but received non array response"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should book a recurring event type with seats", async () => {
|
||||
const recurrenceCount = 2;
|
||||
const body: CreateRecurringBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 11, 0, 0)).toISOString(),
|
||||
eventTypeId: recurringSeatedEventTypeId,
|
||||
attendee: {
|
||||
name: "Mr Proper",
|
||||
email: "mr_proper@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
bookingFieldsResponses: {
|
||||
codingLanguage: "TypeScript",
|
||||
},
|
||||
recurrenceCount,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseDataIsCreateRecurringSeatedBooking(responseBody.data)).toBe(true);
|
||||
|
||||
if (responseDataIsCreateRecurringSeatedBooking(responseBody.data)) {
|
||||
const data: CreateRecurringSeatedBookingOutput_2024_08_13[] = responseBody.data;
|
||||
expect(data.length).toEqual(recurrenceCount);
|
||||
|
||||
const firstBooking = data[0];
|
||||
expect(firstBooking.seatUid).toBeDefined();
|
||||
const seatUid = firstBooking.seatUid;
|
||||
expect(firstBooking.id).toBeDefined();
|
||||
expect(firstBooking.uid).toBeDefined();
|
||||
expect(firstBooking.hosts[0].id).toEqual(user.id);
|
||||
expect(firstBooking.status).toEqual("accepted");
|
||||
expect(firstBooking.start).toEqual(body.start);
|
||||
expect(firstBooking.end).toEqual(
|
||||
DateTime.fromISO(body.start, { zone: "utc" }).plus({ hours: 1 }).toISO()
|
||||
);
|
||||
expect(firstBooking.duration).toEqual(60);
|
||||
expect(firstBooking.eventTypeId).toEqual(recurringSeatedEventTypeId);
|
||||
expect(firstBooking.eventType).toEqual({
|
||||
id: recurringSeatedEventTypeId,
|
||||
slug: recurringSeatedTventTypeSlug,
|
||||
});
|
||||
expect(firstBooking.attendees.length).toEqual(1);
|
||||
expect(firstBooking.attendees[0]).toEqual({
|
||||
name: body.attendee.name,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
seatUid,
|
||||
bookingFieldsResponses: {
|
||||
name: body.attendee.name,
|
||||
...body.bookingFieldsResponses,
|
||||
},
|
||||
});
|
||||
expect(firstBooking.location).toEqual(body.location);
|
||||
expect(firstBooking.absentHost).toEqual(false);
|
||||
|
||||
const secondBooking = data[1];
|
||||
expect(secondBooking.seatUid).toBeDefined();
|
||||
const secondSeatUid = secondBooking.seatUid;
|
||||
expect(secondBooking.id).toBeDefined();
|
||||
expect(secondBooking.uid).toBeDefined();
|
||||
expect(secondBooking.hosts[0].id).toEqual(user.id);
|
||||
expect(secondBooking.status).toEqual("accepted");
|
||||
const expectedStart = DateTime.fromISO(body.start, { zone: "utc" }).plus({ weeks: 1 }).toISO();
|
||||
expect(secondBooking.start).toEqual(expectedStart);
|
||||
expect(secondBooking.end).toEqual(
|
||||
DateTime.fromISO(expectedStart!, { zone: "utc" }).plus({ hours: 1 }).toISO()
|
||||
);
|
||||
expect(secondBooking.duration).toEqual(60);
|
||||
expect(secondBooking.eventTypeId).toEqual(recurringSeatedEventTypeId);
|
||||
expect(secondBooking.eventType).toEqual({
|
||||
id: recurringSeatedEventTypeId,
|
||||
slug: recurringSeatedTventTypeSlug,
|
||||
});
|
||||
expect(secondBooking.attendees.length).toEqual(1);
|
||||
expect(secondBooking.attendees[0]).toEqual({
|
||||
name: body.attendee.name,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
seatUid: secondSeatUid,
|
||||
bookingFieldsResponses: {
|
||||
name: body.attendee.name,
|
||||
...body.bookingFieldsResponses,
|
||||
},
|
||||
});
|
||||
expect(secondBooking.location).toEqual(body.location);
|
||||
expect(secondBooking.absentHost).toEqual(false);
|
||||
createdRecurringSeatedBooking = data;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected recurring booking but received non array response"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should get a booking for an event type with seats", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings/${createdSeatedBooking.uid}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseDataIsGetSeatedBooking(responseBody.data)).toBe(true);
|
||||
|
||||
if (responseDataIsGetSeatedBooking(responseBody.data)) {
|
||||
const data: GetSeatedBookingOutput_2024_08_13 = responseBody.data;
|
||||
const expected = structuredClone(createdSeatedBooking);
|
||||
// note(Lauris): seatUid in get response resides only in each attendee object
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
delete expected.seatUid;
|
||||
expect(data).toEqual(expected);
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected recurring booking but received non array response"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should get a booking for a recurring event type with seats", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings/${createdRecurringSeatedBooking[1].recurringBookingUid}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseDataIsGetRecurringSeatedBooking(responseBody.data)).toBe(true);
|
||||
|
||||
if (responseDataIsGetRecurringSeatedBooking(responseBody.data)) {
|
||||
const data: GetRecurringSeatedBookingOutput_2024_08_13[] = responseBody.data;
|
||||
const expected = structuredClone(createdRecurringSeatedBooking);
|
||||
for (const booking of expected) {
|
||||
// note(Lauris): seatUid in get response resides only in each attendee object
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
delete booking.seatUid;
|
||||
}
|
||||
expect(data).toEqual(expected);
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected recurring booking but received non array response"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should get a specific recurrence of a booking for a recurring event type with seats", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings/${createdRecurringSeatedBooking[0].uid}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseDataIsGetSeatedBooking(responseBody.data)).toBe(true);
|
||||
|
||||
if (responseDataIsGetSeatedBooking(responseBody.data)) {
|
||||
const data: GetSeatedBookingOutput_2024_08_13 = responseBody.data;
|
||||
const expected = structuredClone(createdRecurringSeatedBooking[0]);
|
||||
// note(Lauris): seatUid in get response resides only in each attendee object
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
delete expected.seatUid;
|
||||
expect(data).toEqual(expected);
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected recurring booking but received non array response"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should get all seated bookings", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get("/v2/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);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.length).toEqual(3);
|
||||
|
||||
const seatedBooking = responseBody.data[0];
|
||||
const seatedBookingExpected = structuredClone(createdSeatedBooking);
|
||||
// note(Lauris): seatUid in get response resides only in each attendee object
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
delete seatedBookingExpected.seatUid;
|
||||
expect(seatedBooking).toEqual(seatedBookingExpected);
|
||||
|
||||
const recurringSeatedBookings = [responseBody.data[1], responseBody.data[2]];
|
||||
const recurringSeatedBookingsExpected = structuredClone(createdRecurringSeatedBooking);
|
||||
for (const booking of recurringSeatedBookingsExpected) {
|
||||
// note(Lauris): seatUid in get response resides only in each attendee object
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
delete booking.seatUid;
|
||||
}
|
||||
expect(recurringSeatedBookings).toEqual(recurringSeatedBookingsExpected);
|
||||
});
|
||||
});
|
||||
|
||||
it("should reschedule seated booking", async () => {
|
||||
const body: RescheduleSeatedBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 15, 0, 0)).toISOString(),
|
||||
seatUid: createdSeatedBooking.seatUid,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${createdSeatedBooking.uid}/reschedule`)
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseDataIsGetSeatedBooking(responseBody.data)).toBe(true);
|
||||
|
||||
if (responseDataIsGetSeatedBooking(responseBody.data)) {
|
||||
const data: CreateSeatedBookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.seatUid).toBeDefined();
|
||||
const seatUid = data.seatUid;
|
||||
expect(data.id).toBeDefined();
|
||||
expect(data.uid).toBeDefined();
|
||||
expect(data.hosts[0].id).toEqual(user.id);
|
||||
expect(data.status).toEqual("accepted");
|
||||
expect(data.start).toEqual(body.start);
|
||||
expect(data.end).toEqual(
|
||||
DateTime.fromISO(body.start, { zone: "utc" }).plus({ hours: 1 }).toISO()
|
||||
);
|
||||
expect(data.duration).toEqual(60);
|
||||
expect(data.eventTypeId).toEqual(seatedEventTypeId);
|
||||
expect(data.eventType).toEqual({
|
||||
id: seatedEventTypeId,
|
||||
slug: seatedTventTypeSlug,
|
||||
});
|
||||
expect(data.attendees.length).toEqual(1);
|
||||
const attendee = createdSeatedBooking.attendees.find((a) => a.seatUid === body.seatUid);
|
||||
expect(data.attendees[0]).toEqual({
|
||||
name: attendee?.name,
|
||||
timeZone: attendee?.timeZone,
|
||||
language: attendee?.language,
|
||||
absent: false,
|
||||
seatUid,
|
||||
bookingFieldsResponses: {
|
||||
name: attendee?.name,
|
||||
...attendee?.bookingFieldsResponses,
|
||||
},
|
||||
});
|
||||
expect(data.location).toBeDefined();
|
||||
expect(data.absentHost).toEqual(false);
|
||||
createdSeatedBooking = data;
|
||||
} else {
|
||||
throw new Error("Invalid response data - expected booking but received array response");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should cancel seated booking", async () => {
|
||||
const body: CancelSeatedBookingInput_2024_08_13 = {
|
||||
seatUid: createdSeatedBooking.seatUid,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${createdSeatedBooking.uid}/cancel`)
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseDataIsGetSeatedBooking(responseBody.data)).toBe(true);
|
||||
|
||||
if (responseDataIsGetSeatedBooking(responseBody.data)) {
|
||||
const data: GetSeatedBookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.id).toBeDefined();
|
||||
expect(data.uid).toBeDefined();
|
||||
expect(data.hosts[0].id).toEqual(user.id);
|
||||
expect(data.status).toEqual("cancelled");
|
||||
expect(data.start).toEqual(createdSeatedBooking.start);
|
||||
expect(data.end).toEqual(createdSeatedBooking.end);
|
||||
expect(data.duration).toEqual(60);
|
||||
expect(data.eventTypeId).toEqual(seatedEventTypeId);
|
||||
expect(data.eventType).toEqual({
|
||||
id: seatedEventTypeId,
|
||||
slug: seatedTventTypeSlug,
|
||||
});
|
||||
expect(data.attendees.length).toEqual(0);
|
||||
expect(data.location).toBeDefined();
|
||||
expect(data.absentHost).toEqual(false);
|
||||
} else {
|
||||
throw new Error("Invalid response data - expected booking but received array response");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function responseDataIsCreateSeatedBooking(data: any): data is CreateSeatedBookingOutput_2024_08_13 {
|
||||
return data.hasOwnProperty("seatUid");
|
||||
}
|
||||
|
||||
function responseDataIsCreateRecurringSeatedBooking(
|
||||
data: any
|
||||
): data is CreateRecurringSeatedBookingOutput_2024_08_13[] {
|
||||
return Array.isArray(data);
|
||||
}
|
||||
|
||||
function responseDataIsGetSeatedBooking(data: any): data is GetSeatedBookingOutput_2024_08_13 {
|
||||
return data?.attendees?.every((attendee: any) => attendee?.hasOwnProperty("seatUid"));
|
||||
}
|
||||
|
||||
function responseDataIsGetRecurringSeatedBooking(
|
||||
data: any
|
||||
): data is GetRecurringSeatedBookingOutput_2024_08_13[] {
|
||||
return Array.isArray(data);
|
||||
}
|
||||
|
||||
afterAll(async () => {
|
||||
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||
await teamRepositoryFixture.delete(organization.id);
|
||||
await userRepositoryFixture.deleteByEmail(user.email);
|
||||
await bookingsRepositoryFixture.deleteAllBookings(user.id, user.email);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
+17
-3
@@ -29,6 +29,7 @@ import {
|
||||
BookingOutput_2024_08_13,
|
||||
RecurringBookingOutput_2024_08_13,
|
||||
GetBookingsOutput_2024_08_13,
|
||||
GetSeatedBookingOutput_2024_08_13,
|
||||
} from "@calcom/platform-types";
|
||||
import { PlatformOAuthClient, Team } from "@calcom/prisma/client";
|
||||
|
||||
@@ -369,7 +370,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
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)[] = responseBody.data;
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(1);
|
||||
expect(data[0].eventTypeId).toEqual(team1EventTypeId);
|
||||
});
|
||||
@@ -384,7 +389,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
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)[] = responseBody.data;
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(1);
|
||||
expect(data[0].eventTypeId).toEqual(team2EventTypeId);
|
||||
});
|
||||
@@ -399,7 +408,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
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)[] = responseBody.data;
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(2);
|
||||
expect(data.find((booking) => booking.eventTypeId === team1EventTypeId)).toBeDefined();
|
||||
expect(data.find((booking) => booking.eventTypeId === team2EventTypeId)).toBeDefined();
|
||||
@@ -428,6 +441,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||
await teamRepositoryFixture.delete(organization.id);
|
||||
await userRepositoryFixture.deleteByEmail(teamUser.email);
|
||||
await userRepositoryFixture.deleteByEmail(teamUserEmail2);
|
||||
await bookingsRepositoryFixture.deleteAllBookings(teamUser.id, teamUser.email);
|
||||
await app.close();
|
||||
});
|
||||
|
||||
+104
-20
@@ -28,7 +28,11 @@ import {
|
||||
VERSION_2024_08_13,
|
||||
X_CAL_CLIENT_ID,
|
||||
} from "@calcom/platform-constants";
|
||||
import { GetBookingOutput_2024_08_13, GetBookingsOutput_2024_08_13 } from "@calcom/platform-types";
|
||||
import {
|
||||
GetBookingOutput_2024_08_13,
|
||||
GetBookingsOutput_2024_08_13,
|
||||
GetSeatedBookingOutput_2024_08_13,
|
||||
} from "@calcom/platform-types";
|
||||
import {
|
||||
CreateBookingInput_2024_08_13,
|
||||
BookingOutput_2024_08_13,
|
||||
@@ -478,7 +482,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
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)[] = responseBody.data;
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(5);
|
||||
});
|
||||
});
|
||||
@@ -492,7 +500,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
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)[] = responseBody.data;
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(3);
|
||||
});
|
||||
});
|
||||
@@ -506,7 +518,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
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)[] = responseBody.data;
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(3);
|
||||
});
|
||||
});
|
||||
@@ -520,7 +536,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
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)[] = responseBody.data;
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(4);
|
||||
});
|
||||
});
|
||||
@@ -534,7 +554,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
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)[] = responseBody.data;
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(1);
|
||||
});
|
||||
});
|
||||
@@ -548,7 +572,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
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)[] = responseBody.data;
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(5);
|
||||
});
|
||||
});
|
||||
@@ -562,7 +590,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
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)[] = responseBody.data;
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(3);
|
||||
});
|
||||
});
|
||||
@@ -576,7 +608,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
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)[] = responseBody.data;
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(1);
|
||||
});
|
||||
});
|
||||
@@ -590,7 +626,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
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)[] = responseBody.data;
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(3);
|
||||
});
|
||||
});
|
||||
@@ -604,7 +644,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
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)[] = responseBody.data;
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(2);
|
||||
});
|
||||
});
|
||||
@@ -618,7 +662,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
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)[] = responseBody.data;
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(5);
|
||||
});
|
||||
});
|
||||
@@ -632,7 +680,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
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)[] = responseBody.data;
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(2);
|
||||
});
|
||||
});
|
||||
@@ -646,7 +698,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
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)[] = responseBody.data;
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(3);
|
||||
});
|
||||
});
|
||||
@@ -660,7 +716,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
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)[] = responseBody.data;
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(2);
|
||||
expect(data[0].start).toEqual(createdBooking.start);
|
||||
expect(data[1].start).toEqual(bookingInThePast.startTime.toISOString());
|
||||
@@ -676,7 +736,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
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)[] = responseBody.data;
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(2);
|
||||
expect(data[0].start).toEqual(bookingInThePast.startTime.toISOString());
|
||||
expect(data[1].start).toEqual(createdBooking.start);
|
||||
@@ -692,7 +756,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
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)[] = responseBody.data;
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(2);
|
||||
expect(data[0].start).toEqual(createdBooking.start);
|
||||
expect(data[1].start).toEqual(bookingInThePast.startTime.toISOString());
|
||||
@@ -708,7 +776,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
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)[] = responseBody.data;
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(2);
|
||||
expect(data[0].start).toEqual(bookingInThePast.startTime.toISOString());
|
||||
expect(data[1].start).toEqual(createdBooking.start);
|
||||
@@ -724,7 +796,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
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)[] = responseBody.data;
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(2);
|
||||
expect(data[0].start).toEqual(createdBooking.start);
|
||||
expect(data[1].start).toEqual(bookingInThePast.startTime.toISOString());
|
||||
@@ -740,7 +816,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
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)[] = responseBody.data;
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(2);
|
||||
expect(data[0].start).toEqual(bookingInThePast.startTime.toISOString());
|
||||
expect(data[1].start).toEqual(createdBooking.start);
|
||||
@@ -764,6 +844,8 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const data: BookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.reschedulingReason).toEqual(body.reschedulingReason);
|
||||
expect(data.start).toEqual(body.start);
|
||||
@@ -792,6 +874,8 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const data: BookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.status).toEqual("cancelled");
|
||||
|
||||
|
||||
@@ -3,7 +3,12 @@ import { Type } from "class-transformer";
|
||||
import { IsEnum, ValidateNested } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
import { BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13 } from "@calcom/platform-types";
|
||||
import {
|
||||
BookingOutput_2024_08_13,
|
||||
GetRecurringSeatedBookingOutput_2024_08_13,
|
||||
GetSeatedBookingOutput_2024_08_13,
|
||||
RecurringBookingOutput_2024_08_13,
|
||||
} from "@calcom/platform-types";
|
||||
|
||||
@ApiExtraModels(BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13)
|
||||
export class CancelBookingOutput_2024_08_13 {
|
||||
@@ -17,10 +22,19 @@ export class CancelBookingOutput_2024_08_13 {
|
||||
{ $ref: getSchemaPath(BookingOutput_2024_08_13) },
|
||||
{ $ref: getSchemaPath(RecurringBookingOutput_2024_08_13) },
|
||||
{ type: "array", items: { $ref: getSchemaPath(RecurringBookingOutput_2024_08_13) } },
|
||||
{ $ref: getSchemaPath(GetSeatedBookingOutput_2024_08_13) },
|
||||
{ $ref: getSchemaPath(GetRecurringSeatedBookingOutput_2024_08_13) },
|
||||
{ type: "array", items: { $ref: getSchemaPath(GetRecurringSeatedBookingOutput_2024_08_13) } },
|
||||
],
|
||||
description:
|
||||
"Booking data, which can be either a BookingOutput object, a RecurringBookingOutput object, or an array of RecurringBookingOutput objects",
|
||||
})
|
||||
@Type(() => Object)
|
||||
data!: BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13[];
|
||||
data!:
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13[]
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
| GetRecurringSeatedBookingOutput_2024_08_13
|
||||
| GetRecurringSeatedBookingOutput_2024_08_13[];
|
||||
}
|
||||
|
||||
@@ -3,7 +3,12 @@ import { Type } from "class-transformer";
|
||||
import { IsEnum, ValidateNested } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
import { BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13 } from "@calcom/platform-types";
|
||||
import {
|
||||
BookingOutput_2024_08_13,
|
||||
RecurringBookingOutput_2024_08_13,
|
||||
CreateSeatedBookingOutput_2024_08_13,
|
||||
CreateRecurringSeatedBookingOutput_2024_08_13,
|
||||
} from "@calcom/platform-types";
|
||||
|
||||
@ApiExtraModels(BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13)
|
||||
export class CreateBookingOutput_2024_08_13 {
|
||||
@@ -21,5 +26,9 @@ export class CreateBookingOutput_2024_08_13 {
|
||||
"Booking data, which can be either a BookingOutput object or an array of RecurringBookingOutput objects",
|
||||
})
|
||||
@Type(() => Object)
|
||||
data!: BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13[];
|
||||
data!:
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13[]
|
||||
| CreateSeatedBookingOutput_2024_08_13
|
||||
| CreateRecurringSeatedBookingOutput_2024_08_13[];
|
||||
}
|
||||
|
||||
@@ -3,7 +3,14 @@ import { Type } from "class-transformer";
|
||||
import { IsEnum, ValidateNested } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
import { BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13 } from "@calcom/platform-types";
|
||||
import {
|
||||
BookingOutput_2024_08_13,
|
||||
CreateRecurringSeatedBookingOutput_2024_08_13,
|
||||
CreateSeatedBookingOutput_2024_08_13,
|
||||
GetRecurringSeatedBookingOutput_2024_08_13,
|
||||
GetSeatedBookingOutput_2024_08_13,
|
||||
RecurringBookingOutput_2024_08_13,
|
||||
} from "@calcom/platform-types";
|
||||
|
||||
@ApiExtraModels(BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13)
|
||||
export class RescheduleBookingOutput_2024_08_13 {
|
||||
@@ -15,11 +22,17 @@ export class RescheduleBookingOutput_2024_08_13 {
|
||||
oneOf: [
|
||||
{ $ref: getSchemaPath(BookingOutput_2024_08_13) },
|
||||
{ $ref: getSchemaPath(RecurringBookingOutput_2024_08_13) },
|
||||
{ $ref: getSchemaPath(CreateSeatedBookingOutput_2024_08_13) },
|
||||
{ $ref: getSchemaPath(CreateRecurringSeatedBookingOutput_2024_08_13) },
|
||||
],
|
||||
description:
|
||||
"Booking data, which can be either a BookingOutput object or a RecurringBookingOutput object",
|
||||
})
|
||||
@ValidateNested()
|
||||
@Type(() => Object)
|
||||
data!: BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13;
|
||||
data!:
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| CreateSeatedBookingOutput_2024_08_13
|
||||
| CreateRecurringSeatedBookingOutput_2024_08_13;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
} from "@calcom/platform-libraries";
|
||||
import {
|
||||
CreateBookingInput_2024_08_13,
|
||||
RescheduleBookingInput_2024_08_13,
|
||||
CreateBookingInput,
|
||||
CreateRecurringBookingInput_2024_08_13,
|
||||
GetBookingsInput_2024_08_13,
|
||||
@@ -27,6 +26,10 @@ import {
|
||||
MarkAbsentBookingInput_2024_08_13,
|
||||
BookingOutput_2024_08_13,
|
||||
RecurringBookingOutput_2024_08_13,
|
||||
GetSeatedBookingOutput_2024_08_13,
|
||||
GetRecurringSeatedBookingOutput_2024_08_13,
|
||||
RescheduleBookingInput,
|
||||
CancelBookingInput,
|
||||
} from "@calcom/platform-types";
|
||||
import { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
@@ -54,9 +57,19 @@ export class BookingsService_2024_08_13 {
|
||||
return await this.createInstantBooking(request, body);
|
||||
}
|
||||
|
||||
if (await this.isRecurring(body)) {
|
||||
const eventType = await this.eventTypesRepository.getEventTypeById(body.eventTypeId);
|
||||
const isRecurring = !!eventType?.recurringEvent;
|
||||
const isSeated = !!eventType?.seatsPerTimeSlot;
|
||||
|
||||
if (isRecurring && isSeated) {
|
||||
return await this.createRecurringSeatedBooking(request, body);
|
||||
}
|
||||
if (isRecurring && !isSeated) {
|
||||
return await this.createRecurringBooking(request, body);
|
||||
}
|
||||
if (isSeated) {
|
||||
return await this.createSeatedBooking(request, body);
|
||||
}
|
||||
|
||||
return await this.createRegularBooking(request, body);
|
||||
} catch (error) {
|
||||
@@ -83,47 +96,71 @@ export class BookingsService_2024_08_13 {
|
||||
return this.outputService.getOutputBooking(databaseBooking);
|
||||
}
|
||||
|
||||
async isRecurring(body: CreateBookingInput) {
|
||||
const eventType = await this.eventTypesRepository.getEventTypeById(body.eventTypeId);
|
||||
return !!eventType?.recurringEvent;
|
||||
}
|
||||
|
||||
async createRecurringBooking(request: Request, body: CreateRecurringBookingInput_2024_08_13) {
|
||||
const bookingRequest = await this.inputService.createRecurringBookingRequest(request, body);
|
||||
const bookings = await handleNewRecurringBooking(bookingRequest);
|
||||
const uid = bookings[0].recurringEventId;
|
||||
if (!uid) {
|
||||
throw new Error("Recurring booking was not created");
|
||||
}
|
||||
const ids = bookings.map((booking) => booking.id || 0);
|
||||
return this.outputService.getOutputRecurringBookings(ids);
|
||||
}
|
||||
|
||||
const recurringBooking = await this.bookingsRepository.getRecurringByUidWithAttendeesAndUserAndEvent(uid);
|
||||
return this.outputService.getOutputRecurringBookings(recurringBooking);
|
||||
async createRecurringSeatedBooking(request: Request, body: CreateRecurringBookingInput_2024_08_13) {
|
||||
const bookingRequest = await this.inputService.createRecurringBookingRequest(request, body);
|
||||
const bookings = await handleNewRecurringBooking(bookingRequest);
|
||||
return this.outputService.getOutputCreateRecurringSeatedBookings(
|
||||
bookings.map((booking) => ({ id: booking.id || 0, seatUid: booking.seatReferenceUid || "" }))
|
||||
);
|
||||
}
|
||||
|
||||
async createRegularBooking(request: Request, body: CreateBookingInput_2024_08_13) {
|
||||
const bookingRequest = await this.inputService.createBookingRequest(request, body);
|
||||
const booking = await handleNewBooking(bookingRequest);
|
||||
|
||||
if (!booking.id) {
|
||||
throw new Error("Booking was not created");
|
||||
if (!booking.uid) {
|
||||
throw new Error("Booking missing uid");
|
||||
}
|
||||
|
||||
const databaseBooking = await this.bookingsRepository.getByIdWithAttendeesAndUserAndEvent(booking.id);
|
||||
const databaseBooking = await this.bookingsRepository.getByUidWithAttendeesAndUserAndEvent(booking.uid);
|
||||
if (!databaseBooking) {
|
||||
throw new Error(`Booking with id=${booking.id} was not found in the database`);
|
||||
throw new Error(`Booking with uid=${booking.uid} was not found in the database`);
|
||||
}
|
||||
|
||||
return this.outputService.getOutputBooking(databaseBooking);
|
||||
}
|
||||
|
||||
async createSeatedBooking(request: Request, body: CreateBookingInput_2024_08_13) {
|
||||
const bookingRequest = await this.inputService.createBookingRequest(request, body);
|
||||
const booking = await handleNewBooking(bookingRequest);
|
||||
|
||||
if (!booking.uid) {
|
||||
throw new Error("Booking missing uid");
|
||||
}
|
||||
|
||||
const databaseBooking = await this.bookingsRepository.getByUidWithAttendeesWithBookingSeatAndUserAndEvent(
|
||||
booking.uid
|
||||
);
|
||||
if (!databaseBooking) {
|
||||
throw new Error(`Booking with uid=${booking.uid} was not found in the database`);
|
||||
}
|
||||
|
||||
return this.outputService.getOutputCreateSeatedBooking(databaseBooking, booking.seatReferenceUid || "");
|
||||
}
|
||||
|
||||
async getBooking(uid: string) {
|
||||
const booking = await this.bookingsRepository.getByUidWithAttendeesAndUserAndEvent(uid);
|
||||
const booking = await this.bookingsRepository.getByUidWithAttendeesWithBookingSeatAndUserAndEvent(uid);
|
||||
|
||||
if (booking) {
|
||||
const isRecurring = !!booking.recurringEventId;
|
||||
if (isRecurring) {
|
||||
const isSeated = !!booking.eventType?.seatsPerTimeSlot;
|
||||
|
||||
if (isRecurring && !isSeated) {
|
||||
return this.outputService.getOutputRecurringBooking(booking);
|
||||
}
|
||||
if (isRecurring && isSeated) {
|
||||
return this.outputService.getOutputRecurringSeatedBooking(booking);
|
||||
}
|
||||
if (isSeated) {
|
||||
return this.outputService.getOutputSeatedBooking(booking);
|
||||
}
|
||||
return this.outputService.getOutputBooking(booking);
|
||||
}
|
||||
|
||||
@@ -131,8 +168,13 @@ export class BookingsService_2024_08_13 {
|
||||
if (!recurringBooking.length) {
|
||||
throw new NotFoundException(`Booking with uid=${uid} was not found in the database`);
|
||||
}
|
||||
const ids = recurringBooking.map((booking) => booking.id);
|
||||
const isRecurringSeated = !!recurringBooking[0].eventType?.seatsPerTimeSlot;
|
||||
if (isRecurringSeated) {
|
||||
return this.outputService.getOutputRecurringSeatedBookings(ids);
|
||||
}
|
||||
|
||||
return this.outputService.getOutputRecurringBookings(recurringBooking);
|
||||
return this.outputService.getOutputRecurringBookings(ids);
|
||||
}
|
||||
|
||||
async getBookings(queryParams: GetBookingsInput_2024_08_13, user: { email: string; id: number }) {
|
||||
@@ -151,12 +193,17 @@ export class BookingsService_2024_08_13 {
|
||||
// note(Lauris): fetchedBookings don't have attendees information and responses and i don't want to add them to the handler query,
|
||||
// because its used elsewhere in code that does not need that information, so i get ids, fetch bookings and then return them formatted in same order as ids.
|
||||
const ids = fetchedBookings.bookings.map((booking) => booking.id);
|
||||
const bookings = await this.bookingsRepository.getByIdsWithAttendeesAndUserAndEvent(ids);
|
||||
const bookings = await this.bookingsRepository.getByIdsWithAttendeesWithBookingSeatAndUserAndEvent(ids);
|
||||
|
||||
const bookingMap = new Map(bookings.map((booking) => [booking.id, booking]));
|
||||
const orderedBookings = ids.map((id) => bookingMap.get(id));
|
||||
|
||||
const formattedBookings: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[] = [];
|
||||
const formattedBookings: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
| GetRecurringSeatedBookingOutput_2024_08_13
|
||||
)[] = [];
|
||||
for (const booking of orderedBookings) {
|
||||
if (!booking) {
|
||||
continue;
|
||||
@@ -172,8 +219,13 @@ export class BookingsService_2024_08_13 {
|
||||
};
|
||||
|
||||
const isRecurring = !!formatted.recurringEventId;
|
||||
if (isRecurring) {
|
||||
const isSeated = !!formatted.eventType?.seatsPerTimeSlot;
|
||||
if (isRecurring && !isSeated) {
|
||||
formattedBookings.push(this.outputService.getOutputRecurringBooking(formatted));
|
||||
} else if (isRecurring && isSeated) {
|
||||
formattedBookings.push(this.outputService.getOutputRecurringSeatedBooking(formatted));
|
||||
} else if (isSeated) {
|
||||
formattedBookings.push(this.outputService.getOutputSeatedBooking(formatted));
|
||||
} else {
|
||||
formattedBookings.push(this.outputService.getOutputBooking(formatted));
|
||||
}
|
||||
@@ -182,7 +234,7 @@ export class BookingsService_2024_08_13 {
|
||||
return formattedBookings;
|
||||
}
|
||||
|
||||
async rescheduleBooking(request: Request, bookingUid: string, body: RescheduleBookingInput_2024_08_13) {
|
||||
async rescheduleBooking(request: Request, bookingUid: string, body: RescheduleBookingInput) {
|
||||
try {
|
||||
const bookingRequest = await this.inputService.createRescheduleBookingRequest(
|
||||
request,
|
||||
@@ -190,18 +242,34 @@ export class BookingsService_2024_08_13 {
|
||||
body
|
||||
);
|
||||
const booking = await handleNewBooking(bookingRequest);
|
||||
if (!booking.id) {
|
||||
throw new Error("Booking was not created");
|
||||
if (!booking.uid) {
|
||||
throw new Error("Booking missing uid");
|
||||
}
|
||||
|
||||
const databaseBooking = await this.bookingsRepository.getByIdWithAttendeesAndUserAndEvent(booking.id);
|
||||
const databaseBooking =
|
||||
await this.bookingsRepository.getByUidWithAttendeesWithBookingSeatAndUserAndEvent(booking.uid);
|
||||
if (!databaseBooking) {
|
||||
throw new Error(`Booking with id=${booking.id} was not found in the database`);
|
||||
throw new Error(`Booking with uid=${booking.uid} was not found in the database`);
|
||||
}
|
||||
|
||||
if (databaseBooking.recurringEventId) {
|
||||
const isRecurring = !!databaseBooking.recurringEventId;
|
||||
const isSeated = !!databaseBooking.eventType?.seatsPerTimeSlot;
|
||||
|
||||
if (isRecurring && !isSeated) {
|
||||
return this.outputService.getOutputRecurringBooking(databaseBooking);
|
||||
}
|
||||
if (isRecurring && isSeated) {
|
||||
return this.outputService.getOutputCreateRecurringSeatedBooking(
|
||||
databaseBooking,
|
||||
booking?.seatReferenceUid || ""
|
||||
);
|
||||
}
|
||||
if (isSeated) {
|
||||
return this.outputService.getOutputCreateSeatedBooking(
|
||||
databaseBooking,
|
||||
booking.seatReferenceUid || ""
|
||||
);
|
||||
}
|
||||
return this.outputService.getOutputBooking(databaseBooking);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
@@ -213,7 +281,7 @@ export class BookingsService_2024_08_13 {
|
||||
}
|
||||
}
|
||||
|
||||
async cancelBooking(request: Request, bookingUid: string, body: CancelBookingInput_2024_08_13) {
|
||||
async cancelBooking(request: Request, bookingUid: string, body: CancelBookingInput) {
|
||||
const bookingRequest = await this.inputService.createCancelBookingRequest(request, bookingUid, body);
|
||||
await handleCancelBooking(bookingRequest);
|
||||
return this.getBooking(bookingUid);
|
||||
@@ -249,7 +317,7 @@ export class BookingsService_2024_08_13 {
|
||||
}
|
||||
|
||||
async billBooking(booking: CreatedBooking) {
|
||||
const hostId = booking.hosts[0].id;
|
||||
const hostId = booking.hosts?.[0]?.id;
|
||||
if (!hostId) {
|
||||
this.logger.error(`Booking with uid=${booking.uid} has no host`);
|
||||
return;
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
|
||||
import { bookingResponsesSchema } from "@/ee/bookings/2024-08-13/services/output.service";
|
||||
import {
|
||||
bookingResponsesSchema,
|
||||
seatedBookingResponsesSchema,
|
||||
} from "@/ee/bookings/2024-08-13/services/output.service";
|
||||
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
||||
import { hashAPIKey, isApiKey, stripApiKey } from "@/lib/api-key";
|
||||
import { ApiKeyRepository } from "@/modules/api-key/api-key-repository";
|
||||
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
|
||||
@@ -16,13 +20,17 @@ import { z } from "zod";
|
||||
|
||||
import { X_CAL_CLIENT_ID } from "@calcom/platform-constants";
|
||||
import {
|
||||
CancelBookingInput,
|
||||
CancelBookingInput_2024_08_13,
|
||||
CancelSeatedBookingInput_2024_08_13,
|
||||
CreateBookingInput_2024_08_13,
|
||||
CreateInstantBookingInput_2024_08_13,
|
||||
CreateRecurringBookingInput_2024_08_13,
|
||||
GetBookingsInput_2024_08_13,
|
||||
MarkAbsentBookingInput_2024_08_13,
|
||||
RescheduleBookingInput,
|
||||
RescheduleBookingInput_2024_08_13,
|
||||
RescheduleSeatedBookingInput_2024_08_13,
|
||||
} from "@calcom/platform-types";
|
||||
|
||||
type BookingRequest = NextApiRequest & { userId: number | undefined } & OAuthRequestParams;
|
||||
@@ -73,7 +81,8 @@ export class InputBookingsService_2024_08_13 {
|
||||
private readonly eventTypesRepository: EventTypesRepository_2024_06_14,
|
||||
private readonly bookingsRepository: BookingsRepository_2024_08_13,
|
||||
private readonly config: ConfigService,
|
||||
private readonly apiKeyRepository: ApiKeyRepository
|
||||
private readonly apiKeyRepository: ApiKeyRepository,
|
||||
private readonly bookingSeatRepository: BookingSeatRepository
|
||||
) {}
|
||||
|
||||
async createBookingRequest(
|
||||
@@ -239,9 +248,11 @@ export class InputBookingsService_2024_08_13 {
|
||||
async createRescheduleBookingRequest(
|
||||
request: Request,
|
||||
bookingUid: string,
|
||||
body: RescheduleBookingInput_2024_08_13
|
||||
body: RescheduleBookingInput
|
||||
): Promise<BookingRequest> {
|
||||
const bodyTransformed = await this.transformInputRescheduleBooking(bookingUid, body);
|
||||
const bodyTransformed = this.isRescheduleSeatedBody(body)
|
||||
? await this.transformInputRescheduleSeatedBooking(bookingUid, body)
|
||||
: await this.transformInputRescheduleBooking(bookingUid, body);
|
||||
const oAuthClientId = request.get(X_CAL_CLIENT_ID);
|
||||
|
||||
const newRequest = { ...request };
|
||||
@@ -258,6 +269,60 @@ export class InputBookingsService_2024_08_13 {
|
||||
return newRequest as unknown as BookingRequest;
|
||||
}
|
||||
|
||||
isRescheduleSeatedBody(body: RescheduleBookingInput): body is RescheduleSeatedBookingInput_2024_08_13 {
|
||||
return body.hasOwnProperty("seatUid");
|
||||
}
|
||||
|
||||
async transformInputRescheduleSeatedBooking(
|
||||
bookingUid: string,
|
||||
inputBooking: RescheduleSeatedBookingInput_2024_08_13
|
||||
) {
|
||||
const booking = await this.bookingsRepository.getByUidWithAttendeesAndUserAndEvent(bookingUid);
|
||||
// todo create booking seat module, repository and fetch the seat to get info
|
||||
if (!booking) {
|
||||
throw new NotFoundException(`Booking with uid=${bookingUid} not found`);
|
||||
}
|
||||
if (!booking.eventTypeId) {
|
||||
throw new NotFoundException(`Booking with uid=${bookingUid} is missing event type`);
|
||||
}
|
||||
const eventType = await this.eventTypesRepository.getEventTypeByIdWithOwnerAndTeam(booking.eventTypeId);
|
||||
if (!eventType) {
|
||||
throw new NotFoundException(`Event type with id=${booking.eventTypeId} not found`);
|
||||
}
|
||||
|
||||
const seat = await this.bookingSeatRepository.getByReferenceUid(inputBooking.seatUid);
|
||||
if (!seat) {
|
||||
throw new NotFoundException(`Seat with uid=${inputBooking.seatUid} does not exist.`);
|
||||
}
|
||||
|
||||
const { responses: bookingResponses } = seatedBookingResponsesSchema.parse(seat.data);
|
||||
const attendee = booking.attendees.find((attendee) => attendee.email === bookingResponses.email);
|
||||
|
||||
if (!attendee) {
|
||||
throw new NotFoundException(
|
||||
`Attendee with e-mail ${bookingResponses.email} for booking with uid=${bookingUid} and seatUid=${inputBooking.seatUid} not found`
|
||||
);
|
||||
}
|
||||
|
||||
const startTime = DateTime.fromISO(inputBooking.start, { zone: "utc" }).setZone(attendee.timeZone);
|
||||
const endTime = startTime.plus({ minutes: eventType.length });
|
||||
|
||||
return {
|
||||
start: startTime.toISO(),
|
||||
end: endTime.toISO(),
|
||||
eventTypeId: eventType.id,
|
||||
timeZone: attendee.timeZone,
|
||||
language: attendee.locale,
|
||||
// todo(Lauris): expose after refactoring metadata https://app.campsite.co/cal/posts/zysq8w9rwm9c
|
||||
// metadata: booking.metadata || {},
|
||||
metadata: {},
|
||||
hasHashedBookingLink: false,
|
||||
guests: [],
|
||||
responses: { ...bookingResponses },
|
||||
rescheduleUid: inputBooking.seatUid,
|
||||
};
|
||||
}
|
||||
|
||||
async transformInputRescheduleBooking(bookingUid: string, inputBooking: RescheduleBookingInput_2024_08_13) {
|
||||
const booking = await this.bookingsRepository.getByUidWithAttendeesAndUserAndEvent(bookingUid);
|
||||
if (!booking) {
|
||||
@@ -371,9 +436,11 @@ export class InputBookingsService_2024_08_13 {
|
||||
async createCancelBookingRequest(
|
||||
request: Request,
|
||||
bookingUid: string,
|
||||
body: CancelBookingInput_2024_08_13
|
||||
body: CancelBookingInput
|
||||
): Promise<BookingRequest> {
|
||||
const bodyTransformed = await this.transformInputCancelBooking(bookingUid, body);
|
||||
const bodyTransformed = this.isCancelSeatedBody(body)
|
||||
? await this.transformInputCancelSeatedBooking(bookingUid, body)
|
||||
: await this.transformInputCancelBooking(bookingUid, body);
|
||||
const oAuthClientId = request.get(X_CAL_CLIENT_ID);
|
||||
|
||||
const newRequest = { ...request };
|
||||
@@ -389,6 +456,10 @@ export class InputBookingsService_2024_08_13 {
|
||||
return newRequest as unknown as BookingRequest;
|
||||
}
|
||||
|
||||
isCancelSeatedBody(body: CancelBookingInput): body is CancelSeatedBookingInput_2024_08_13 {
|
||||
return body.hasOwnProperty("seatUid");
|
||||
}
|
||||
|
||||
async transformInputCancelBooking(bookingUid: string, inputBooking: CancelBookingInput_2024_08_13) {
|
||||
let allRemainingBookings = false;
|
||||
let uid = bookingUid;
|
||||
@@ -410,6 +481,31 @@ export class InputBookingsService_2024_08_13 {
|
||||
};
|
||||
}
|
||||
|
||||
async transformInputCancelSeatedBooking(
|
||||
bookingUid: string,
|
||||
inputBooking: CancelSeatedBookingInput_2024_08_13
|
||||
) {
|
||||
let allRemainingBookings = false;
|
||||
let uid = bookingUid;
|
||||
const recurringBooking = await this.bookingsRepository.getRecurringByUidWithAttendeesAndUserAndEvent(
|
||||
bookingUid
|
||||
);
|
||||
|
||||
if (recurringBooking.length) {
|
||||
// note(Lauirs): this means that bookingUid is equal to recurringEventId on individual bookings of recurring one aka main recurring event
|
||||
allRemainingBookings = true;
|
||||
// note(Lauirs): we need to set uid as one of the individual recurring ids, not the main recurring event id
|
||||
uid = recurringBooking[0].uid;
|
||||
}
|
||||
|
||||
return {
|
||||
uid,
|
||||
cancellationReason: "",
|
||||
allRemainingBookings,
|
||||
seatReferenceUid: inputBooking.seatUid,
|
||||
};
|
||||
}
|
||||
|
||||
transformInputMarkAbsentBooking(inputBooking: MarkAbsentBookingInput_2024_08_13) {
|
||||
return {
|
||||
noShowHost: inputBooking.host,
|
||||
|
||||
@@ -4,15 +4,34 @@ import { plainToClass } from "class-transformer";
|
||||
import { DateTime } from "luxon";
|
||||
import { z } from "zod";
|
||||
|
||||
import { BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13 } from "@calcom/platform-types";
|
||||
import { Booking } from "@calcom/prisma/client";
|
||||
import {
|
||||
BookingOutput_2024_08_13,
|
||||
CreateRecurringSeatedBookingOutput_2024_08_13,
|
||||
CreateSeatedBookingOutput_2024_08_13,
|
||||
GetRecurringSeatedBookingOutput_2024_08_13,
|
||||
GetSeatedBookingOutput_2024_08_13,
|
||||
RecurringBookingOutput_2024_08_13,
|
||||
SeatedAttendee,
|
||||
} from "@calcom/platform-types";
|
||||
import { Booking, BookingSeat } from "@calcom/prisma/client";
|
||||
|
||||
export const bookingResponsesSchema = z
|
||||
.object({
|
||||
email: z.string(),
|
||||
name: z.string(),
|
||||
guests: z.array(z.string()).optional(),
|
||||
rescheduledReason: z.string().optional(),
|
||||
rescheduleReason: z.string().optional(),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
export const seatedBookingResponsesSchema = z
|
||||
.object({
|
||||
responses: z
|
||||
.object({
|
||||
email: z.string(),
|
||||
name: z.string(),
|
||||
})
|
||||
.passthrough(),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
@@ -27,6 +46,7 @@ type DatabaseBooking = Booking & {
|
||||
timeZone: string;
|
||||
locale: string | null;
|
||||
noShow: boolean | null;
|
||||
bookingSeat?: BookingSeat | null;
|
||||
}[];
|
||||
} & { user: { id: number; name: string | null; email: string } | null };
|
||||
|
||||
@@ -72,17 +92,18 @@ export class OutputBookingsService_2024_08_13 {
|
||||
};
|
||||
|
||||
const bookingTransformed = plainToClass(BookingOutput_2024_08_13, booking, { strategy: "excludeAll" });
|
||||
// note(Lauris): I don't know why plainToClass erases bookings responses so attaching manually
|
||||
bookingTransformed.bookingFieldsResponses = bookingResponses;
|
||||
return bookingTransformed;
|
||||
}
|
||||
|
||||
async getOutputRecurringBookings(databaseBookings: DatabaseBooking[]) {
|
||||
async getOutputRecurringBookings(bookingsIds: number[]) {
|
||||
const transformed = [];
|
||||
|
||||
for (const booking of databaseBookings) {
|
||||
const databaseBooking = await this.bookingsRepository.getByIdWithAttendeesAndUserAndEvent(booking.id);
|
||||
for (const bookingId of bookingsIds) {
|
||||
const databaseBooking = await this.bookingsRepository.getByIdWithAttendeesAndUserAndEvent(bookingId);
|
||||
if (!databaseBooking) {
|
||||
throw new Error(`Booking with id=${booking.id} was not found in the database`);
|
||||
throw new Error(`Booking with id=${bookingId} was not found in the database`);
|
||||
}
|
||||
|
||||
transformed.push(this.getOutputRecurringBooking(databaseBooking));
|
||||
@@ -132,4 +153,159 @@ export class OutputBookingsService_2024_08_13 {
|
||||
|
||||
return plainToClass(RecurringBookingOutput_2024_08_13, booking, { strategy: "excludeAll" });
|
||||
}
|
||||
|
||||
getOutputCreateSeatedBooking(
|
||||
databaseBooking: DatabaseBooking,
|
||||
seatUid: string
|
||||
): CreateSeatedBookingOutput_2024_08_13 {
|
||||
const getSeatedBookingOutput = this.getOutputSeatedBooking(databaseBooking);
|
||||
return { ...getSeatedBookingOutput, seatUid };
|
||||
}
|
||||
|
||||
getOutputSeatedBooking(databaseBooking: DatabaseBooking) {
|
||||
const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString());
|
||||
const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString());
|
||||
const duration = dateEnd.diff(dateStart, "minutes").minutes;
|
||||
|
||||
const booking = {
|
||||
id: databaseBooking.id,
|
||||
uid: databaseBooking.uid,
|
||||
title: databaseBooking.title,
|
||||
description: databaseBooking.description,
|
||||
hosts: [databaseBooking.user],
|
||||
status: databaseBooking.status.toLowerCase(),
|
||||
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
|
||||
start: databaseBooking.startTime,
|
||||
end: databaseBooking.endTime,
|
||||
duration,
|
||||
eventType: databaseBooking.eventType,
|
||||
// note(Lauris): eventTypeId is deprecated
|
||||
eventTypeId: databaseBooking.eventTypeId,
|
||||
attendees: [],
|
||||
location: databaseBooking.location,
|
||||
// note(Lauris): meetingUrl is deprecated
|
||||
meetingUrl: databaseBooking.location,
|
||||
absentHost: !!databaseBooking.noShowHost,
|
||||
};
|
||||
|
||||
const parsed = plainToClass(GetSeatedBookingOutput_2024_08_13, booking, { strategy: "excludeAll" });
|
||||
|
||||
// note(Lauris): I don't know why plainToClass erases booking.attendees[n].responses so attaching manually
|
||||
parsed.attendees = databaseBooking.attendees.map((attendee) => {
|
||||
const { responses } = seatedBookingResponsesSchema.parse(attendee.bookingSeat?.data);
|
||||
|
||||
const attendeeData = {
|
||||
name: attendee.name,
|
||||
email: attendee.email,
|
||||
timeZone: attendee.timeZone,
|
||||
language: attendee.locale,
|
||||
absent: !!attendee.noShow,
|
||||
seatUid: attendee.bookingSeat?.referenceUid,
|
||||
bookingFieldsResponses: {},
|
||||
};
|
||||
const attendeeParsed = plainToClass(SeatedAttendee, attendeeData, { strategy: "excludeAll" });
|
||||
attendeeParsed.bookingFieldsResponses = responses || {};
|
||||
// note(Lauris): as of now email is not returned for privacy
|
||||
delete attendeeParsed.bookingFieldsResponses.email;
|
||||
|
||||
return attendeeParsed;
|
||||
});
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
async getOutputRecurringSeatedBookings(bookingsIds: number[]) {
|
||||
const transformed = [];
|
||||
|
||||
for (const bookingId of bookingsIds) {
|
||||
const databaseBooking =
|
||||
await this.bookingsRepository.getByIdWithAttendeesWithBookingSeatAndUserAndEvent(bookingId);
|
||||
if (!databaseBooking) {
|
||||
throw new Error(`Booking with id=${bookingId} was not found in the database`);
|
||||
}
|
||||
|
||||
transformed.push(this.getOutputRecurringSeatedBooking(databaseBooking));
|
||||
}
|
||||
|
||||
return transformed.sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime());
|
||||
}
|
||||
|
||||
async getOutputCreateRecurringSeatedBookings(bookings: { id: number; seatUid: string }[]) {
|
||||
const transformed = [];
|
||||
|
||||
for (const booking of bookings) {
|
||||
const databaseBooking =
|
||||
await this.bookingsRepository.getByIdWithAttendeesWithBookingSeatAndUserAndEvent(booking.id);
|
||||
if (!databaseBooking) {
|
||||
throw new Error(`Booking with id=${booking.id} was not found in the database`);
|
||||
}
|
||||
transformed.push(this.getOutputCreateRecurringSeatedBooking(databaseBooking, booking.seatUid));
|
||||
}
|
||||
|
||||
return transformed.sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime());
|
||||
}
|
||||
|
||||
getOutputCreateRecurringSeatedBooking(
|
||||
databaseBooking: DatabaseBooking,
|
||||
seatUid: string
|
||||
): CreateRecurringSeatedBookingOutput_2024_08_13 {
|
||||
const getRecurringSeatedBookingOutput = this.getOutputRecurringSeatedBooking(databaseBooking);
|
||||
return { ...getRecurringSeatedBookingOutput, seatUid };
|
||||
}
|
||||
|
||||
getOutputRecurringSeatedBooking(databaseBooking: DatabaseBooking) {
|
||||
const dateStart = DateTime.fromISO(databaseBooking.startTime.toISOString());
|
||||
const dateEnd = DateTime.fromISO(databaseBooking.endTime.toISOString());
|
||||
const duration = dateEnd.diff(dateStart, "minutes").minutes;
|
||||
|
||||
const booking = {
|
||||
id: databaseBooking.id,
|
||||
uid: databaseBooking.uid,
|
||||
title: databaseBooking.title,
|
||||
description: databaseBooking.description,
|
||||
hosts: [databaseBooking.user],
|
||||
status: databaseBooking.status.toLowerCase(),
|
||||
cancellationReason: databaseBooking.cancellationReason || undefined,
|
||||
rescheduledFromUid: databaseBooking.fromReschedule || undefined,
|
||||
start: databaseBooking.startTime,
|
||||
end: databaseBooking.endTime,
|
||||
duration,
|
||||
eventType: databaseBooking.eventType,
|
||||
// note(Lauris): eventTypeId is deprecated
|
||||
eventTypeId: databaseBooking.eventTypeId,
|
||||
attendees: [],
|
||||
location: databaseBooking.location,
|
||||
// note(Lauris): meetingUrl is deprecated
|
||||
meetingUrl: databaseBooking.location,
|
||||
recurringBookingUid: databaseBooking.recurringEventId,
|
||||
absentHost: !!databaseBooking.noShowHost,
|
||||
};
|
||||
|
||||
const parsed = plainToClass(GetRecurringSeatedBookingOutput_2024_08_13, booking, {
|
||||
strategy: "excludeAll",
|
||||
});
|
||||
|
||||
// note(Lauris): I don't know why plainToClass erases booking.attendees[n].responses so attaching manually
|
||||
parsed.attendees = databaseBooking.attendees.map((attendee) => {
|
||||
const { responses } = seatedBookingResponsesSchema.parse(attendee.bookingSeat?.data);
|
||||
|
||||
const attendeeData = {
|
||||
name: attendee.name,
|
||||
email: attendee.email,
|
||||
timeZone: attendee.timeZone,
|
||||
language: attendee.locale,
|
||||
absent: !!attendee.noShow,
|
||||
seatUid: attendee.bookingSeat?.referenceUid,
|
||||
bookingFieldsResponses: {},
|
||||
};
|
||||
const attendeeParsed = plainToClass(SeatedAttendee, attendeeData, { strategy: "excludeAll" });
|
||||
attendeeParsed.bookingFieldsResponses = responses || {};
|
||||
// note(Lauris): as of now email is not returned for privacy
|
||||
delete attendeeParsed.bookingFieldsResponses.email;
|
||||
|
||||
return attendeeParsed;
|
||||
});
|
||||
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
providers: [BookingSeatRepository],
|
||||
exports: [BookingSeatRepository],
|
||||
})
|
||||
export class BookingSeatModule {}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
@Injectable()
|
||||
export class BookingSeatRepository {
|
||||
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||
|
||||
async getByReferenceUid(referenceUid: string) {
|
||||
return this.dbRead.prisma.bookingSeat.findUnique({
|
||||
where: {
|
||||
referenceUid,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
+9
-6
@@ -387,15 +387,18 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
1
|
||||
);
|
||||
|
||||
const responseTeamEvent = responseBody.data[0];
|
||||
expect(responseTeamEvent?.teamId).toEqual(team.id);
|
||||
const responseTeamEvent = responseBody.data.find((event) => event.teamId === team.id);
|
||||
expect(responseTeamEvent).toBeDefined();
|
||||
if (!responseTeamEvent) {
|
||||
throw new Error("Team event not found");
|
||||
}
|
||||
|
||||
const responseTeammate1Event = responseBody.data[1];
|
||||
expect(responseTeammate1Event?.ownerId).toEqual(teammate1.id);
|
||||
const responseTeammate1Event = responseBody.data.find((event) => event.ownerId === teammate1.id);
|
||||
expect(responseTeammate1Event).toBeDefined();
|
||||
expect(responseTeammate1Event?.parentEventTypeId).toEqual(responseTeamEvent?.id);
|
||||
|
||||
const responseTeammate2Event = responseBody.data[2];
|
||||
expect(responseTeammate2Event?.ownerId).toEqual(teammate2.id);
|
||||
const responseTeammate2Event = responseBody.data.find((event) => event.ownerId === teammate2.id);
|
||||
expect(responseTeammate2Event).toBeDefined();
|
||||
expect(responseTeammate2Event?.parentEventTypeId).toEqual(responseTeamEvent?.id);
|
||||
|
||||
managedEventType = responseTeamEvent;
|
||||
|
||||
@@ -33,7 +33,8 @@ export class CreateManagedUserInput {
|
||||
@ApiProperty({
|
||||
example: "America/New_York",
|
||||
description: `Timezone is used to create user's default schedule from Monday to Friday from 9AM to 5PM. If it is not passed then user does not have
|
||||
a default schedule and it must be created manually via the /schedules endpoint. Until the schedule is created, the user can't access availability atom to set his / her availability nor booked.`,
|
||||
a default schedule and it must be created manually via the /schedules endpoint. Until the schedule is created, the user can't access availability atom to set his / her availability nor booked.
|
||||
It will default to Europe/London if not passed.`,
|
||||
})
|
||||
timeZone?: string;
|
||||
|
||||
|
||||
@@ -3020,7 +3020,7 @@
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "Sort results by their creation time (when booking was made) in ascending or descending order.",
|
||||
"example": "?sortEnd=asc OR ?sortEnd=desc",
|
||||
"example": "?sortCreated=asc OR ?sortCreated=desc",
|
||||
"schema": {
|
||||
"enum": [
|
||||
"asc",
|
||||
@@ -3141,16 +3141,6 @@
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/RescheduleBookingInput_2024_08_13"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "",
|
||||
@@ -3191,16 +3181,6 @@
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CancelBookingInput_2024_08_13"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
@@ -5413,7 +5393,7 @@
|
||||
"timeZone": {
|
||||
"type": "string",
|
||||
"example": "America/New_York",
|
||||
"description": "Timezone is used to create user's default schedule from Monday to Friday from 9AM to 5PM. If it is not passed then user does not have\n a default schedule and it must be created manually via the /schedules endpoint. Until the schedule is created, the user can't access availability atom to set his / her availability nor booked."
|
||||
"description": "Timezone is used to create user's default schedule from Monday to Friday from 9AM to 5PM. If it is not passed then user does not have\n a default schedule and it must be created manually via the /schedules endpoint. Until the schedule is created, the user can't access availability atom to set his / her availability nor booked.\n It will default to Europe/London if not passed."
|
||||
},
|
||||
"locale": {
|
||||
"enum": [
|
||||
@@ -12595,8 +12575,7 @@
|
||||
"cancelled",
|
||||
"accepted",
|
||||
"rejected",
|
||||
"pending",
|
||||
"rescheduled"
|
||||
"pending"
|
||||
],
|
||||
"example": "accepted"
|
||||
},
|
||||
@@ -12633,6 +12612,20 @@
|
||||
"eventType": {
|
||||
"$ref": "#/components/schemas/EventType"
|
||||
},
|
||||
"meetingUrl": {
|
||||
"type": "string",
|
||||
"description": "Deprecated - rely on 'location' field instead.",
|
||||
"example": "https://example.com/recurring-meeting",
|
||||
"deprecated": true
|
||||
},
|
||||
"location": {
|
||||
"type": "string",
|
||||
"example": "https://example.com/meeting"
|
||||
},
|
||||
"absentHost": {
|
||||
"type": "boolean",
|
||||
"example": true
|
||||
},
|
||||
"attendees": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -12649,20 +12642,6 @@
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"meetingUrl": {
|
||||
"type": "string",
|
||||
"description": "Deprecated - rely on 'location' field instead.",
|
||||
"example": "https://example.com/recurring-meeting",
|
||||
"deprecated": true
|
||||
},
|
||||
"location": {
|
||||
"type": "string",
|
||||
"example": "https://example.com/meeting"
|
||||
},
|
||||
"absentHost": {
|
||||
"type": "boolean",
|
||||
"example": true
|
||||
},
|
||||
"bookingFieldsResponses": {
|
||||
"type": "object",
|
||||
"description": "Booking field responses consisting of an object with booking field slug as keys and user response as values.",
|
||||
@@ -12683,8 +12662,8 @@
|
||||
"duration",
|
||||
"eventTypeId",
|
||||
"eventType",
|
||||
"attendees",
|
||||
"absentHost"
|
||||
"absentHost",
|
||||
"attendees"
|
||||
]
|
||||
},
|
||||
"RecurringBookingOutput_2024_08_13": {
|
||||
@@ -12692,15 +12671,15 @@
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"example": 456
|
||||
"example": 123
|
||||
},
|
||||
"uid": {
|
||||
"type": "string",
|
||||
"example": "recurring_uid_123"
|
||||
"example": "booking_uid_123"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"example": "Recurring meeting"
|
||||
"example": "Consultation"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
@@ -12720,19 +12699,19 @@
|
||||
"rejected",
|
||||
"pending"
|
||||
],
|
||||
"example": "pending"
|
||||
"example": "accepted"
|
||||
},
|
||||
"cancellationReason": {
|
||||
"type": "string",
|
||||
"example": "Event was cancelled"
|
||||
"example": "User requested cancellation"
|
||||
},
|
||||
"reschedulingReason": {
|
||||
"type": "string",
|
||||
"example": "Event was rescheduled"
|
||||
"example": "User rescheduled the event"
|
||||
},
|
||||
"rescheduledFromUid": {
|
||||
"type": "string",
|
||||
"example": "previous_recurring_uid_123"
|
||||
"example": "previous_uid_123"
|
||||
},
|
||||
"start": {
|
||||
"type": "string",
|
||||
@@ -12744,7 +12723,7 @@
|
||||
},
|
||||
"duration": {
|
||||
"type": "number",
|
||||
"example": 30
|
||||
"example": 60
|
||||
},
|
||||
"eventTypeId": {
|
||||
"type": "number",
|
||||
@@ -12755,9 +12734,19 @@
|
||||
"eventType": {
|
||||
"$ref": "#/components/schemas/EventType"
|
||||
},
|
||||
"recurringBookingUid": {
|
||||
"meetingUrl": {
|
||||
"type": "string",
|
||||
"example": "recurring_uid_987"
|
||||
"description": "Deprecated - rely on 'location' field instead.",
|
||||
"example": "https://example.com/recurring-meeting",
|
||||
"deprecated": true
|
||||
},
|
||||
"location": {
|
||||
"type": "string",
|
||||
"example": "https://example.com/meeting"
|
||||
},
|
||||
"absentHost": {
|
||||
"type": "boolean",
|
||||
"example": true
|
||||
},
|
||||
"attendees": {
|
||||
"type": "array",
|
||||
@@ -12767,34 +12756,24 @@
|
||||
},
|
||||
"guests": {
|
||||
"example": [
|
||||
"guest3@example.com",
|
||||
"guest4@example.com"
|
||||
"guest1@example.com",
|
||||
"guest2@example.com"
|
||||
],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"meetingUrl": {
|
||||
"type": "string",
|
||||
"description": "Deprecated - rely on 'location' field instead.",
|
||||
"example": "https://example.com/recurring-meeting",
|
||||
"deprecated": true
|
||||
},
|
||||
"location": {
|
||||
"type": "string",
|
||||
"example": "https://example.com/recurring-meeting"
|
||||
},
|
||||
"absentHost": {
|
||||
"type": "boolean",
|
||||
"example": false
|
||||
},
|
||||
"bookingFieldsResponses": {
|
||||
"type": "object",
|
||||
"description": "Booking field responses consisting of an object with booking field slug as keys and user response as values.",
|
||||
"example": {
|
||||
"customField": "customValue"
|
||||
}
|
||||
},
|
||||
"recurringBookingUid": {
|
||||
"type": "string",
|
||||
"example": "recurring_uid_987"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -12809,9 +12788,9 @@
|
||||
"duration",
|
||||
"eventTypeId",
|
||||
"eventType",
|
||||
"recurringBookingUid",
|
||||
"absentHost",
|
||||
"attendees",
|
||||
"absentHost"
|
||||
"recurringBookingUid"
|
||||
]
|
||||
},
|
||||
"CreateBookingOutput_2024_08_13": {
|
||||
@@ -12869,6 +12848,18 @@
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/RecurringBookingOutput_2024_08_13"
|
||||
}
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/GetSeatedBookingOutput_2024_08_13"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13"
|
||||
}
|
||||
}
|
||||
],
|
||||
"description": "Booking data, which can be either a BookingOutput object, a RecurringBookingOutput object, or an array of RecurringBookingOutput objects"
|
||||
@@ -12902,6 +12893,12 @@
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/RecurringBookingOutput_2024_08_13"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/GetSeatedBookingOutput_2024_08_13"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -12916,24 +12913,6 @@
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"RescheduleBookingInput_2024_08_13": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"start": {
|
||||
"type": "string",
|
||||
"description": "Start time in ISO 8601 format for the new booking",
|
||||
"example": "2024-08-13T10:00:00Z"
|
||||
},
|
||||
"reschedulingReason": {
|
||||
"type": "string",
|
||||
"example": "User requested reschedule",
|
||||
"description": "Reason for rescheduling the booking"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"start"
|
||||
]
|
||||
},
|
||||
"RescheduleBookingOutput_2024_08_13": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -12952,6 +12931,12 @@
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/RecurringBookingOutput_2024_08_13"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/CreateSeatedBookingOutput_2024_08_13"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/CreateRecurringSeatedBookingOutput_2024_08_13"
|
||||
}
|
||||
],
|
||||
"description": "Booking data, which can be either a BookingOutput object or a RecurringBookingOutput object"
|
||||
@@ -12962,18 +12947,6 @@
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"CancelBookingInput_2024_08_13": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cancellationReason": {
|
||||
"type": "string",
|
||||
"example": "User requested cancellation"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"cancellationReason"
|
||||
]
|
||||
},
|
||||
"CancelBookingOutput_2024_08_13": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -12998,6 +12971,18 @@
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/RecurringBookingOutput_2024_08_13"
|
||||
}
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/GetSeatedBookingOutput_2024_08_13"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/GetRecurringSeatedBookingOutput_2024_08_13"
|
||||
}
|
||||
}
|
||||
],
|
||||
"description": "Booking data, which can be either a BookingOutput object, a RecurringBookingOutput object, or an array of RecurringBookingOutput objects"
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import type { PipeTransform } from "@nestjs/common";
|
||||
import { Injectable, BadRequestException } from "@nestjs/common";
|
||||
import { plainToClass } from "class-transformer";
|
||||
import type { ValidationError } from "class-validator";
|
||||
import { validateSync } from "class-validator";
|
||||
|
||||
import { CancelBookingInput_2024_08_13, CancelSeatedBookingInput_2024_08_13 } from "./cancel-booking.input";
|
||||
|
||||
export type CancelBookingInput = CancelBookingInput_2024_08_13 | CancelSeatedBookingInput_2024_08_13;
|
||||
|
||||
@Injectable()
|
||||
export class CancelBookingInputPipe implements PipeTransform {
|
||||
// note(Lauris): we need empty constructor otherwise v2 can't be started due to error:
|
||||
// CancelBookingInputPipe is not a constructor
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
constructor() {}
|
||||
|
||||
transform(value: CancelBookingInput): CancelBookingInput {
|
||||
if (!value) {
|
||||
throw new BadRequestException("Body is required");
|
||||
}
|
||||
if (typeof value !== "object") {
|
||||
throw new BadRequestException("Body should be an object");
|
||||
}
|
||||
|
||||
if (this.isCancelSeatedBookingInput(value)) {
|
||||
return this.validateCancelBookingSeated(value);
|
||||
}
|
||||
|
||||
return this.validateCancelBooking(value);
|
||||
}
|
||||
|
||||
validateCancelBooking(value: CancelBookingInput_2024_08_13) {
|
||||
const object = plainToClass(CancelBookingInput_2024_08_13, value);
|
||||
|
||||
const errors = validateSync(object, {
|
||||
whitelist: true,
|
||||
forbidNonWhitelisted: true,
|
||||
skipMissingProperties: false,
|
||||
});
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new BadRequestException(this.formatErrors(errors));
|
||||
}
|
||||
|
||||
return object;
|
||||
}
|
||||
|
||||
validateCancelBookingSeated(value: CancelSeatedBookingInput_2024_08_13) {
|
||||
const object = plainToClass(CancelSeatedBookingInput_2024_08_13, value);
|
||||
|
||||
const errors = validateSync(object, {
|
||||
whitelist: true,
|
||||
forbidNonWhitelisted: true,
|
||||
skipMissingProperties: false,
|
||||
});
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new BadRequestException(this.formatErrors(errors));
|
||||
}
|
||||
|
||||
return object;
|
||||
}
|
||||
|
||||
private formatErrors(errors: ValidationError[]): string {
|
||||
return errors
|
||||
.map((err) => {
|
||||
const constraints = err.constraints ? Object.values(err.constraints).join(", ") : "";
|
||||
const childrenErrors =
|
||||
err.children && err.children.length > 0 ? `${this.formatErrors(err.children)}` : "";
|
||||
return `${err.property} property is wrong,${constraints} ${childrenErrors}`;
|
||||
})
|
||||
.join(", ");
|
||||
}
|
||||
|
||||
private isCancelSeatedBookingInput(
|
||||
value: CancelBookingInput
|
||||
): value is CancelSeatedBookingInput_2024_08_13 {
|
||||
return value.hasOwnProperty("seatUid");
|
||||
}
|
||||
}
|
||||
@@ -7,3 +7,13 @@ export class CancelBookingInput_2024_08_13 {
|
||||
@ApiProperty({ example: "User requested cancellation" })
|
||||
cancellationReason?: string;
|
||||
}
|
||||
|
||||
export class CancelSeatedBookingInput_2024_08_13 {
|
||||
@ApiProperty({
|
||||
type: String,
|
||||
example: "3be561a9-31f1-4b8e-aefc-9d9a085f0dd1",
|
||||
description: "Uid of the specific seat withing booking.",
|
||||
})
|
||||
@IsString()
|
||||
seatUid!: string;
|
||||
}
|
||||
|
||||
@@ -185,7 +185,7 @@ export class GetBookingsInput_2024_08_13 {
|
||||
required: false,
|
||||
description:
|
||||
"Sort results by their creation time (when booking was made) in ascending or descending order.",
|
||||
example: "?sortEnd=asc OR ?sortEnd=desc",
|
||||
example: "?sortCreated=asc OR ?sortCreated=desc",
|
||||
enum: SortOrder,
|
||||
})
|
||||
sortCreated?: SortOrderType;
|
||||
|
||||
@@ -4,3 +4,5 @@ export * from "./get-bookings.input";
|
||||
export * from "./reschedule-booking.input";
|
||||
export * from "./cancel-booking.input";
|
||||
export * from "./mark-absent.input";
|
||||
export * from "./reschedule-booking-input.pipe";
|
||||
export * from "./cancel-booking-input.pipe";
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import type { PipeTransform } from "@nestjs/common";
|
||||
import { Injectable, BadRequestException } from "@nestjs/common";
|
||||
import { plainToClass } from "class-transformer";
|
||||
import type { ValidationError } from "class-validator";
|
||||
import { validateSync } from "class-validator";
|
||||
|
||||
import {
|
||||
RescheduleBookingInput_2024_08_13,
|
||||
RescheduleSeatedBookingInput_2024_08_13,
|
||||
} from "./reschedule-booking.input";
|
||||
|
||||
export type RescheduleBookingInput =
|
||||
| RescheduleBookingInput_2024_08_13
|
||||
| RescheduleSeatedBookingInput_2024_08_13;
|
||||
|
||||
@Injectable()
|
||||
export class RescheduleBookingInputPipe implements PipeTransform {
|
||||
// note(Lauris): we need empty constructor otherwise v2 can't be started due to error:
|
||||
// RescheduleBookingInputPipe is not a constructor
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
constructor() {}
|
||||
|
||||
transform(value: RescheduleBookingInput): RescheduleBookingInput {
|
||||
if (!value) {
|
||||
throw new BadRequestException("Body is required");
|
||||
}
|
||||
if (typeof value !== "object") {
|
||||
throw new BadRequestException("Body should be an object");
|
||||
}
|
||||
|
||||
if (this.isSeatedRescheduleInput(value)) {
|
||||
return this.validateSeatedReschedule(value);
|
||||
}
|
||||
|
||||
return this.validateReschedule(value);
|
||||
}
|
||||
|
||||
validateReschedule(value: RescheduleBookingInput_2024_08_13) {
|
||||
const object = plainToClass(RescheduleBookingInput_2024_08_13, value);
|
||||
|
||||
const errors = validateSync(object, {
|
||||
whitelist: true,
|
||||
forbidNonWhitelisted: true,
|
||||
skipMissingProperties: false,
|
||||
});
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new BadRequestException(this.formatErrors(errors));
|
||||
}
|
||||
|
||||
return object;
|
||||
}
|
||||
|
||||
validateSeatedReschedule(value: RescheduleSeatedBookingInput_2024_08_13) {
|
||||
const object = plainToClass(RescheduleSeatedBookingInput_2024_08_13, value);
|
||||
|
||||
const errors = validateSync(object, {
|
||||
whitelist: true,
|
||||
forbidNonWhitelisted: true,
|
||||
skipMissingProperties: false,
|
||||
});
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new BadRequestException(this.formatErrors(errors));
|
||||
}
|
||||
|
||||
return object;
|
||||
}
|
||||
|
||||
private formatErrors(errors: ValidationError[]): string {
|
||||
return errors
|
||||
.map((err) => {
|
||||
const constraints = err.constraints ? Object.values(err.constraints).join(", ") : "";
|
||||
const childrenErrors =
|
||||
err.children && err.children.length > 0 ? `${this.formatErrors(err.children)}` : "";
|
||||
return `${err.property} property is wrong,${constraints} ${childrenErrors}`;
|
||||
})
|
||||
.join(", ");
|
||||
}
|
||||
|
||||
private isSeatedRescheduleInput(
|
||||
value: RescheduleBookingInput
|
||||
): value is RescheduleSeatedBookingInput_2024_08_13 {
|
||||
return "seatUid" in value;
|
||||
}
|
||||
}
|
||||
@@ -18,3 +18,20 @@ export class RescheduleBookingInput_2024_08_13 {
|
||||
})
|
||||
reschedulingReason?: string;
|
||||
}
|
||||
|
||||
export class RescheduleSeatedBookingInput_2024_08_13 {
|
||||
@IsDateString()
|
||||
@ApiProperty({
|
||||
description: "Start time in ISO 8601 format for the new booking",
|
||||
example: "2024-08-13T10:00:00Z",
|
||||
})
|
||||
start!: string;
|
||||
|
||||
@ApiProperty({
|
||||
type: String,
|
||||
example: "3be561a9-31f1-4b8e-aefc-9d9a085f0dd1",
|
||||
description: "Uid of the specific seat withing booking.",
|
||||
})
|
||||
@IsString()
|
||||
seatUid!: string;
|
||||
}
|
||||
|
||||
@@ -40,6 +40,24 @@ class Attendee {
|
||||
absent!: boolean;
|
||||
}
|
||||
|
||||
export class SeatedAttendee extends Attendee {
|
||||
@ApiProperty({ type: String, example: "3be561a9-31f1-4b8e-aefc-9d9a085f0dd1" })
|
||||
@IsString()
|
||||
@Expose()
|
||||
seatUid!: string;
|
||||
|
||||
@ApiProperty({
|
||||
type: Object,
|
||||
description:
|
||||
"Booking field responses consisting of an object with booking field slug as keys and user response as values.",
|
||||
example: { customField: "customValue" },
|
||||
required: false,
|
||||
})
|
||||
@IsObject()
|
||||
@Expose()
|
||||
bookingFieldsResponses!: Record<string, unknown>;
|
||||
}
|
||||
|
||||
class Host {
|
||||
@ApiProperty({ type: Number, example: 1 })
|
||||
@IsInt()
|
||||
@@ -69,7 +87,7 @@ class EventType {
|
||||
slug!: string;
|
||||
}
|
||||
|
||||
export class BookingOutput_2024_08_13 {
|
||||
class BaseBookingOutput_2024_08_13 {
|
||||
@ApiProperty({ type: Number, example: 123 })
|
||||
@IsInt()
|
||||
@Expose()
|
||||
@@ -96,10 +114,10 @@ export class BookingOutput_2024_08_13 {
|
||||
@Expose()
|
||||
hosts!: Host[];
|
||||
|
||||
@ApiProperty({ enum: ["cancelled", "accepted", "rejected", "pending", "rescheduled"], example: "accepted" })
|
||||
@IsEnum(["cancelled", "accepted", "rejected", "pending", "rescheduled"])
|
||||
@ApiProperty({ enum: ["cancelled", "accepted", "rejected", "pending"], example: "accepted" })
|
||||
@IsEnum(["cancelled", "accepted", "rejected", "pending"])
|
||||
@Expose()
|
||||
status!: "cancelled" | "accepted" | "rejected" | "pending" | "rescheduled";
|
||||
status!: "cancelled" | "accepted" | "rejected" | "pending";
|
||||
|
||||
@ApiProperty({ type: String, required: false, example: "User requested cancellation" })
|
||||
@IsString()
|
||||
@@ -149,19 +167,6 @@ export class BookingOutput_2024_08_13 {
|
||||
@Expose()
|
||||
eventType!: EventType;
|
||||
|
||||
@ApiProperty({ type: [Attendee] })
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => Attendee)
|
||||
@Expose()
|
||||
attendees!: Attendee[];
|
||||
|
||||
@ApiProperty({ type: [String], required: false, example: ["guest1@example.com", "guest2@example.com"] })
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
@IsOptional()
|
||||
@Expose()
|
||||
guests?: string[];
|
||||
|
||||
@ApiProperty({
|
||||
type: String,
|
||||
required: false,
|
||||
@@ -183,111 +188,16 @@ export class BookingOutput_2024_08_13 {
|
||||
@IsBoolean()
|
||||
@Expose()
|
||||
absentHost!: boolean;
|
||||
|
||||
@ApiProperty({
|
||||
type: Object,
|
||||
description:
|
||||
"Booking field responses consisting of an object with booking field slug as keys and user response as values.",
|
||||
example: { customField: "customValue" },
|
||||
required: false,
|
||||
})
|
||||
@IsObject()
|
||||
@Expose()
|
||||
bookingFieldsResponses!: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export class RecurringBookingOutput_2024_08_13 {
|
||||
@ApiProperty({ type: Number, example: 456 })
|
||||
@IsInt()
|
||||
@Expose()
|
||||
id!: number;
|
||||
|
||||
@ApiProperty({ type: String, example: "recurring_uid_123" })
|
||||
@IsString()
|
||||
@Expose()
|
||||
uid!: string;
|
||||
|
||||
@ApiProperty({ type: String, example: "Recurring meeting" })
|
||||
@IsString()
|
||||
@Expose()
|
||||
title!: string;
|
||||
|
||||
@ApiProperty({ type: String, example: "Learn how to integrate scheduling into marketplace." })
|
||||
@IsString()
|
||||
@Expose()
|
||||
description!: string;
|
||||
|
||||
@ApiProperty({ type: [Host] })
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => Host)
|
||||
@Expose()
|
||||
hosts!: Host[];
|
||||
|
||||
@ApiProperty({ enum: ["cancelled", "accepted", "rejected", "pending"], example: "pending" })
|
||||
@IsEnum(["cancelled", "accepted", "rejected", "pending"])
|
||||
@Expose()
|
||||
status!: "cancelled" | "accepted" | "rejected" | "pending";
|
||||
|
||||
@ApiProperty({ type: String, required: false, example: "Event was cancelled" })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Expose()
|
||||
cancellationReason?: string;
|
||||
|
||||
@ApiProperty({ type: String, required: false, example: "Event was rescheduled" })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Expose()
|
||||
reschedulingReason?: string;
|
||||
|
||||
@ApiProperty({ type: String, required: false, example: "previous_recurring_uid_123" })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Expose()
|
||||
rescheduledFromUid?: string;
|
||||
|
||||
@ApiProperty({ type: String, example: "2024-08-13T15:30:00Z" })
|
||||
@IsDateString()
|
||||
@Expose()
|
||||
start!: string;
|
||||
|
||||
@ApiProperty({ type: String, example: "2024-08-13T16:30:00Z" })
|
||||
@IsDateString()
|
||||
@Expose()
|
||||
end!: string;
|
||||
|
||||
@ApiProperty({ type: Number, example: 30 })
|
||||
@IsInt()
|
||||
@Expose()
|
||||
duration!: number;
|
||||
|
||||
@ApiProperty({
|
||||
type: Number,
|
||||
example: 50,
|
||||
deprecated: true,
|
||||
description: "Deprecated - rely on 'eventType' object containing the id instead.",
|
||||
})
|
||||
@IsInt()
|
||||
@Expose()
|
||||
eventTypeId!: number;
|
||||
|
||||
@ApiProperty({ type: EventType })
|
||||
@Type(() => EventType)
|
||||
@Expose()
|
||||
eventType!: EventType;
|
||||
|
||||
@ApiProperty({ type: String, example: "recurring_uid_987" })
|
||||
@IsString()
|
||||
@Expose()
|
||||
recurringBookingUid!: string;
|
||||
|
||||
export class BookingOutput_2024_08_13 extends BaseBookingOutput_2024_08_13 {
|
||||
@ApiProperty({ type: [Attendee] })
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => Attendee)
|
||||
@Expose()
|
||||
attendees!: Attendee[];
|
||||
|
||||
@ApiProperty({ type: [String], required: false, example: ["guest3@example.com", "guest4@example.com"] })
|
||||
@ApiProperty({ type: [String], required: false, example: ["guest1@example.com", "guest2@example.com"] })
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
@IsOptional()
|
||||
@@ -295,26 +205,22 @@ export class RecurringBookingOutput_2024_08_13 {
|
||||
guests?: string[];
|
||||
|
||||
@ApiProperty({
|
||||
type: String,
|
||||
type: Object,
|
||||
description:
|
||||
"Booking field responses consisting of an object with booking field slug as keys and user response as values.",
|
||||
example: { customField: "customValue" },
|
||||
required: false,
|
||||
description: "Deprecated - rely on 'location' field instead.",
|
||||
example: "https://example.com/recurring-meeting",
|
||||
deprecated: true,
|
||||
})
|
||||
@IsUrl()
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
@Expose()
|
||||
meetingUrl?: string;
|
||||
bookingFieldsResponses!: Record<string, unknown>;
|
||||
}
|
||||
|
||||
@ApiProperty({ type: String, required: false, example: "https://example.com/recurring-meeting" })
|
||||
@IsOptional()
|
||||
export class RecurringBookingOutput_2024_08_13 extends BookingOutput_2024_08_13 {
|
||||
@ApiProperty({ type: String, example: "recurring_uid_987" })
|
||||
@IsString()
|
||||
@Expose()
|
||||
location!: string;
|
||||
|
||||
@ApiProperty({ type: Boolean, example: false })
|
||||
@IsBoolean()
|
||||
@Expose()
|
||||
absentHost!: boolean;
|
||||
recurringBookingUid!: string;
|
||||
|
||||
@ApiProperty({
|
||||
type: Object,
|
||||
@@ -327,3 +233,61 @@ export class RecurringBookingOutput_2024_08_13 {
|
||||
@Expose()
|
||||
bookingFieldsResponses!: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export class GetSeatedBookingOutput_2024_08_13 extends BaseBookingOutput_2024_08_13 {
|
||||
@ApiProperty({ type: [SeatedAttendee] })
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => SeatedAttendee)
|
||||
@Expose()
|
||||
attendees!: SeatedAttendee[];
|
||||
}
|
||||
|
||||
export class GetRecurringSeatedBookingOutput_2024_08_13 extends BaseBookingOutput_2024_08_13 {
|
||||
@ApiProperty({ type: [SeatedAttendee] })
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => SeatedAttendee)
|
||||
@Expose()
|
||||
attendees!: SeatedAttendee[];
|
||||
|
||||
@ApiProperty({ type: String, example: "recurring_uid_987" })
|
||||
@IsString()
|
||||
@Expose()
|
||||
recurringBookingUid!: string;
|
||||
}
|
||||
|
||||
// note(Lauris): CreateSeatedBookingOutput_2024_08_13 is the same as GetSeatedBookingOutput_2024_08_13 except it has seatUid, so instead of extending BaseBookingOutput_2024_08_13
|
||||
// we could extend GetSeatedBookingOutput_2024_08_13 but the problem then is that attendees end up at the top of the response even above id
|
||||
// or uid keys making it harder to read and understand the response, so i decided to duplicate the fields here and the response is as expected - with seatUid and attendees at the bottom.
|
||||
export class CreateSeatedBookingOutput_2024_08_13 extends BaseBookingOutput_2024_08_13 {
|
||||
@ApiProperty({ type: String, example: "3be561a9-31f1-4b8e-aefc-9d9a085f0dd1" })
|
||||
@IsString()
|
||||
@Expose()
|
||||
seatUid!: string;
|
||||
|
||||
@ApiProperty({ type: [SeatedAttendee] })
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => SeatedAttendee)
|
||||
@Expose()
|
||||
attendees!: SeatedAttendee[];
|
||||
}
|
||||
|
||||
// note(Lauris): CreateRecurringSeatedBookingOutput_2024_08_13 is the same as GetRecurringSeatedBookingOutput_2024_08_13 except it has seatUid, so instead of extending BaseBookingOutput_2024_08_13
|
||||
// we could extend GetRecurringSeatedBookingOutput_2024_08_13 but the problem then is that attendees and recurringBookingUid end up at the top of the response even above id
|
||||
// or uid keys making it harder to read and understand the response, so i decided to duplicate the fields here and the response is as expected - with seatUid, attendees and recurringBookingUid at the bottom.
|
||||
export class CreateRecurringSeatedBookingOutput_2024_08_13 extends BaseBookingOutput_2024_08_13 {
|
||||
@ApiProperty({ type: String, example: "3be561a9-31f1-4b8e-aefc-9d9a085f0dd1" })
|
||||
@IsString()
|
||||
@Expose()
|
||||
seatUid!: string;
|
||||
|
||||
@ApiProperty({ type: [SeatedAttendee] })
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => SeatedAttendee)
|
||||
@Expose()
|
||||
attendees!: SeatedAttendee[];
|
||||
|
||||
@ApiProperty({ type: String, example: "recurring_uid_987" })
|
||||
@IsString()
|
||||
@Expose()
|
||||
recurringBookingUid!: string;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,12 @@ import { Type } from "class-transformer";
|
||||
import { IsEnum, ValidateNested } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
import { BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13 } from "@calcom/platform-types";
|
||||
import {
|
||||
BookingOutput_2024_08_13,
|
||||
GetSeatedBookingOutput_2024_08_13,
|
||||
RecurringBookingOutput_2024_08_13,
|
||||
GetRecurringSeatedBookingOutput_2024_08_13,
|
||||
} from "@calcom/platform-types";
|
||||
|
||||
@ApiExtraModels(BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13)
|
||||
export class GetBookingOutput_2024_08_13 {
|
||||
@@ -17,12 +22,21 @@ export class GetBookingOutput_2024_08_13 {
|
||||
{ $ref: getSchemaPath(BookingOutput_2024_08_13) },
|
||||
{ $ref: getSchemaPath(RecurringBookingOutput_2024_08_13) },
|
||||
{ type: "array", items: { $ref: getSchemaPath(RecurringBookingOutput_2024_08_13) } },
|
||||
{ $ref: getSchemaPath(GetSeatedBookingOutput_2024_08_13) },
|
||||
{ $ref: getSchemaPath(GetRecurringSeatedBookingOutput_2024_08_13) },
|
||||
{ type: "array", items: { $ref: getSchemaPath(GetRecurringSeatedBookingOutput_2024_08_13) } },
|
||||
],
|
||||
description:
|
||||
"Booking data, which can be either a BookingOutput object, a RecurringBookingOutput object, or an array of RecurringBookingOutput objects",
|
||||
})
|
||||
@Type(() => Object)
|
||||
data!: BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13[];
|
||||
data!:
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13[]
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
| GetRecurringSeatedBookingOutput_2024_08_13
|
||||
| GetRecurringSeatedBookingOutput_2024_08_13[];
|
||||
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,12 @@ import { ApiProperty, ApiExtraModels, getSchemaPath } from "@nestjs/swagger";
|
||||
import { IsEnum, ValidateNested } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
import { BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13 } from "@calcom/platform-types";
|
||||
import {
|
||||
BookingOutput_2024_08_13,
|
||||
GetRecurringSeatedBookingOutput_2024_08_13,
|
||||
GetSeatedBookingOutput_2024_08_13,
|
||||
RecurringBookingOutput_2024_08_13,
|
||||
} from "@calcom/platform-types";
|
||||
|
||||
@ApiExtraModels(BookingOutput_2024_08_13, RecurringBookingOutput_2024_08_13)
|
||||
export class GetBookingsOutput_2024_08_13 {
|
||||
@@ -16,13 +21,20 @@ export class GetBookingsOutput_2024_08_13 {
|
||||
oneOf: [
|
||||
{ $ref: getSchemaPath(BookingOutput_2024_08_13) },
|
||||
{ $ref: getSchemaPath(RecurringBookingOutput_2024_08_13) },
|
||||
{ $ref: getSchemaPath(GetSeatedBookingOutput_2024_08_13) },
|
||||
{ $ref: getSchemaPath(GetRecurringSeatedBookingOutput_2024_08_13) },
|
||||
],
|
||||
},
|
||||
description:
|
||||
"Array of booking data, which can contain either BookingOutput objects or RecurringBookingOutput objects",
|
||||
})
|
||||
@ValidateNested({ each: true })
|
||||
data!: (BookingOutput_2024_08_13 | RecurringBookingOutput_2024_08_13)[];
|
||||
data!: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
| GetRecurringSeatedBookingOutput_2024_08_13
|
||||
)[];
|
||||
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user