feat: endpoint to update location for existing booking (#25367)
* feat: init endpoint to update booking location * chore: update controller * chore: DTOs for update location endpoint * chore: add update booking method * fix: refactor * feat: service update booking location * chore: update platform libraries * chore: update exports * chore: e2e tests for updating booking location endpoint * chore: update e2e tests for updating booking location endpoint * chore: update bookings module * fixup * chore: update configs * fixup: revert changes * chore: remove unused handlers * fix: update inputs * refactor: booking location service * fixup * revert: unnecessary changes * chore: cleanup * fix: e2e tests * chore: implement cubic feedback * chore: implement feedback * fixup * chore: more PR feedback
This commit is contained in:
1 parent
6ebd401953
commit
f9ff3248e5
11 files changed
+833
-17
No files matched your search
@@ -1,9 +1,11 @@
|
||||
import { BookingGuestsController_2024_08_13 } from "@/ee/bookings/2024-08-13/controllers/booking-guests.controller";
|
||||
import { BookingLocationController_2024_08_13 } from "@/ee/bookings/2024-08-13/controllers/booking-location.controller";
|
||||
import { BookingsController_2024_08_13 } from "@/ee/bookings/2024-08-13/controllers/bookings.controller";
|
||||
import { BookingPbacGuard } from "@/ee/bookings/2024-08-13/guards/booking-pbac.guard";
|
||||
import { BookingReferencesRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/booking-references.repository";
|
||||
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository";
|
||||
import { BookingGuestsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-guests.service";
|
||||
import { BookingLocationService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-location.service";
|
||||
import { BookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-references.service";
|
||||
import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service";
|
||||
import { CalVideoOutputService } from "@/ee/bookings/2024-08-13/services/cal-video.output.service";
|
||||
@@ -100,8 +102,13 @@ import { Module } from "@nestjs/common";
|
||||
CalVideoService,
|
||||
CalVideoOutputService,
|
||||
BookingPbacGuard,
|
||||
BookingLocationService_2024_08_13,
|
||||
],
|
||||
controllers: [
|
||||
BookingsController_2024_08_13,
|
||||
BookingGuestsController_2024_08_13,
|
||||
BookingLocationController_2024_08_13,
|
||||
],
|
||||
controllers: [BookingsController_2024_08_13, BookingGuestsController_2024_08_13],
|
||||
exports: [InputBookingsService_2024_08_13, OutputBookingsService_2024_08_13, BookingsService_2024_08_13],
|
||||
})
|
||||
export class BookingsModule_2024_08_13 {}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { BookingUidGuard } from "@/ee/bookings/2024-08-13/guards/booking-uid.guard";
|
||||
import { UpdateBookingLocationOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/update-location.output";
|
||||
import { BookingLocationService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-location.service";
|
||||
import { VERSION_2024_08_13_VALUE, VERSION_2024_08_13 } from "@/lib/api-versions";
|
||||
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
|
||||
import { Controller, Patch, Logger, Body, UseGuards, Param, HttpCode, HttpStatus } from "@nestjs/common";
|
||||
import { ApiOperation, ApiTags as DocsTags, ApiHeader } from "@nestjs/swagger";
|
||||
|
||||
import { BOOKING_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { UpdateBookingLocationInput_2024_08_13 } from "@calcom/platform-types";
|
||||
|
||||
@Controller({
|
||||
path: "/v2/bookings/:bookingUid/location",
|
||||
version: VERSION_2024_08_13_VALUE,
|
||||
})
|
||||
@UseGuards(PermissionsGuard)
|
||||
@DocsTags("Bookings")
|
||||
@ApiHeader({
|
||||
name: "cal-api-version",
|
||||
description: `Must be set to ${VERSION_2024_08_13}. This header is required as this endpoint does not exist in older API versions.`,
|
||||
example: VERSION_2024_08_13,
|
||||
required: true,
|
||||
})
|
||||
export class BookingLocationController_2024_08_13 {
|
||||
private readonly logger = new Logger("BookingLocationController_2024_08_13");
|
||||
|
||||
constructor(private readonly bookingLocationService: BookingLocationService_2024_08_13) {}
|
||||
|
||||
@Patch("/")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Permissions([BOOKING_WRITE])
|
||||
@UseGuards(ApiAuthGuard, BookingUidGuard)
|
||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||
@ApiOperation({
|
||||
summary: "Update booking location for an existing booking",
|
||||
description: `**Current Limitation:** Updating a booking location will update the location in Cal.com, but the corresponding Calendar event will not be updated automatically. The old location will persist in the Calendar event. This is a known limitation that will be addressed in a future update.
|
||||
|
||||
<Note>The cal-api-version header is required for this endpoint. Without it, the request will fail with a 404 error.</Note>`,
|
||||
})
|
||||
async updateBookingLocation(
|
||||
@Param("bookingUid") bookingUid: string,
|
||||
@Body() body: UpdateBookingLocationInput_2024_08_13,
|
||||
@GetUser() user: ApiAuthGuardUser
|
||||
): Promise<UpdateBookingLocationOutput_2024_08_13> {
|
||||
const booking = await this.bookingLocationService.updateBookingLocation(bookingUid, body, user);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: booking,
|
||||
};
|
||||
}
|
||||
}
|
||||
+492
@@ -0,0 +1,492 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { UpdateBookingLocationOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/update-location.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 * 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 { TokensRepositoryFixture } from "test/fixtures/repository/tokens.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { randomString } from "test/utils/randomString";
|
||||
|
||||
import {
|
||||
CAL_API_VERSION_HEADER,
|
||||
ERROR_STATUS,
|
||||
SUCCESS_STATUS,
|
||||
VERSION_2024_08_13,
|
||||
} from "@calcom/platform-constants";
|
||||
import { UpdateBookingLocationInput_2024_08_13 } from "@calcom/platform-types";
|
||||
import type { BookingOutput_2024_08_13 } from "@calcom/platform-types";
|
||||
import { Booking } from "@calcom/prisma/client";
|
||||
import type { Team } from "@calcom/prisma/client";
|
||||
|
||||
type TestUser = {
|
||||
id: number;
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
email: string;
|
||||
};
|
||||
|
||||
type TestSetup = {
|
||||
organizer: TestUser;
|
||||
unrelatedUser: TestUser;
|
||||
eventTypeId: number;
|
||||
};
|
||||
|
||||
describe("Bookings Endpoints 2024-08-13 update booking location", () => {
|
||||
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 teamRepositoryFixture: TeamRepositoryFixture;
|
||||
let tokensRepositoryFixture: TokensRepositoryFixture;
|
||||
|
||||
let testSetup: TestSetup;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await 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);
|
||||
tokensRepositoryFixture = new TokensRepositoryFixture(moduleRef);
|
||||
|
||||
organization = await teamRepositoryFixture.create({
|
||||
name: `update-booking-location-organization-${randomString()}`,
|
||||
});
|
||||
|
||||
await setupTestData();
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
describe("PATCH /v2/bookings/:bookingUid/location", () => {
|
||||
it("should return 401 when updating location without authentication", async () => {
|
||||
const { bookingUid } = await createBooking();
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.patch(`/v2/bookings/${bookingUid}/location`)
|
||||
.send({ location: "https://unauthenticated.example.com" })
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(401);
|
||||
});
|
||||
|
||||
describe("can update booking with different locations", () => {
|
||||
const address = "123 Main St";
|
||||
const link = "https://cal.com/join/123456";
|
||||
const phone = "+37121999999";
|
||||
|
||||
let booking: Booking;
|
||||
let bookingId: number;
|
||||
let bookingUid: string;
|
||||
let eventTypeWithAllLocationsId: number;
|
||||
|
||||
beforeAll(async () => {
|
||||
const createdEventType = await eventTypesRepositoryFixture.create(
|
||||
{
|
||||
title: "book using any location",
|
||||
slug: `book-using-any-location-${randomString()}`,
|
||||
length: 15,
|
||||
locations: [
|
||||
{ type: "integrations:daily" },
|
||||
{ type: "inPerson", address: "123 Main St", displayLocationPublicly: true },
|
||||
{ type: "link", link: "https://cal.com/join/123456", displayLocationPublicly: true },
|
||||
{ type: "userPhone", hostPhoneNumber: "+37121999999", displayLocationPublicly: true },
|
||||
{ type: "attendeeInPerson" },
|
||||
{ type: "phone" },
|
||||
{ type: "somewhereElse" },
|
||||
],
|
||||
},
|
||||
testSetup.organizer.id
|
||||
);
|
||||
|
||||
eventTypeWithAllLocationsId = createdEventType.id;
|
||||
|
||||
booking = await bookingsRepositoryFixture.create({
|
||||
uid: `booking-uid-${randomString(10)}`,
|
||||
title: "booking title",
|
||||
startTime: "2048-08-14T09:00:00.000Z",
|
||||
endTime: "2048-08-14T10:00:00.000Z",
|
||||
eventType: {
|
||||
connect: {
|
||||
id: eventTypeWithAllLocationsId,
|
||||
},
|
||||
},
|
||||
status: "ACCEPTED",
|
||||
metadata: {},
|
||||
responses: "null",
|
||||
user: {
|
||||
connect: {
|
||||
id: testSetup.organizer.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
bookingUid = booking.uid;
|
||||
bookingId = booking.id;
|
||||
});
|
||||
|
||||
it("can update location to type address", async () => {
|
||||
const updatedBookingBody: UpdateBookingLocationInput_2024_08_13 = {
|
||||
location: {
|
||||
type: "address",
|
||||
address: address,
|
||||
},
|
||||
};
|
||||
|
||||
const updatedBookingResponse = await request(app.getHttpServer())
|
||||
.patch(`/v2/bookings/${bookingUid}/location`)
|
||||
.send(updatedBookingBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${testSetup.organizer.accessToken}`)
|
||||
.expect(200);
|
||||
|
||||
const updatedBookingResponseBody: UpdateBookingLocationOutput_2024_08_13 =
|
||||
updatedBookingResponse.body;
|
||||
expect(updatedBookingResponseBody.status).toEqual(SUCCESS_STATUS);
|
||||
if (!responseDataIsBooking(updatedBookingResponseBody.data)) {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
const updatedBooking = updatedBookingResponseBody.data as BookingOutput_2024_08_13;
|
||||
expect(updatedBooking).toHaveProperty("id");
|
||||
expect(updatedBooking.location).toEqual(address);
|
||||
});
|
||||
|
||||
it("can update location to type link", async () => {
|
||||
const updatedBookingBody: UpdateBookingLocationInput_2024_08_13 = {
|
||||
location: {
|
||||
type: "link",
|
||||
link: link,
|
||||
},
|
||||
};
|
||||
|
||||
const updatedBookingResponse = await request(app.getHttpServer())
|
||||
.patch(`/v2/bookings/${bookingUid}/location`)
|
||||
.send(updatedBookingBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${testSetup.organizer.accessToken}`)
|
||||
.expect(200);
|
||||
|
||||
const updatedBookingResponseBody: UpdateBookingLocationOutput_2024_08_13 =
|
||||
updatedBookingResponse.body;
|
||||
expect(updatedBookingResponseBody.status).toEqual(SUCCESS_STATUS);
|
||||
if (!responseDataIsBooking(updatedBookingResponseBody.data)) {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
const updatedBooking = updatedBookingResponseBody.data as BookingOutput_2024_08_13;
|
||||
expect(updatedBooking).toHaveProperty("id");
|
||||
expect(updatedBooking.location).toEqual(link);
|
||||
});
|
||||
|
||||
it("can update location to type phone", async () => {
|
||||
const updatedBookingBody: UpdateBookingLocationInput_2024_08_13 = {
|
||||
location: {
|
||||
type: "phone",
|
||||
phone: phone,
|
||||
},
|
||||
};
|
||||
|
||||
const updatedBookingResponse = await request(app.getHttpServer())
|
||||
.patch(`/v2/bookings/${bookingUid}/location`)
|
||||
.send(updatedBookingBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${testSetup.organizer.accessToken}`)
|
||||
.expect(200);
|
||||
|
||||
const updatedBookingResponseBody: UpdateBookingLocationOutput_2024_08_13 =
|
||||
updatedBookingResponse.body;
|
||||
expect(updatedBookingResponseBody.status).toEqual(SUCCESS_STATUS);
|
||||
if (!responseDataIsBooking(updatedBookingResponseBody.data)) {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
const updatedBooking = updatedBookingResponseBody.data as BookingOutput_2024_08_13;
|
||||
expect(updatedBooking).toHaveProperty("id");
|
||||
expect(updatedBooking.location).toEqual(phone);
|
||||
});
|
||||
|
||||
it("can update location to type attendeeAddress", async () => {
|
||||
const attendeeAddress = "123 Example St, City, Valhalla";
|
||||
const updatedBookingBody: UpdateBookingLocationInput_2024_08_13 = {
|
||||
location: {
|
||||
type: "attendeeAddress",
|
||||
address: attendeeAddress,
|
||||
},
|
||||
};
|
||||
|
||||
const updatedBookingResponse = await request(app.getHttpServer())
|
||||
.patch(`/v2/bookings/${bookingUid}/location`)
|
||||
.send(updatedBookingBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${testSetup.organizer.accessToken}`)
|
||||
.expect(200);
|
||||
|
||||
const updatedBookingResponseBody: UpdateBookingLocationOutput_2024_08_13 =
|
||||
updatedBookingResponse.body;
|
||||
expect(updatedBookingResponseBody.status).toEqual(SUCCESS_STATUS);
|
||||
if (!responseDataIsBooking(updatedBookingResponseBody.data)) {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
const updatedBooking = updatedBookingResponseBody.data as BookingOutput_2024_08_13;
|
||||
expect(updatedBooking).toHaveProperty("id");
|
||||
expect(updatedBooking.location).toEqual(attendeeAddress);
|
||||
});
|
||||
|
||||
it("can update location to type attendeePhone", async () => {
|
||||
const attendeePhone = "+37120993151";
|
||||
const updatedBookingBody: UpdateBookingLocationInput_2024_08_13 = {
|
||||
location: {
|
||||
type: "attendeePhone",
|
||||
phone: attendeePhone,
|
||||
},
|
||||
};
|
||||
|
||||
const updatedBookingResponse = await request(app.getHttpServer())
|
||||
.patch(`/v2/bookings/${bookingUid}/location`)
|
||||
.send(updatedBookingBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${testSetup.organizer.accessToken}`)
|
||||
.expect(200);
|
||||
|
||||
const updatedBookingResponseBody: UpdateBookingLocationOutput_2024_08_13 =
|
||||
updatedBookingResponse.body;
|
||||
expect(updatedBookingResponseBody.status).toEqual(SUCCESS_STATUS);
|
||||
if (!responseDataIsBooking(updatedBookingResponseBody.data)) {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
const updatedBooking = updatedBookingResponseBody.data as BookingOutput_2024_08_13;
|
||||
expect(updatedBooking).toHaveProperty("id");
|
||||
expect(updatedBooking.location).toEqual(attendeePhone);
|
||||
});
|
||||
|
||||
it("can update location to type attendeeDefined", async () => {
|
||||
const attendeeDefinedLocation = "namek 100";
|
||||
const updatedBookingBody: UpdateBookingLocationInput_2024_08_13 = {
|
||||
location: {
|
||||
type: "attendeeDefined",
|
||||
location: attendeeDefinedLocation,
|
||||
},
|
||||
};
|
||||
|
||||
const updatedBookingResponse = await request(app.getHttpServer())
|
||||
.patch(`/v2/bookings/${bookingUid}/location`)
|
||||
.send(updatedBookingBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${testSetup.organizer.accessToken}`)
|
||||
.expect(200);
|
||||
|
||||
const updatedBookingResponseBody: UpdateBookingLocationOutput_2024_08_13 =
|
||||
updatedBookingResponse.body;
|
||||
expect(updatedBookingResponseBody.status).toEqual(SUCCESS_STATUS);
|
||||
if (!responseDataIsBooking(updatedBookingResponseBody.data)) {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
const updatedBooking = updatedBookingResponseBody.data as BookingOutput_2024_08_13;
|
||||
expect(updatedBooking).toHaveProperty("id");
|
||||
expect(updatedBooking.location).toEqual(attendeeDefinedLocation);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (responseDataIsBooking(booking)) {
|
||||
await bookingsRepositoryFixture.deleteById(bookingId);
|
||||
} else {
|
||||
throw new Error("Unexpected response data type");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should return booking unchanged when no location payload is provided", async () => {
|
||||
const initialLocation = `https://initial-${randomString()}.example.com`;
|
||||
const { bookingUid } = await createBooking(initialLocation);
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.patch(`/v2/bookings/${bookingUid}/location`)
|
||||
.send({})
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${testSetup.organizer.accessToken}`)
|
||||
.expect(200);
|
||||
|
||||
const responseBody: UpdateBookingLocationOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
if (!responseDataIsBooking(responseBody.data)) {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
expect(responseBody.data.location).toEqual(initialLocation);
|
||||
});
|
||||
|
||||
it("should return 403 when unrelated user attempts to update booking location", async () => {
|
||||
const { bookingUid } = await createBooking();
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.patch(`/v2/bookings/${bookingUid}/location`)
|
||||
.send({ location: { type: "link", link: "https://non-organizer.example.com" } })
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${testSetup.unrelatedUser.accessToken}`)
|
||||
.expect(403);
|
||||
});
|
||||
|
||||
it("should return 400 when invalid location payload is provided", async () => {
|
||||
const { bookingUid } = await createBooking();
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.patch(`/v2/bookings/${bookingUid}/location`)
|
||||
.send({ location: { type: "invalid-location-type" } })
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${testSetup.organizer.accessToken}`)
|
||||
.expect(400);
|
||||
|
||||
expect(response.body.status).toEqual(ERROR_STATUS);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await teamRepositoryFixture.delete(organization.id);
|
||||
|
||||
await userRepositoryFixture.deleteByEmail(testSetup.organizer.email);
|
||||
await userRepositoryFixture.deleteByEmail(testSetup.unrelatedUser.email);
|
||||
|
||||
await bookingsRepositoryFixture.deleteAllBookings(testSetup.organizer.id, testSetup.organizer.email);
|
||||
|
||||
await app.close();
|
||||
});
|
||||
|
||||
async function setupTestData() {
|
||||
const oAuthClient = await createOAuthClient(organization.id);
|
||||
|
||||
const organizerUser = await userRepositoryFixture.create({
|
||||
email: `update-booking-location-organizer-${randomString()}@api.com`,
|
||||
platformOAuthClients: {
|
||||
connect: { id: oAuthClient.id },
|
||||
},
|
||||
});
|
||||
|
||||
const unrelatedUser = await userRepositoryFixture.create({
|
||||
email: `update-booking-location-unrelated-${randomString()}@api.com`,
|
||||
platformOAuthClients: {
|
||||
connect: { id: oAuthClient.id },
|
||||
},
|
||||
});
|
||||
|
||||
const organizerTokens = await tokensRepositoryFixture.createTokens(organizerUser.id, oAuthClient.id);
|
||||
const unrelatedTokens = await tokensRepositoryFixture.createTokens(unrelatedUser.id, oAuthClient.id);
|
||||
|
||||
const userSchedule: CreateScheduleInput_2024_04_15 = {
|
||||
name: `update-booking-location-schedule-${randomString()}`,
|
||||
timeZone: "Europe/Rome",
|
||||
isDefault: true,
|
||||
};
|
||||
await schedulesService.createUserSchedule(organizerUser.id, userSchedule);
|
||||
|
||||
const eventType = await eventTypesRepositoryFixture.create(
|
||||
{
|
||||
title: `update-booking-location-event-type-${randomString()}`,
|
||||
slug: `update-booking-location-event-type-${randomString()}`,
|
||||
length: 45,
|
||||
},
|
||||
organizerUser.id
|
||||
);
|
||||
|
||||
testSetup = {
|
||||
organizer: {
|
||||
id: organizerUser.id,
|
||||
email: organizerUser.email,
|
||||
accessToken: organizerTokens.accessToken,
|
||||
refreshToken: organizerTokens.refreshToken,
|
||||
},
|
||||
unrelatedUser: {
|
||||
id: unrelatedUser.id,
|
||||
email: unrelatedUser.email,
|
||||
accessToken: unrelatedTokens.accessToken,
|
||||
refreshToken: unrelatedTokens.refreshToken,
|
||||
},
|
||||
eventTypeId: eventType.id,
|
||||
};
|
||||
}
|
||||
|
||||
async function createOAuthClient(organizationId: number) {
|
||||
const data = {
|
||||
logo: "logo-url",
|
||||
name: "name",
|
||||
redirectUris: ["http://localhost:4444"],
|
||||
permissions: 32,
|
||||
areEmailsEnabled: true,
|
||||
};
|
||||
const secret = "secret";
|
||||
|
||||
return oauthClientRepositoryFixture.create(organizationId, data, secret);
|
||||
}
|
||||
|
||||
async function createBooking(location?: string) {
|
||||
const bookingLocation = location ?? `https://initial-${randomString()}.example.com`;
|
||||
const bookingUid = `booking-uid-${randomString(10)}`;
|
||||
|
||||
await bookingsRepositoryFixture.create({
|
||||
uid: bookingUid,
|
||||
title: "booking title",
|
||||
startTime: new Date(Date.UTC(2035, 0, 9, 13, 0, 0)).toISOString(),
|
||||
endTime: new Date(Date.UTC(2035, 0, 9, 13, 45, 0)).toISOString(),
|
||||
eventType: {
|
||||
connect: {
|
||||
id: testSetup.eventTypeId,
|
||||
},
|
||||
},
|
||||
status: "ACCEPTED",
|
||||
metadata: {},
|
||||
responses: "null",
|
||||
location: bookingLocation,
|
||||
user: {
|
||||
connect: {
|
||||
id: testSetup.organizer.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
bookingUid,
|
||||
location: bookingLocation,
|
||||
};
|
||||
}
|
||||
|
||||
function responseDataIsBooking(data: unknown): data is BookingOutput_2024_08_13 {
|
||||
return !Array.isArray(data) && typeof data === "object" && data !== null && "id" in data;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
import { GetBookingOutput_2024_08_13 } from "@calcom/platform-types";
|
||||
|
||||
export class UpdateBookingLocationOutput_2024_08_13 extends GetBookingOutput_2024_08_13 {}
|
||||
@@ -206,4 +206,14 @@ export class BookingsRepository_2024_08_13 {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async updateBooking(bookingUid: string, body: Prisma.BookingUpdateInput) {
|
||||
return this.dbWrite.prisma.booking.update({
|
||||
where: {
|
||||
uid: bookingUid,
|
||||
},
|
||||
data: body,
|
||||
select: { uid: true },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository";
|
||||
import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service";
|
||||
import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service";
|
||||
import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
|
||||
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
||||
import { EventTypeAccessService } from "@/modules/event-types/services/event-type-access.service";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { Injectable, Logger, NotFoundException, ForbiddenException } from "@nestjs/common";
|
||||
|
||||
import type {
|
||||
UpdateBookingLocationInput_2024_08_13,
|
||||
BookingInputLocation_2024_08_13,
|
||||
UpdateBookingInputLocation_2024_08_13,
|
||||
} from "@calcom/platform-types";
|
||||
import { Booking } from "@calcom/prisma/client";
|
||||
|
||||
@Injectable()
|
||||
export class BookingLocationService_2024_08_13 {
|
||||
private readonly logger = new Logger("BookingLocationService_2024_08_13");
|
||||
|
||||
constructor(
|
||||
private readonly bookingsRepository: BookingsRepository_2024_08_13,
|
||||
private readonly bookingsService: BookingsService_2024_08_13,
|
||||
private readonly usersRepository: UsersRepository,
|
||||
private readonly inputService: InputBookingsService_2024_08_13,
|
||||
private readonly eventTypesRepository: EventTypesRepository_2024_06_14,
|
||||
private readonly eventTypeAccessService: EventTypeAccessService
|
||||
) {}
|
||||
|
||||
async updateBookingLocation(
|
||||
bookingUid: string,
|
||||
input: UpdateBookingLocationInput_2024_08_13,
|
||||
user: ApiAuthGuardUser
|
||||
) {
|
||||
const existingBooking = await this.bookingsRepository.getByUidWithEventType(bookingUid);
|
||||
if (!existingBooking) {
|
||||
throw new NotFoundException(`Booking with uid=${bookingUid} not found`);
|
||||
}
|
||||
|
||||
if (existingBooking.eventTypeId && existingBooking.eventType) {
|
||||
const eventType = await this.eventTypesRepository.getEventTypeByIdWithOwnerAndTeam(
|
||||
existingBooking.eventTypeId
|
||||
);
|
||||
if (eventType) {
|
||||
const isAllowed = await this.eventTypeAccessService.userIsEventTypeAdminOrOwner(user, eventType);
|
||||
if (!isAllowed) {
|
||||
throw new ForbiddenException(
|
||||
"User is not authorized to update this booking location. User must be the event type owner, host, team admin or owner, or org admin or owner."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { location } = input;
|
||||
|
||||
if (location) {
|
||||
return await this.updateLocation(existingBooking, location, user);
|
||||
}
|
||||
|
||||
return this.bookingsService.getBooking(existingBooking.uid, user);
|
||||
}
|
||||
|
||||
private async updateLocation(
|
||||
existingBooking: Booking,
|
||||
inputLocation: UpdateBookingInputLocation_2024_08_13,
|
||||
user: ApiAuthGuardUser
|
||||
) {
|
||||
const bookingUid = existingBooking.uid;
|
||||
const bookingLocation = this.getLocationValue(inputLocation) ?? existingBooking.location;
|
||||
|
||||
if (!existingBooking.userId) {
|
||||
throw new NotFoundException(`No user found for booking with uid=${bookingUid}`);
|
||||
}
|
||||
|
||||
if (!existingBooking.eventTypeId) {
|
||||
throw new NotFoundException(`No event type found for booking with uid=${bookingUid}`);
|
||||
}
|
||||
|
||||
const existingBookingHost = await this.usersRepository.findById(existingBooking.userId);
|
||||
|
||||
if (!existingBookingHost) {
|
||||
throw new NotFoundException(`No user found for booking with uid=${bookingUid}`);
|
||||
}
|
||||
|
||||
const bookingFieldsLocation = this.inputService.transformLocation(
|
||||
inputLocation as BookingInputLocation_2024_08_13
|
||||
);
|
||||
|
||||
const responses = (existingBooking.responses || {}) as Record<string, unknown>;
|
||||
const { location: _existingLocation, ...rest } = responses;
|
||||
|
||||
const updatedBookingResponses = { ...rest, location: bookingFieldsLocation };
|
||||
|
||||
const updatedBooking = await this.bookingsRepository.updateBooking(bookingUid, {
|
||||
location: bookingLocation,
|
||||
responses: updatedBookingResponses,
|
||||
});
|
||||
|
||||
return this.bookingsService.getBooking(updatedBooking.uid, user);
|
||||
}
|
||||
|
||||
private getLocationValue(loc: UpdateBookingInputLocation_2024_08_13): string | undefined {
|
||||
if (loc.type === "address") return loc.address;
|
||||
if (loc.type === "link") return loc.link;
|
||||
if (loc.type === "phone") return loc.phone;
|
||||
if (loc.type === "attendeeAddress") return loc.address;
|
||||
if (loc.type === "attendeePhone") return loc.phone;
|
||||
if (loc.type === "attendeeDefined") return loc.location;
|
||||
|
||||
this.logger.log(
|
||||
`Booking location service getLocationValue - loc ${JSON.stringify(
|
||||
loc
|
||||
)} was passed but the type is not supported.`
|
||||
);
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable no-undef */
|
||||
// vite.config.ts
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { resolve } from "path";
|
||||
|
||||
@@ -10,3 +10,5 @@ export * from "./cancel-booking-input.pipe";
|
||||
export * from "./decline-booking.input";
|
||||
export * from "./validators/validate-metadata";
|
||||
export * from "./add-guests.input";
|
||||
export * from "./update-location.input";
|
||||
export * from "./location.input";
|
||||
@@ -0,0 +1,128 @@
|
||||
import { ApiPropertyOptional, getSchemaPath } from "@nestjs/swagger";
|
||||
import { ApiProperty as DocsProperty } from "@nestjs/swagger";
|
||||
import { plainToInstance } from "class-transformer";
|
||||
import { IsOptional } from "class-validator";
|
||||
import { IsString, MinLength, IsUrl, IsPhoneNumber, IsDefined } from "class-validator";
|
||||
import type { ValidationOptions, ValidatorConstraintInterface } from "class-validator";
|
||||
import { registerDecorator, validate, ValidatorConstraint } from "class-validator";
|
||||
|
||||
import {
|
||||
BookingInputAddressLocation_2024_08_13,
|
||||
BookingInputAttendeeAddressLocation_2024_08_13,
|
||||
BookingInputAttendeeDefinedLocation_2024_08_13,
|
||||
BookingInputAttendeePhoneLocation_2024_08_13,
|
||||
BookingInputLinkLocation_2024_08_13,
|
||||
BookingInputPhoneLocation_2024_08_13,
|
||||
} from "./location.input";
|
||||
|
||||
export class UpdateInputAddressLocation_2024_08_13 extends BookingInputAddressLocation_2024_08_13 {
|
||||
@IsDefined()
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
@DocsProperty({ example: "123 Example St, City, Country" })
|
||||
address!: string;
|
||||
}
|
||||
|
||||
export class UpdateBookingInputAttendeeAddressLocation_2024_08_13 extends BookingInputAttendeeAddressLocation_2024_08_13 {}
|
||||
|
||||
export class UpdateBookingInputAttendeeDefinedLocation_2024_08_13 extends BookingInputAttendeeDefinedLocation_2024_08_13 {}
|
||||
|
||||
export class UpdateBookingInputAttendeePhoneLocation_2024_08_13 extends BookingInputAttendeePhoneLocation_2024_08_13 {}
|
||||
|
||||
export class UpdateBookingInputLinkLocation_2024_08_13 extends BookingInputLinkLocation_2024_08_13 {
|
||||
@IsDefined()
|
||||
@IsUrl()
|
||||
@DocsProperty({ example: "https://meet.google.com/txi-fein-xyz" })
|
||||
link!: string;
|
||||
}
|
||||
|
||||
export class UpdateBookingInputPhoneLocation_2024_08_13 extends BookingInputPhoneLocation_2024_08_13 {
|
||||
@IsDefined()
|
||||
@IsPhoneNumber()
|
||||
@DocsProperty({ example: "+37120993151" })
|
||||
phone!: string;
|
||||
}
|
||||
|
||||
export type UpdateBookingInputLocation_2024_08_13 =
|
||||
| UpdateInputAddressLocation_2024_08_13
|
||||
| UpdateBookingInputAttendeeAddressLocation_2024_08_13
|
||||
| UpdateBookingInputAttendeeDefinedLocation_2024_08_13
|
||||
| UpdateBookingInputAttendeePhoneLocation_2024_08_13
|
||||
| UpdateBookingInputLinkLocation_2024_08_13
|
||||
| UpdateBookingInputPhoneLocation_2024_08_13;
|
||||
|
||||
@ValidatorConstraint({ async: true })
|
||||
class UpdateBookingInputLocationValidator_2024_08_13 implements ValidatorConstraintInterface {
|
||||
private validationMessage = "";
|
||||
|
||||
private classTypeMap: { [key: string]: new () => UpdateBookingInputLocation_2024_08_13 } = {
|
||||
address: UpdateInputAddressLocation_2024_08_13,
|
||||
attendeeAddress: UpdateBookingInputAttendeeAddressLocation_2024_08_13,
|
||||
attendeeDefined: UpdateBookingInputAttendeeDefinedLocation_2024_08_13,
|
||||
attendeePhone: UpdateBookingInputAttendeePhoneLocation_2024_08_13,
|
||||
link: UpdateBookingInputLinkLocation_2024_08_13,
|
||||
phone: UpdateBookingInputPhoneLocation_2024_08_13,
|
||||
};
|
||||
|
||||
async validate(location: { type: string }) {
|
||||
const { type } = location;
|
||||
if (!type) {
|
||||
this.validationMessage = `UpdateBookingInputLocationValidator_2024_08_13 - Booking 'location' must have a 'type' property.`;
|
||||
return false;
|
||||
}
|
||||
|
||||
const ClassType = this.classTypeMap[type];
|
||||
if (!ClassType) {
|
||||
this.validationMessage = `UpdateBookingInputLocationValidator_2024_08_13 - Unsupported booking location type '${type}'. Valid types are address, link, phone, attendeePhone, attendeeAddress, and attendeeDefined.`;
|
||||
return false;
|
||||
}
|
||||
|
||||
const instance = plainToInstance(ClassType, location);
|
||||
const errors = await validate(instance, { skipMissingProperties: false });
|
||||
if (errors.length > 0) {
|
||||
const message = errors.flatMap((error) => Object.values(error.constraints || {})).join(", ");
|
||||
this.validationMessage = `UpdateBookingInputLocationValidator_2024_08_13 - Validation failed for ${type} location: ${message}`;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
defaultMessage() {
|
||||
return (
|
||||
this.validationMessage ||
|
||||
`UpdateBookingInputLocationValidator_2024_08_13 - Validation failed for one or more location entries.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function ValidateUpdateBookingLocation_2024_08_13(validationOptions?: ValidationOptions) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return function (object: any, propertyName: string) {
|
||||
registerDecorator({
|
||||
name: "ValidateUpdateLocation",
|
||||
target: object.constructor,
|
||||
propertyName: propertyName,
|
||||
options: validationOptions,
|
||||
validator: new UpdateBookingInputLocationValidator_2024_08_13(),
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export class UpdateBookingLocationInput_2024_08_13 {
|
||||
@IsOptional()
|
||||
@ValidateUpdateBookingLocation_2024_08_13()
|
||||
@ApiPropertyOptional({
|
||||
description:
|
||||
"One of the event type locations. If instead of passing one of the location objects as required by schema you are still passing a string please use an object.",
|
||||
oneOf: [
|
||||
{ $ref: getSchemaPath(UpdateInputAddressLocation_2024_08_13) },
|
||||
{ $ref: getSchemaPath(UpdateBookingInputAttendeeAddressLocation_2024_08_13) },
|
||||
{ $ref: getSchemaPath(UpdateBookingInputAttendeeDefinedLocation_2024_08_13) },
|
||||
{ $ref: getSchemaPath(UpdateBookingInputAttendeePhoneLocation_2024_08_13) },
|
||||
{ $ref: getSchemaPath(UpdateBookingInputLinkLocation_2024_08_13) },
|
||||
{ $ref: getSchemaPath(UpdateBookingInputPhoneLocation_2024_08_13) },
|
||||
],
|
||||
})
|
||||
location?: UpdateBookingInputLocation_2024_08_13;
|
||||
}
|
||||
+13
-13
@@ -5,7 +5,7 @@ import { IsString, IsUrl, IsIn, IsPhoneNumber, IsBoolean, MinLength } from "clas
|
||||
import type { ValidationOptions, ValidatorConstraintInterface } from "class-validator";
|
||||
import { registerDecorator, validate, ValidatorConstraint } from "class-validator";
|
||||
|
||||
export const inputLocations = [
|
||||
export const eventTypeInputLocations = [
|
||||
"address",
|
||||
"link",
|
||||
"integration",
|
||||
@@ -17,7 +17,7 @@ export const inputLocations = [
|
||||
] as const;
|
||||
|
||||
export class InputAddressLocation_2024_06_14 {
|
||||
@IsIn(inputLocations)
|
||||
@IsIn(eventTypeInputLocations)
|
||||
@DocsProperty({ example: "address", description: "only allowed value for type is `address`" })
|
||||
type!: "address";
|
||||
|
||||
@@ -32,7 +32,7 @@ export class InputAddressLocation_2024_06_14 {
|
||||
}
|
||||
|
||||
export class InputOrganizersDefaultApp_2024_06_14 {
|
||||
@IsIn(inputLocations)
|
||||
@IsIn(eventTypeInputLocations)
|
||||
@DocsProperty({
|
||||
example: "organizersDefaultApp",
|
||||
description: "only allowed value for type is `organizersDefaultApp`",
|
||||
@@ -41,7 +41,7 @@ export class InputOrganizersDefaultApp_2024_06_14 {
|
||||
}
|
||||
|
||||
export class InputLinkLocation_2024_06_14 {
|
||||
@IsIn(inputLocations)
|
||||
@IsIn(eventTypeInputLocations)
|
||||
@DocsProperty({ example: "link", description: "only allowed value for type is `link`" })
|
||||
type!: "link";
|
||||
|
||||
@@ -54,7 +54,7 @@ export class InputLinkLocation_2024_06_14 {
|
||||
public!: boolean;
|
||||
}
|
||||
|
||||
export const supportedIntegrations = [
|
||||
export const eventTypeSupportedIntegrations = [
|
||||
"cal-video",
|
||||
"google-meet",
|
||||
"zoom",
|
||||
@@ -85,20 +85,20 @@ export const supportedIntegrations = [
|
||||
"demodesk-video",
|
||||
"campfire-video",
|
||||
] as const;
|
||||
export type Integration_2024_06_14 = (typeof supportedIntegrations)[number];
|
||||
export type Integration_2024_06_14 = (typeof eventTypeSupportedIntegrations)[number];
|
||||
|
||||
export class InputIntegrationLocation_2024_06_14 {
|
||||
@IsIn(inputLocations)
|
||||
@IsIn(eventTypeInputLocations)
|
||||
@DocsProperty({ example: "integration", description: "only allowed value for type is `integration`" })
|
||||
type!: "integration";
|
||||
|
||||
@IsIn(supportedIntegrations)
|
||||
@DocsProperty({ example: supportedIntegrations[0], enum: supportedIntegrations })
|
||||
@IsIn(eventTypeSupportedIntegrations)
|
||||
@DocsProperty({ example: eventTypeSupportedIntegrations[0], enum: eventTypeSupportedIntegrations })
|
||||
integration!: Integration_2024_06_14;
|
||||
}
|
||||
|
||||
export class InputPhoneLocation_2024_06_14 {
|
||||
@IsIn(inputLocations)
|
||||
@IsIn(eventTypeInputLocations)
|
||||
@DocsProperty({ example: "phone", description: "only allowed value for type is `phone`" })
|
||||
type!: "phone";
|
||||
|
||||
@@ -112,7 +112,7 @@ export class InputPhoneLocation_2024_06_14 {
|
||||
}
|
||||
|
||||
export class InputAttendeeAddressLocation_2024_06_14 {
|
||||
@IsIn(inputLocations)
|
||||
@IsIn(eventTypeInputLocations)
|
||||
@DocsProperty({
|
||||
example: "attendeeAddress",
|
||||
description: "only allowed value for type is `attendeeAddress`",
|
||||
@@ -120,13 +120,13 @@ export class InputAttendeeAddressLocation_2024_06_14 {
|
||||
type!: "attendeeAddress";
|
||||
}
|
||||
export class InputAttendeePhoneLocation_2024_06_14 {
|
||||
@IsIn(inputLocations)
|
||||
@IsIn(eventTypeInputLocations)
|
||||
@DocsProperty({ example: "attendeePhone", description: "only allowed value for type is `attendeePhone`" })
|
||||
type!: "attendeePhone";
|
||||
}
|
||||
|
||||
export class InputAttendeeDefinedLocation_2024_06_14 {
|
||||
@IsIn(inputLocations)
|
||||
@IsIn(eventTypeInputLocations)
|
||||
@DocsProperty({
|
||||
example: "attendeeDefined",
|
||||
description: "only allowed value for type is `attendeeDefined`",
|
||||
|
||||
+2
-2
@@ -11,11 +11,11 @@ import {
|
||||
InputAttendeeDefinedLocation_2024_06_14,
|
||||
InputAttendeePhoneLocation_2024_06_14,
|
||||
InputLinkLocation_2024_06_14,
|
||||
inputLocations,
|
||||
eventTypeInputLocations,
|
||||
InputPhoneLocation_2024_06_14,
|
||||
} from "../inputs";
|
||||
|
||||
const outputLocations = [...inputLocations, "conferencing", "unknown"] as const;
|
||||
const outputLocations = [...eventTypeInputLocations, "conferencing", "unknown"] as const;
|
||||
|
||||
export class OutputAddressLocation_2024_06_14 extends InputAddressLocation_2024_06_14 {}
|
||||
export class OutputLinkLocation_2024_06_14 extends InputLinkLocation_2024_06_14 {}
|
||||
|
||||
Reference in new issue
Block a user