From e879c923eaf7ca6c1b297b919d323b5e178657a9 Mon Sep 17 00:00:00 2001 From: Dhairyashil Shinde <93669429+dhairyashiil@users.noreply.github.com> Date: Mon, 12 Jan 2026 23:27:17 +0530 Subject: [PATCH] feat(api): add bookingUrl field to event types API v2 response (#26195) * feat(api): add bookingUrl field to event types API v2 response Add a new bookingUrl field to EventTypeOutput_2024_06_14 that contains the full, correct booking URL for each event type. This fixes the issue where companion app and Chrome extension were hardcoding cal.com URLs instead of using organization subdomains. Changes: - Add bookingUrl field to EventTypeOutput_2024_06_14 type definition - Update repository to include organization data when fetching users - Add buildBookingUrl method to compute URL using getOrgFullOrigin - Include bookingUrl in getResponseEventType return object The booking URL is computed using the first user's organization slug (if any) to generate the correct subdomain URL (e.g., i.cal.com/keith/30min for organization users instead of cal.com/keith/30min). * fix: include organization data in getEventTypeByIdWithHosts Update getEventTypeByIdWithHosts to use usersInclude to fetch organization slug data, ensuring bookingUrl is computed correctly for the GET /v2/event-types/{eventTypeId} endpoint. * test: add unit tests and E2E assertions for bookingUrl field * fix(api-v2): correct bookingUrl format for org users and fix double slashes Fix issues with bookingUrl field in event types API v2 response: - Remove trailing slashes from base URLs to prevent double slashes - Use profile.username instead of user.username for organization users (profile contains clean username without org suffix) - Include user profiles in repository queries to access profile data - Create local org-domains utility to replace @calcom/features dependency which isn't available in API v2 runtime Changes: - Add apps/api/v2/src/lib/org-domains.ts with getOrgFullOrigin function adapted from @calcom/features/ee/organizations/lib/orgDomains - Update event-types.repository.ts to include profiles in usersInclude - Update buildBookingUrl() to prioritize profile data when available - Update unit tests to cover organization user profile scenarios Fixes: - Double slashes in URLs (e.g., http://localhost:3000//user/slug) - Incorrect username format for org users (e.g., owner1-acme instead of owner1) - Missing organization subdomain in booking URLs (e.g., should be http://acme.localhost:3000/owner1/30min not http://localhost:3000/owner1-acme/30min) * deslop ai code * handle empty username in both code and write test for it, use select instead of include * feat(api-v2): reuse core org domain logic for event type bookingUrl Export getOrgFullOrigin and subdomainSuffix from @calcom/platform-libraries/organizations to reuse existing core logic instead of duplicating it in API v2. This ensures consistency across the codebase and reduces maintenance burden. - Export getOrgFullOrigin and subdomainSuffix from platform-libraries/organizations - Update output-event-types.service to import from @calcom/platform-libraries/organizations - Remove duplicate org-domains.ts file from API v2 - Update test mocks to use platform-libraries import path - buildBookingUrl method now uses core getOrgFullOrigin function This addresses feedback to reuse core code rather than introducing duplicate logic. * test(api-v2): exclude bookingUrl from output comparison in e2e test * refactor(api-v2): reuse core logic for event type bookingUrl Replace duplicated username/org extraction logic in buildBookingUrl with core functions. Use getBookerBaseUrlSync from @calcom/platform-libraries/organizations for base URL generation, and add enrichUserWithProfile method that follows the same pattern as core's UserRepository.enrichUsersWithTheirProfiles. The enrichment logic is applied synchronously since profiles are already fetched by the repository, avoiding the need to make the service async. This ensures consistency with core patterns while maintaining the existing synchronous API contract. - Replace getOrgFullOrigin with getBookerBaseUrlSync - Add enrichUserWithProfile method following core enrichment pattern - Simplify buildBookingUrl to use enriched user data - Update tests to mock getBookerBaseUrlSync and verify enrichment flow * trailing slash cubic comment * address review comments * address review comments 2 * better code * Updated the test to give the user an organization * address review comments again * Revert "address review comments again" This reverts commit 622ea3fc8dcd7f5c3113614afcfc86beadecca0e. * allow both the full UserWithProfile and the partial EventTypeUser (from select queries) to use the same getUserMainProfile() method, eliminating code duplication without changing the performance-optimized repository queries. * reveiw points --- .../controllers/bookings.controller.ts | 2 +- .../event-types.controller.e2e-spec.ts | 14 +- .../event-types.repository.ts | 74 +++++- .../output-event-types.service.spec.ts | 229 ++++++++++++++++++ .../services/output-event-types.service.ts | 105 +++++--- .../modules/users/services/users.service.ts | 23 +- docs/api-reference/v2/openapi.json | 9 +- packages/platform/libraries/organizations.ts | 3 + .../outputs/event-type.output.ts | 8 + 9 files changed, 422 insertions(+), 45 deletions(-) create mode 100644 apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/output-event-types.service.spec.ts diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts index 586adbb500..1b332ae113 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts @@ -280,7 +280,7 @@ export class BookingsController_2024_08_13 { const { bookings, pagination } = await this.bookingsService.getBookings(queryParams, { email: user.email, id: user.id, - orgId: profile?.organizationId, + orgId: profile?.organizationId ?? undefined, }); return { diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/controllers/event-types.controller.e2e-spec.ts b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/controllers/event-types.controller.e2e-spec.ts index 44a1bde054..44e9ddef62 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/controllers/event-types.controller.e2e-spec.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/controllers/event-types.controller.e2e-spec.ts @@ -635,6 +635,7 @@ describe("Event types Endpoints", () => { expect(createdEventType.bookingFields).toEqual(expectedBookingFields); expect(createdEventType.bookingRequiresAuthentication).toEqual(true); + expect(createdEventType.bookingUrl).toContain(`/${user.username}/${body.slug}`); expect(createdEventType.disableCancelling).toEqual({ disabled: true }); expect(createdEventType.disableRescheduling).toEqual({ disabled: true }); expect(createdEventType.calVideoSettings?.sendTranscriptionEmails).toEqual(true); @@ -731,6 +732,7 @@ describe("Event types Endpoints", () => { ); expect(fetchedEventType?.color).toEqual(eventType.color); expect(fetchedEventType?.hidden).toEqual(false); + expect(fetchedEventType?.bookingUrl).toContain(`/${user.username}/${eventType.slug}`); expect(fetchedEventType?.disableCancelling).toEqual(eventType.disableCancelling); expect(fetchedEventType?.disableRescheduling).toEqual(eventType.disableRescheduling); expect(fetchedEventType?.calVideoSettings?.sendTranscriptionEmails).toEqual( @@ -1551,6 +1553,7 @@ describe("Event types Endpoints", () => { eventType.lockTimeZoneToggleOnBookingPage ); expect(fetchedEventType.color).toEqual(eventType.color); + expect(fetchedEventType.bookingUrl).toContain(`/${user.username}/${eventType.slug}`); }); it("system admin can access another user's event type by id", async () => { @@ -2677,12 +2680,19 @@ describe("Event types Endpoints", () => { expect(createdEventType.title).toEqual(body.title); secondCreatedEventType = responseBody.data; - const { id, title, slug, ...restFirst } = firstCreatedEventType; - const { id: id2, title: title2, slug: slug2, ...restSecond } = secondCreatedEventType; + const { id, title, slug, bookingUrl, ...restFirst } = firstCreatedEventType; + const { + id: id2, + title: title2, + slug: slug2, + bookingUrl: bookingUrl2, + ...restSecond + } = secondCreatedEventType; expect(restFirst).toEqual(restSecond); expect(id2).not.toEqual(id); expect(title2).not.toEqual(title); expect(slug2).not.toEqual(slug); + expect(bookingUrl2).not.toEqual(bookingUrl); }); }); diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/event-types.repository.ts b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/event-types.repository.ts index 63af9164fd..f81c81d7dc 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/event-types.repository.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/event-types.repository.ts @@ -1,14 +1,16 @@ +import type { SortOrderType } from "@calcom/platform-types"; +import type { Prisma } from "@calcom/prisma/client"; +import { Injectable } from "@nestjs/common"; import { InputEventTransformed_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/transformed"; import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; -import { Injectable } from "@nestjs/common"; - -import type { SortOrderType } from "@calcom/platform-types"; -import type { Prisma } from "@calcom/prisma/client"; @Injectable() export class EventTypesRepository_2024_06_14 { - constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {} + constructor( + private readonly dbRead: PrismaReadService, + private readonly dbWrite: PrismaWriteService + ) {} async createUserEventType( userId: number, @@ -58,13 +60,58 @@ export class EventTypesRepository_2024_06_14 { }); } + private readonly usersInclude = { + select: { + id: true, + name: true, + username: true, + avatarUrl: true, + brandColor: true, + darkBrandColor: true, + weekStart: true, + metadata: true, + organizationId: true, + organization: { + select: { slug: true }, + }, + movedToProfile: { + select: { + id: true, + username: true, + organizationId: true, + organization: { + select: { + id: true, + slug: true, + isPlatform: true, + }, + }, + }, + }, + profiles: { + select: { + id: true, + username: true, + organizationId: true, + organization: { + select: { + id: true, + slug: true, + isPlatform: true, + }, + }, + }, + }, + }, + }; + async getUserEventType(userId: number, eventTypeId: number) { return this.dbRead.prisma.eventType.findFirst({ where: { id: eventTypeId, userId, }, - include: { users: true, schedule: true, destinationCalendar: true }, + include: { users: this.usersInclude, schedule: true, destinationCalendar: true }, }); } @@ -74,7 +121,7 @@ export class EventTypesRepository_2024_06_14 { userId, }, ...(sortCreatedAt && { orderBy: { id: sortCreatedAt } }), - include: { users: true, schedule: true, destinationCalendar: true }, + include: { users: this.usersInclude, schedule: true, destinationCalendar: true }, }); } @@ -85,14 +132,19 @@ export class EventTypesRepository_2024_06_14 { hidden: false, }, ...(sortCreatedAt && { orderBy: { id: sortCreatedAt } }), - include: { users: true, schedule: true, destinationCalendar: true }, + include: { users: this.usersInclude, schedule: true, destinationCalendar: true }, }); } async getEventTypeById(eventTypeId: number) { return this.dbRead.prisma.eventType.findUnique({ where: { id: eventTypeId }, - include: { users: true, schedule: true, destinationCalendar: true, calVideoSettings: true }, + include: { + users: this.usersInclude, + schedule: true, + destinationCalendar: true, + calVideoSettings: true, + }, }); } @@ -100,7 +152,7 @@ export class EventTypesRepository_2024_06_14 { return this.dbRead.prisma.eventType.findUnique({ where: { id: eventTypeId }, include: { - users: true, + users: this.usersInclude, schedule: true, destinationCalendar: true, calVideoSettings: true, @@ -140,7 +192,7 @@ export class EventTypesRepository_2024_06_14 { slug: slug, }, }, - include: { users: true, schedule: true, destinationCalendar: true }, + include: { users: this.usersInclude, schedule: true, destinationCalendar: true }, }); } diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/output-event-types.service.spec.ts b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/output-event-types.service.spec.ts new file mode 100644 index 0000000000..503642631b --- /dev/null +++ b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/output-event-types.service.spec.ts @@ -0,0 +1,229 @@ +import { ConfigService } from "@nestjs/config"; + +import { OutputEventTypesService_2024_06_14 } from "./output-event-types.service"; +import { UsersService } from "@/modules/users/services/users.service"; +import { UsersRepository } from "@/modules/users/users.repository"; + +jest.mock("@calcom/platform-libraries/organizations", () => ({ + getBookerBaseUrlSync: jest.fn((slug: string | null) => { + if (!slug) return "https://cal.com"; + return `https://${slug}.cal.com`; + }), +})); + +describe("OutputEventTypesService_2024_06_14", () => { + let service: OutputEventTypesService_2024_06_14; + let configService: jest.Mocked; + let usersService: UsersService; + + beforeEach(() => { + configService = { + get: jest.fn((key: string, defaultValue?: string) => { + if (key === "app.baseUrl") return "https://cal.com"; + return defaultValue; + }), + } as any; + + const usersRepository = {} as UsersRepository; + usersService = new UsersService(usersRepository); + + service = new OutputEventTypesService_2024_06_14(configService, usersService); + }); + + describe("buildBookingUrl", () => { + it("should return correct URL for user without organization", () => { + const user = { + id: 1, + name: "John Doe", + username: "john-doe", + avatarUrl: null, + brandColor: null, + darkBrandColor: null, + weekStart: "Monday", + metadata: {}, + organizationId: null, + organization: null, + movedToProfile: null, + profiles: [], + }; + const slug = "30min"; + + const result = service.buildBookingUrl(user, slug); + + expect(result).toBe("https://cal.com/john-doe/30min"); + }); + + it("should use profile username for org users", () => { + const user = { + id: 1, + name: "Owner", + username: "owner1-acme", + avatarUrl: null, + brandColor: null, + darkBrandColor: null, + weekStart: "Monday", + metadata: {}, + organizationId: 1, + organization: { slug: "acme" }, + movedToProfile: null, + profiles: [ + { + id: 100, + username: "owner1", + organizationId: 1, + organization: { id: 1, slug: "acme", isPlatform: false }, + }, + ], + }; + const slug = "30min"; + + const result = service.buildBookingUrl(user, slug); + + expect(result).toBe("https://acme.cal.com/owner1/30min"); + }); + + it("should fall back to user username when profile has no username", () => { + const user = { + id: 1, + name: "Keith", + username: "keith", + avatarUrl: null, + brandColor: null, + darkBrandColor: null, + weekStart: "Monday", + metadata: {}, + organizationId: 2, + organization: { slug: "i" }, + movedToProfile: null, + profiles: [ + { + id: 200, + username: null, + organizationId: 2, + organization: { id: 2, slug: "i", isPlatform: false }, + }, + ], + }; + const slug = "30min"; + + const result = service.buildBookingUrl(user, slug); + + expect(result).toBe("https://i.cal.com/keith/30min"); + }); + + it("should return empty string when user is undefined", () => { + const user = undefined; + const slug = "30min"; + + const result = service.buildBookingUrl(user, slug); + + expect(result).toBe(""); + }); + + it("should handle organization with null slug", () => { + const user = { + id: 1, + name: "User", + username: "user", + avatarUrl: null, + brandColor: null, + darkBrandColor: null, + weekStart: "Monday", + metadata: {}, + organizationId: 3, + organization: { slug: null }, + movedToProfile: null, + profiles: [ + { + id: 300, + username: "user", + organizationId: 3, + organization: { id: 3, slug: null, isPlatform: false }, + }, + ], + }; + const slug = "consultation"; + + const result = service.buildBookingUrl(user, slug); + + expect(result).toBe("https://cal.com/user/consultation"); + }); + + it("should handle base URL with trailing slash", () => { + const { getBookerBaseUrlSync } = require("@calcom/platform-libraries/organizations"); + getBookerBaseUrlSync.mockReturnValueOnce("https://acme.cal.com/"); + + const user = { + id: 1, + name: "John", + username: "john-acme", + avatarUrl: null, + brandColor: null, + darkBrandColor: null, + weekStart: "Monday", + metadata: {}, + organizationId: 1, + organization: { slug: "acme" }, + movedToProfile: null, + profiles: [ + { + id: 400, + username: "john", + organizationId: 1, + organization: { id: 1, slug: "acme", isPlatform: false }, + }, + ], + }; + const slug = "30min"; + + const result = service.buildBookingUrl(user, slug); + + expect(result).toBe("https://acme.cal.com/john/30min"); + }); + + it("should return empty string when username is empty", () => { + const user = { + id: 1, + name: "User", + username: "", + avatarUrl: null, + brandColor: null, + darkBrandColor: null, + weekStart: "Monday", + metadata: {}, + organizationId: null, + organization: null, + movedToProfile: null, + profiles: [], + }; + const slug = "30min"; + + const result = service.buildBookingUrl(user, slug); + + expect(result).toBe(""); + }); + + it("should return empty string when username is null", () => { + const user = { + id: 1, + name: "User", + username: null, + avatarUrl: null, + brandColor: null, + darkBrandColor: null, + weekStart: "Monday", + metadata: {}, + organizationId: null, + organization: null, + movedToProfile: null, + profiles: [], + }; + const slug = "30min"; + + const result = service.buildBookingUrl(user, slug); + + expect(result).toBe(""); + }); + }); +}); + diff --git a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/output-event-types.service.ts b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/output-event-types.service.ts index ea7207a8b6..1719913757 100644 --- a/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/output-event-types.service.ts +++ b/apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/output-event-types.service.ts @@ -1,41 +1,65 @@ import { - transformLocationsInternalToApi, - transformBookingFieldsInternalToApi, - InternalLocationSchema, - SystemField, - CustomField, - transformIntervalLimitsInternalToApi, - transformFutureBookingLimitsInternalToApi, - transformRecurrenceInternalToApi, - transformBookerLayoutsInternalToApi, - transformRequiresConfirmationInternalToApi, - transformEventTypeColorsInternalToApi, - transformSeatsInternalToApi, - InternalLocation, - BookingFieldSchema, -} from "@/ee/event-types/event-types_2024_06_14/transformers"; -import { Injectable } from "@nestjs/common"; - -import { - userMetadata, + getBookingFieldsWithSystemFields, parseBookingLimit, parseRecurringEvent, - getBookingFieldsWithSystemFields, + userMetadata, } from "@calcom/platform-libraries"; import { EventTypeMetaDataSchema, parseEventTypeColor } from "@calcom/platform-libraries/event-types"; +import { getBookerBaseUrlSync } from "@calcom/platform-libraries/organizations"; import type { - TransformFutureBookingsLimitSchema_2024_06_14, BookerLayoutsTransformedSchema, - NoticeThresholdTransformedSchema, EventTypeOutput_2024_06_14, - OutputUnknownLocation_2024_06_14, - OutputUnknownBookingField_2024_06_14, + NoticeThresholdTransformedSchema, OutputBookingField_2024_06_14, + OutputUnknownBookingField_2024_06_14, + OutputUnknownLocation_2024_06_14, + TransformFutureBookingsLimitSchema_2024_06_14, } from "@calcom/platform-types"; -import type { EventType, User, Schedule, DestinationCalendar, CalVideoSettings } from "@calcom/prisma/client"; +import type { + CalVideoSettings, + DestinationCalendar, + EventType, + Prisma, + Schedule, + Team, +} from "@calcom/prisma/client"; +import { Injectable } from "@nestjs/common"; +import { ConfigService } from "@nestjs/config"; +import { + BookingFieldSchema, + CustomField, + InternalLocation, + InternalLocationSchema, + SystemField, + transformBookerLayoutsInternalToApi, + transformBookingFieldsInternalToApi, + transformEventTypeColorsInternalToApi, + transformFutureBookingLimitsInternalToApi, + transformIntervalLimitsInternalToApi, + transformLocationsInternalToApi, + transformRecurrenceInternalToApi, + transformRequiresConfirmationInternalToApi, + transformSeatsInternalToApi, +} from "@/ee/event-types/event-types_2024_06_14/transformers"; +import { ProfileMinimal, UsersService } from "@/modules/users/services/users.service"; + +type EventTypeUser = { + id: number; + name: string | null; + username: string | null; + avatarUrl: string | null; + brandColor: string | null; + darkBrandColor: string | null; + weekStart: string; + metadata: Prisma.JsonValue; + organizationId: number | null; + organization?: { slug: string | null } | null; + movedToProfile?: ProfileMinimal | null; + profiles?: ProfileMinimal[]; +}; type EventTypeRelations = { - users: User[]; + users: EventTypeUser[]; schedule: Schedule | null; destinationCalendar?: DestinationCalendar | null; calVideoSettings?: CalVideoSettings | null; @@ -106,6 +130,11 @@ type Input = Pick< @Injectable() export class OutputEventTypesService_2024_06_14 { + constructor( + private readonly configService: ConfigService, + private readonly usersService: UsersService + ) {} + getResponseEventType( ownerId: number, databaseEventType: Input, @@ -184,6 +213,7 @@ export class OutputEventTypesService_2024_06_14 { } as TransformFutureBookingsLimitSchema_2024_06_14); const destinationCalendar = this.transformDestinationCalendar(databaseEventType.destinationCalendar); const bookerActiveBookingsLimit = this.transformBookerActiveBookingsLimit(databaseEventType); + const bookingUrl = this.buildBookingUrl(databaseEventType.users[0], slug); const disableReschedulingOutput = this.transformDisableRescheduling( disableRescheduling, minimumRescheduleNotice @@ -239,6 +269,7 @@ export class OutputEventTypesService_2024_06_14 { hidden, bookingRequiresAuthentication, bookerActiveBookingsLimit, + bookingUrl, disableCancelling: disableCancellingOutput, disableRescheduling: disableReschedulingOutput, interfaceLanguage, @@ -342,7 +373,7 @@ export class OutputEventTypesService_2024_06_14 { return EventTypeMetaDataSchema.parse(metadata); } - transformUsers(users: User[]) { + transformUsers(users: EventTypeUser[]) { return users.map((user) => { const metadata = user.metadata ? userMetadata.parse(user.metadata) : {}; return { @@ -405,6 +436,26 @@ export class OutputEventTypesService_2024_06_14 { }); } + buildBookingUrl(user: EventTypeUser | undefined, slug: string): string { + if (!user) { + return ""; + } + + const profile = this.usersService.getUserMainProfile(user); + const username = profile?.username ?? user.username; + + if (!username) { + return ""; + } + + const orgSlug = profile?.organization?.slug ?? null; + const webAppUrl = this.configService.get("app.baseUrl", "https://app.cal.com"); + const baseUrl = orgSlug ? getBookerBaseUrlSync(orgSlug) : webAppUrl; + const normalizedBaseUrl = baseUrl.replace(/\/$/, ""); + + return `${normalizedBaseUrl}/${username}/${slug}`; + } + getResponseEventTypesWithoutHiddenFields( eventTypes: EventTypeOutput_2024_06_14[] ): EventTypeOutput_2024_06_14[] { diff --git a/apps/api/v2/src/modules/users/services/users.service.ts b/apps/api/v2/src/modules/users/services/users.service.ts index e66e2ff281..3a1e3cc9a3 100644 --- a/apps/api/v2/src/modules/users/services/users.service.ts +++ b/apps/api/v2/src/modules/users/services/users.service.ts @@ -3,6 +3,23 @@ import { Injectable } from "@nestjs/common"; import type { User } from "@calcom/prisma/client"; +export type ProfileMinimal = { + id: number; + username?: string | null; + organizationId: number | null; + organization: { + id: number; + slug?: string | null; + isPlatform: boolean; + } | null; +}; + +export type UserWithProfileMinimal = { + organizationId?: number | null; + movedToProfile?: ProfileMinimal | null; + profiles?: ProfileMinimal[]; +}; + @Injectable() export class UsersService { constructor(private readonly usersRepository: UsersRepository) {} @@ -22,7 +39,7 @@ export class UsersService { return users; } - getUserMainProfile(user: UserWithProfile) { + getUserMainProfile(user: UserWithProfileMinimal): ProfileMinimal | undefined { return ( user?.movedToProfile || user.profiles?.find((p) => p.organizationId === user.organizationId) || @@ -30,8 +47,8 @@ export class UsersService { ); } - getUserMainOrgId(user: UserWithProfile) { - return this.getUserMainProfile(user)?.organizationId ?? user.organizationId; + getUserMainOrgId(user: UserWithProfileMinimal): number | null { + return this.getUserMainProfile(user)?.organizationId ?? user.organizationId ?? null; } getUserProfileByOrgId(user: UserWithProfile, organizationId: number) { diff --git a/docs/api-reference/v2/openapi.json b/docs/api-reference/v2/openapi.json index 6965a18763..0e5ab88b51 100644 --- a/docs/api-reference/v2/openapi.json +++ b/docs/api-reference/v2/openapi.json @@ -21003,6 +21003,12 @@ "items": { "type": "string" } + }, + "bookingUrl": { + "type": "string", + "description": "Full URL to the booking page for this event type", + "example": "https://cal.com/john-doe/30min", + "format": "uri" } }, "required": [ @@ -21026,7 +21032,8 @@ "hidden", "bookingRequiresAuthentication", "ownerId", - "users" + "users", + "bookingUrl" ] }, "CreateEventTypeOutput_2024_06_14": { diff --git a/packages/platform/libraries/organizations.ts b/packages/platform/libraries/organizations.ts index 62ed49528c..0d46ef8949 100644 --- a/packages/platform/libraries/organizations.ts +++ b/packages/platform/libraries/organizations.ts @@ -2,3 +2,6 @@ export { getOrganizationRepository } from "@calcom/features/ee/organizations/di/ export { OrganizationRepository } from "@calcom/features/ee/organizations/repositories/OrganizationRepository"; export { OrganizationMembershipService } from "@calcom/features/ee/organizations/lib/service/OrganizationMembershipService"; export type { IOrganizationRepository } from "@calcom/features/ee/organizations/lib/repository/IOrganizationRepository"; + +export { getOrgFullOrigin, subdomainSuffix } from "@calcom/features/ee/organizations/lib/orgDomains"; +export { getBookerBaseUrlSync } from "@calcom/features/ee/organizations/lib/getBookerBaseUrlSync"; diff --git a/packages/platform/types/event-types/event-types_2024_06_14/outputs/event-type.output.ts b/packages/platform/types/event-types/event-types_2024_06_14/outputs/event-type.output.ts index 04efab7be6..e8748beed4 100644 --- a/packages/platform/types/event-types/event-types_2024_06_14/outputs/event-type.output.ts +++ b/packages/platform/types/event-types/event-types_2024_06_14/outputs/event-type.output.ts @@ -546,6 +546,14 @@ export class EventTypeOutput_2024_06_14 extends BaseEventTypeOutput_2024_06_14 { @IsArray() @DocsProperty() users!: User_2024_06_14[]; + + @IsString() + @DocsProperty({ + description: "Full URL to the booking page for this event type", + example: "https://cal.com/john-doe/30min", + format: "uri", + }) + bookingUrl!: string; } export class TeamEventTypeOutput_2024_06_14 extends BaseEventTypeOutput_2024_06_14 {