Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b323cbcc8 | ||
|
|
18a0e2d75c | ||
|
|
87c89a9968 | ||
|
|
e237749abd | ||
|
|
994a36d039 | ||
|
|
c989817373 |
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
undecided:
|
||||
- "@calcom/prisma"
|
||||
@@ -38,7 +38,7 @@
|
||||
"@axiomhq/winston": "^1.2.0",
|
||||
"@calcom/platform-constants": "*",
|
||||
"@calcom/platform-enums": "*",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.306",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.311",
|
||||
"@calcom/platform-types": "*",
|
||||
"@calcom/platform-utils": "*",
|
||||
"@calcom/prisma": "*",
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import { createRouterCaller } from "app/_trpc/context";
|
||||
import type { PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { AppCategories } from "@calcom/prisma/enums";
|
||||
import { appsRouter } from "@calcom/trpc/server/routers/viewer/apps/_router";
|
||||
import { calendarsRouter } from "@calcom/trpc/server/routers/viewer/calendars/_router";
|
||||
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
|
||||
import InstalledApps from "~/apps/installed/[category]/installed-category-view";
|
||||
|
||||
const querySchema = z.object({
|
||||
@@ -31,6 +35,11 @@ const InstalledAppsWrapper = async ({ params }: PageProps) => {
|
||||
redirect("/apps/installed/calendar");
|
||||
}
|
||||
|
||||
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
|
||||
if (!session?.user?.id) {
|
||||
return redirect("/auth/login");
|
||||
}
|
||||
|
||||
const [calendarsCaller, appsCaller] = await Promise.all([
|
||||
createRouterCaller(calendarsRouter),
|
||||
createRouterCaller(appsRouter),
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import { getServerSideProps } from "@lib/settings/organizations/new/getServerSideProps";
|
||||
|
||||
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/about-view";
|
||||
|
||||
@@ -17,10 +11,7 @@ export const generateMetadata = async () =>
|
||||
"/settings/organizations/new/about"
|
||||
);
|
||||
|
||||
const getData = withAppDirSsr(getServerSideProps);
|
||||
|
||||
const ServerPage = async ({ params, searchParams }: PageProps) => {
|
||||
await getData(buildLegacyCtx(await headers(), await cookies(), await params, await searchParams));
|
||||
const ServerPage = async () => {
|
||||
return (
|
||||
<LayoutWrapper>
|
||||
<LegacyPage />
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import { getServerSideProps } from "@lib/settings/organizations/new/getServerSideProps";
|
||||
|
||||
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/add-teams-view";
|
||||
|
||||
@@ -17,10 +11,7 @@ export const generateMetadata = async () =>
|
||||
"/settings/organizations/new/add-teams"
|
||||
);
|
||||
|
||||
const getData = withAppDirSsr(getServerSideProps);
|
||||
|
||||
const ServerPage = async ({ params, searchParams }: PageProps) => {
|
||||
await getData(buildLegacyCtx(await headers(), await cookies(), await params, await searchParams));
|
||||
const ServerPage = async () => {
|
||||
return (
|
||||
<LayoutWrapper>
|
||||
<LegacyPage />
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { cookies } from "next/headers";
|
||||
import { headers } from "next/headers";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import { getServerSideProps } from "@lib/settings/organizations/new/getServerSideProps";
|
||||
|
||||
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/onboarding-handover";
|
||||
|
||||
@@ -18,10 +11,7 @@ export const generateMetadata = async () =>
|
||||
"/settings/organizations/new/handover"
|
||||
);
|
||||
|
||||
const getData = withAppDirSsr(getServerSideProps);
|
||||
|
||||
const ServerPage = async ({ params, searchParams }: PageProps) => {
|
||||
await getData(buildLegacyCtx(await headers(), await cookies(), await params, await searchParams));
|
||||
const ServerPage = async () => {
|
||||
return (
|
||||
<LayoutWrapper>
|
||||
<LegacyPage />
|
||||
|
||||
+7
-12
@@ -1,21 +1,16 @@
|
||||
import type { GetServerSidePropsResult } from "next";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
|
||||
export const getServerSideProps = async (): Promise<GetServerSidePropsResult<{ isOrg: boolean }>> => {
|
||||
export default async function Layout({ children }: { children: React.ReactNode }) {
|
||||
const featuresRepository = new FeaturesRepository(prisma);
|
||||
const organizations = await featuresRepository.checkIfFeatureIsEnabledGlobally("organizations");
|
||||
// Check if organizations are enabled
|
||||
|
||||
if (!organizations) {
|
||||
return {
|
||||
notFound: true,
|
||||
};
|
||||
return notFound();
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
isOrg: true,
|
||||
},
|
||||
};
|
||||
};
|
||||
return children;
|
||||
}
|
||||
+1
-10
@@ -1,10 +1,4 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import { getServerSideProps } from "@lib/settings/organizations/new/getServerSideProps";
|
||||
|
||||
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/onboard-members-view";
|
||||
|
||||
@@ -17,10 +11,7 @@ export const generateMetadata = async () =>
|
||||
"/settings/organizations/new/onboard-members"
|
||||
);
|
||||
|
||||
const getData = withAppDirSsr(getServerSideProps);
|
||||
|
||||
const ServerPage = async ({ params, searchParams }: PageProps) => {
|
||||
await getData(buildLegacyCtx(await headers(), await cookies(), await params, await searchParams));
|
||||
const ServerPage = async () => {
|
||||
return (
|
||||
<LayoutWrapper>
|
||||
<LegacyPage />
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import { getServerSideProps } from "@lib/settings/organizations/new/getServerSideProps";
|
||||
|
||||
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/create-new-view";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
@@ -19,14 +13,7 @@ export const generateMetadata = async () =>
|
||||
"/settings/organizations/new"
|
||||
);
|
||||
|
||||
type Props = {
|
||||
isOrg: boolean;
|
||||
};
|
||||
|
||||
const getData = withAppDirSsr<Props>(getServerSideProps);
|
||||
|
||||
const ServerPage = async ({ params, searchParams }: PageProps) => {
|
||||
await getData(buildLegacyCtx(await headers(), await cookies(), await params, await searchParams));
|
||||
const ServerPage = async () => {
|
||||
return (
|
||||
<LayoutWrapper>
|
||||
<LicenseRequired>
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import type { PageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
import { getServerSideProps } from "@lib/settings/organizations/new/getServerSideProps";
|
||||
|
||||
import LegacyPage, { LayoutWrapper } from "~/settings/organizations/new/payment-status-view";
|
||||
|
||||
@@ -17,10 +11,7 @@ export const generateMetadata = async () =>
|
||||
"/settings/organizations/new/status"
|
||||
);
|
||||
|
||||
const getData = withAppDirSsr(getServerSideProps);
|
||||
|
||||
const ServerPage = async ({ params, searchParams }: PageProps) => {
|
||||
await getData(buildLegacyCtx(await headers(), await cookies(), await params, await searchParams));
|
||||
const ServerPage = async () => {
|
||||
return (
|
||||
<LayoutWrapper>
|
||||
<LegacyPage />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "5.5.17",
|
||||
"version": "5.5.18",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
|
||||
@@ -4264,9 +4264,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Orgs / Teams / Event Types / Private Links"
|
||||
]
|
||||
"tags": ["Orgs / Teams / Event Types / Private Links"]
|
||||
},
|
||||
"get": {
|
||||
"operationId": "OrganizationsEventTypesPrivateLinksController_getPrivateLinks",
|
||||
@@ -4328,9 +4326,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Orgs / Teams / Event Types / Private Links"
|
||||
]
|
||||
"tags": ["Orgs / Teams / Event Types / Private Links"]
|
||||
}
|
||||
},
|
||||
"/v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId}/private-links/{linkId}": {
|
||||
@@ -4402,9 +4398,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Orgs / Teams / Event Types / Private Links"
|
||||
]
|
||||
"tags": ["Orgs / Teams / Event Types / Private Links"]
|
||||
},
|
||||
"delete": {
|
||||
"operationId": "OrganizationsEventTypesPrivateLinksController_deletePrivateLink",
|
||||
@@ -4474,9 +4468,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Orgs / Teams / Event Types / Private Links"
|
||||
]
|
||||
"tags": ["Orgs / Teams / Event Types / Private Links"]
|
||||
}
|
||||
},
|
||||
"/v2/organizations/{orgId}/teams/{teamId}/memberships": {
|
||||
@@ -22121,13 +22113,7 @@
|
||||
"example": "2025-12-31T23:59:59.000Z"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"linkId",
|
||||
"eventTypeId",
|
||||
"isExpired",
|
||||
"bookingUrl",
|
||||
"expiresAt"
|
||||
]
|
||||
"required": ["linkId", "eventTypeId", "isExpired", "bookingUrl", "expiresAt"]
|
||||
},
|
||||
"UsageBasedPrivateLinkOutput": {
|
||||
"type": "object",
|
||||
@@ -22164,14 +22150,7 @@
|
||||
"example": 3
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"linkId",
|
||||
"eventTypeId",
|
||||
"isExpired",
|
||||
"bookingUrl",
|
||||
"maxUsageCount",
|
||||
"usageCount"
|
||||
]
|
||||
"required": ["linkId", "eventTypeId", "isExpired", "bookingUrl", "maxUsageCount", "usageCount"]
|
||||
},
|
||||
"CreatePrivateLinkOutput": {
|
||||
"type": "object",
|
||||
@@ -22193,10 +22172,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status",
|
||||
"data"
|
||||
]
|
||||
"required": ["status", "data"]
|
||||
},
|
||||
"GetPrivateLinksOutput": {
|
||||
"type": "object",
|
||||
@@ -22221,10 +22197,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status",
|
||||
"data"
|
||||
]
|
||||
"required": ["status", "data"]
|
||||
},
|
||||
"UpdatePrivateLinkOutput": {
|
||||
"type": "object",
|
||||
@@ -22246,10 +22219,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status",
|
||||
"data"
|
||||
]
|
||||
"required": ["status", "data"]
|
||||
},
|
||||
"DeletePrivateLinkOutput": {
|
||||
"type": "object",
|
||||
@@ -22274,10 +22244,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status",
|
||||
"data"
|
||||
]
|
||||
"required": ["status", "data"]
|
||||
},
|
||||
"StripConnectOutputDto": {
|
||||
"type": "object",
|
||||
@@ -26267,205 +26234,6 @@
|
||||
},
|
||||
"required": ["status", "data"]
|
||||
},
|
||||
"CreatePrivateLinkInput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"expiresAt": {
|
||||
"type": "string",
|
||||
"description": "Expiration date for time-based links",
|
||||
"format": "date-time",
|
||||
"example": "2024-12-31T23:59:59.000Z"
|
||||
},
|
||||
"maxUsageCount": {
|
||||
"type": "number",
|
||||
"description": "Maximum number of times the link can be used. If omitted and expiresAt is not provided, defaults to 1 (one time use).",
|
||||
"example": 10,
|
||||
"minimum": 1,
|
||||
"default": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"TimeBasedPrivateLinkOutput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"linkId": {
|
||||
"type": "string",
|
||||
"description": "The private link ID",
|
||||
"example": "abc123def456"
|
||||
},
|
||||
"eventTypeId": {
|
||||
"type": "number",
|
||||
"description": "Event type ID this link belongs to",
|
||||
"example": 123
|
||||
},
|
||||
"isExpired": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the link is currently expired",
|
||||
"example": false
|
||||
},
|
||||
"bookingUrl": {
|
||||
"type": "string",
|
||||
"description": "Full booking URL for this private link",
|
||||
"format": "uri",
|
||||
"example": "https://cal.com/d/abc123def456/30min"
|
||||
},
|
||||
"expiresAt": {
|
||||
"type": "string",
|
||||
"description": "Expiration date for this time-based link",
|
||||
"format": "date-time",
|
||||
"example": "2025-12-31T23:59:59.000Z"
|
||||
}
|
||||
},
|
||||
"required": ["linkId", "eventTypeId", "isExpired", "bookingUrl", "expiresAt"]
|
||||
},
|
||||
"UsageBasedPrivateLinkOutput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"linkId": {
|
||||
"type": "string",
|
||||
"description": "The private link ID",
|
||||
"example": "abc123def456"
|
||||
},
|
||||
"eventTypeId": {
|
||||
"type": "number",
|
||||
"description": "Event type ID this link belongs to",
|
||||
"example": 123
|
||||
},
|
||||
"isExpired": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the link is currently expired",
|
||||
"example": false
|
||||
},
|
||||
"bookingUrl": {
|
||||
"type": "string",
|
||||
"description": "Full booking URL for this private link",
|
||||
"format": "uri",
|
||||
"example": "https://cal.com/d/abc123def456/30min"
|
||||
},
|
||||
"maxUsageCount": {
|
||||
"type": "number",
|
||||
"description": "Maximum number of times this link can be used",
|
||||
"example": 10
|
||||
},
|
||||
"usageCount": {
|
||||
"type": "number",
|
||||
"description": "Current usage count for this link",
|
||||
"example": 3
|
||||
}
|
||||
},
|
||||
"required": ["linkId", "eventTypeId", "isExpired", "bookingUrl", "maxUsageCount", "usageCount"]
|
||||
},
|
||||
"CreatePrivateLinkOutput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"description": "Response status",
|
||||
"example": "success"
|
||||
},
|
||||
"data": {
|
||||
"description": "Created private link data (either time-based or usage-based)",
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/TimeBasedPrivateLinkOutput"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/UsageBasedPrivateLinkOutput"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": ["status", "data"]
|
||||
},
|
||||
"GetPrivateLinksOutput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"description": "Response status",
|
||||
"example": "success"
|
||||
},
|
||||
"data": {
|
||||
"type": "array",
|
||||
"description": "Array of private links for the event type (mix of time-based and usage-based)",
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/TimeBasedPrivateLinkOutput"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/UsageBasedPrivateLinkOutput"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["status", "data"]
|
||||
},
|
||||
"UpdatePrivateLinkBody": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"expiresAt": {
|
||||
"format": "date-time",
|
||||
"type": "string",
|
||||
"description": "New expiration date for time-based links",
|
||||
"example": "2024-12-31T23:59:59.000Z"
|
||||
},
|
||||
"maxUsageCount": {
|
||||
"type": "number",
|
||||
"description": "New maximum number of times the link can be used",
|
||||
"example": 10,
|
||||
"minimum": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"UpdatePrivateLinkOutput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"description": "Response status",
|
||||
"example": "success"
|
||||
},
|
||||
"data": {
|
||||
"description": "Updated private link data (either time-based or usage-based)",
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/TimeBasedPrivateLinkOutput"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/UsageBasedPrivateLinkOutput"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": ["status", "data"]
|
||||
},
|
||||
"DeletePrivateLinkOutput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"description": "Response status",
|
||||
"example": "success"
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"description": "Deleted link information",
|
||||
"properties": {
|
||||
"linkId": {
|
||||
"type": "string",
|
||||
"example": "abc123def456"
|
||||
},
|
||||
"message": {
|
||||
"type": "string",
|
||||
"example": "Private link deleted successfully"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["status", "data"]
|
||||
},
|
||||
"UserWebhookOutputDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -1430,6 +1430,8 @@ async function handler(
|
||||
eventType.schedulingType === SchedulingType.ROUND_ROBIN &&
|
||||
originalRescheduledBooking.userId !== evt.organizer.id;
|
||||
|
||||
const skipDeleteEventsAndMeetings = changedOrganizer;
|
||||
|
||||
const isBookingRequestedReschedule =
|
||||
!!originalRescheduledBooking &&
|
||||
!!originalRescheduledBooking.rescheduled &&
|
||||
@@ -1702,7 +1704,8 @@ async function handler(
|
||||
undefined,
|
||||
changedOrganizer,
|
||||
previousHostDestinationCalendar,
|
||||
isBookingRequestedReschedule
|
||||
isBookingRequestedReschedule,
|
||||
skipDeleteEventsAndMeetings
|
||||
);
|
||||
// This gets overridden when updating the event - to check if notes have been hidden or not. We just reset this back
|
||||
// to the default description when we are sending the emails.
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* Tests for calendar event deletion during round robin organizer change operations.
|
||||
* Covers scenarios where events need to be deleted from original hosts and
|
||||
* credential handling during organizer changes.
|
||||
*/
|
||||
import {
|
||||
getDate,
|
||||
createBookingScenario,
|
||||
getScenarioData,
|
||||
getMockBookingAttendee,
|
||||
TestData,
|
||||
getOrganizer,
|
||||
getGoogleCalendarCredential,
|
||||
mockCalendarToHaveNoBusySlots,
|
||||
} from "@calcom/web/test/utils/bookingScenario/bookingScenario";
|
||||
import {
|
||||
expectBookingToBeInDatabase,
|
||||
expectSuccessfulRoundRobinReschedulingEmails,
|
||||
} from "@calcom/web/test/utils/bookingScenario/expects";
|
||||
import { setupAndTeardown } from "@calcom/web/test/utils/bookingScenario/setupAndTeardown";
|
||||
|
||||
import { describe, expect } from "vitest";
|
||||
|
||||
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
|
||||
import { SchedulingType, BookingStatus } from "@calcom/prisma/enums";
|
||||
import { test } from "@calcom/web/test/fixtures/fixtures";
|
||||
|
||||
describe("roundRobinReassignment test", () => {
|
||||
setupAndTeardown();
|
||||
|
||||
test("should delete calendar events from original host when round robin reassignment changes organizer", async ({
|
||||
emails,
|
||||
}) => {
|
||||
const roundRobinReassignment = (await import("./roundRobinReassignment")).default;
|
||||
|
||||
const originalHost = getOrganizer({
|
||||
name: "Original Host",
|
||||
email: "originalhost@example.com",
|
||||
id: 101,
|
||||
schedules: [TestData.schedules.IstWorkHours],
|
||||
credentials: [getGoogleCalendarCredential()],
|
||||
selectedCalendars: [TestData.selectedCalendars.google],
|
||||
});
|
||||
|
||||
const newHost = getOrganizer({
|
||||
name: "New Host",
|
||||
email: "newhost@example.com",
|
||||
id: 102,
|
||||
schedules: [TestData.schedules.IstWorkHours],
|
||||
credentials: [getGoogleCalendarCredential()],
|
||||
selectedCalendars: [TestData.selectedCalendars.google],
|
||||
});
|
||||
|
||||
const { dateString: dateStringPlusOne } = getDate({ dateIncrement: 1 });
|
||||
const bookingToReassignUid = "booking-to-reassign-with-deletion";
|
||||
|
||||
await createBookingScenario(
|
||||
getScenarioData({
|
||||
eventTypes: [
|
||||
{
|
||||
id: 1,
|
||||
slug: "round-robin-event",
|
||||
schedulingType: SchedulingType.ROUND_ROBIN,
|
||||
length: 45,
|
||||
users: [{ id: 101 }, { id: 102 }],
|
||||
hosts: [
|
||||
{ userId: 101, isFixed: false },
|
||||
{ userId: 102, isFixed: false },
|
||||
],
|
||||
},
|
||||
],
|
||||
bookings: [
|
||||
{
|
||||
id: 123,
|
||||
eventTypeId: 1,
|
||||
userId: originalHost.id,
|
||||
uid: bookingToReassignUid,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
startTime: `${dateStringPlusOne}T05:00:00.000Z`,
|
||||
endTime: `${dateStringPlusOne}T05:45:00.000Z`,
|
||||
attendees: [
|
||||
getMockBookingAttendee({
|
||||
id: 2,
|
||||
name: "attendee",
|
||||
email: "attendee@test.com",
|
||||
locale: "en",
|
||||
timeZone: "Asia/Kolkata",
|
||||
}),
|
||||
],
|
||||
references: [
|
||||
{
|
||||
id: 1,
|
||||
type: appStoreMetadata.googlecalendar.type,
|
||||
uid: "ORIGINAL_EVENT_ID",
|
||||
meetingId: "ORIGINAL_EVENT_ID",
|
||||
meetingPassword: null,
|
||||
meetingUrl: null,
|
||||
externalCalendarId: "MOCK_EXTERNAL_CALENDAR_ID",
|
||||
credentialId: 1,
|
||||
deleted: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
organizer: originalHost,
|
||||
usersApartFromOrganizer: [newHost],
|
||||
apps: [TestData.apps["google-calendar"]],
|
||||
})
|
||||
);
|
||||
|
||||
const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", {
|
||||
create: { uid: "NEW_EVENT_ID" },
|
||||
update: { uid: "UPDATED_EVENT_ID" },
|
||||
});
|
||||
|
||||
await roundRobinReassignment({
|
||||
bookingId: 123,
|
||||
});
|
||||
|
||||
// Verify that calendar deletion occurred (may be called multiple times due to duplicate references)
|
||||
expect(calendarMock.deleteEventCalls.length).toBeGreaterThanOrEqual(1);
|
||||
const deleteCall = calendarMock.deleteEventCalls[0];
|
||||
expect(deleteCall.args.uid).toBe("ORIGINAL_EVENT_ID");
|
||||
expect(deleteCall.args.externalCalendarId).toBe("MOCK_EXTERNAL_CALENDAR_ID");
|
||||
expect(deleteCall.args.event.organizer.email).toBe(newHost.email); // Current implementation uses new host credentials
|
||||
expect(deleteCall.args.event.uid).toBe(bookingToReassignUid);
|
||||
|
||||
// Verify that creation occurred with new host credentials
|
||||
expect(calendarMock.createEventCalls.length).toBe(1);
|
||||
const createCall = calendarMock.createEventCalls[0];
|
||||
expect(createCall.args.calEvent.organizer.email).toBe(newHost.email);
|
||||
|
||||
// Verify the booking was reassigned to the new host
|
||||
expectBookingToBeInDatabase({
|
||||
uid: bookingToReassignUid,
|
||||
userId: newHost.id,
|
||||
});
|
||||
|
||||
expectSuccessfulRoundRobinReschedulingEmails({
|
||||
prevOrganizer: originalHost,
|
||||
newOrganizer: newHost,
|
||||
emails,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -570,7 +570,8 @@ export default class EventManager {
|
||||
newBookingId?: number,
|
||||
changedOrganizer?: boolean,
|
||||
previousHostDestinationCalendar?: DestinationCalendar[] | null,
|
||||
isBookingRequestedReschedule?: boolean
|
||||
isBookingRequestedReschedule?: boolean,
|
||||
skipDeleteEventsAndMeetings?: boolean
|
||||
): Promise<CreateUpdateResult> {
|
||||
const originalEvt = processLocation(event);
|
||||
const evt = cloneDeep(originalEvt);
|
||||
@@ -637,7 +638,7 @@ export default class EventManager {
|
||||
const shouldUpdateBookingReferences =
|
||||
!!changedOrganizer || isLocationChanged || !!isBookingRequestedReschedule || isDailyVideoRoomExpired;
|
||||
|
||||
if (evt.requiresConfirmation && !changedOrganizer) {
|
||||
if (evt.requiresConfirmation && !skipDeleteEventsAndMeetings) {
|
||||
log.debug("RescheduleRequiresConfirmation: Deleting Event and Meeting for previous booking");
|
||||
// As the reschedule requires confirmation, we can't update the events and meetings to new time yet. So, just delete them and let it be handled when organizer confirms the booking.
|
||||
await this.deleteEventsAndMeetings({
|
||||
@@ -646,6 +647,14 @@ export default class EventManager {
|
||||
});
|
||||
} else {
|
||||
if (changedOrganizer) {
|
||||
if (!skipDeleteEventsAndMeetings) {
|
||||
log.debug("RescheduleOrganizerChanged: Deleting Event and Meeting for previous booking");
|
||||
await this.deleteEventsAndMeetings({
|
||||
event: { ...event, destinationCalendar: previousHostDestinationCalendar },
|
||||
bookingReferences: booking.references,
|
||||
});
|
||||
}
|
||||
|
||||
log.debug("RescheduleOrganizerChanged: Creating Event and Meeting for for new booking");
|
||||
const createdEvent = await this.create(originalEvt);
|
||||
results.push(...createdEvent.results);
|
||||
|
||||
@@ -53,6 +53,7 @@ export class BusyTimesService {
|
||||
})[]
|
||||
| null;
|
||||
bypassBusyCalendarTimes: boolean;
|
||||
silentlyHandleCalendarFailures?: boolean;
|
||||
shouldServeCache?: boolean;
|
||||
}) {
|
||||
const {
|
||||
@@ -70,6 +71,7 @@ export class BusyTimesService {
|
||||
rescheduleUid,
|
||||
duration,
|
||||
bypassBusyCalendarTimes = false,
|
||||
silentlyHandleCalendarFailures = false,
|
||||
shouldServeCache,
|
||||
} = params;
|
||||
|
||||
@@ -198,63 +200,71 @@ export class BusyTimesService {
|
||||
);
|
||||
|
||||
if (!calendarBusyTimesQuery.success) {
|
||||
throw new Error(
|
||||
`Failed to fetch busy calendar times for selected calendars ${selectedCalendars.map(
|
||||
(calendar) => calendar.id
|
||||
)}`
|
||||
if (silentlyHandleCalendarFailures) {
|
||||
logger.warn(
|
||||
`Calendar busy times fetch failed but handling silently due to silentlyHandleCalendarFailures flag for user ${username}`,
|
||||
{
|
||||
selectedCalendarIds: selectedCalendars.map((calendar) => calendar.id),
|
||||
}
|
||||
);
|
||||
} else {
|
||||
throw new Error(
|
||||
`Failed to fetch busy calendar times for selected calendars ${selectedCalendars.map(
|
||||
(calendar) => calendar.id
|
||||
)}`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const calendarBusyTimes = calendarBusyTimesQuery.data;
|
||||
const endConnectedCalendarsGet = performance.now();
|
||||
logger.debug(
|
||||
`Connected Calendars get took ${
|
||||
endConnectedCalendarsGet - startConnectedCalendarsGet
|
||||
} ms for user ${username}`,
|
||||
JSON.stringify({
|
||||
eventTypeId,
|
||||
startTimeDate,
|
||||
endTimeDate,
|
||||
calendarBusyTimes,
|
||||
})
|
||||
);
|
||||
|
||||
const openSeatsDateRanges = Object.keys(bookingSeatCountMap).map((key) => {
|
||||
const [start, end] = key.split("<>");
|
||||
return {
|
||||
start: dayjs(start),
|
||||
end: dayjs(end),
|
||||
};
|
||||
});
|
||||
|
||||
if (rescheduleUid) {
|
||||
const originalRescheduleBooking = bookings.find((booking) => booking.uid === rescheduleUid);
|
||||
if (originalRescheduleBooking) {
|
||||
openSeatsDateRanges.push({
|
||||
start: dayjs(originalRescheduleBooking.startTime),
|
||||
end: dayjs(originalRescheduleBooking.endTime),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const result = subtract(
|
||||
calendarBusyTimes.map((value) => ({
|
||||
...value,
|
||||
end: dayjs(value.end),
|
||||
start: dayjs(value.start),
|
||||
})),
|
||||
openSeatsDateRanges
|
||||
);
|
||||
|
||||
busyTimes.push(
|
||||
...result.map((busyTime) => ({
|
||||
...busyTime,
|
||||
start: busyTime.start.subtract(afterEventBuffer || 0, "minute").toDate(),
|
||||
end: busyTime.end.add(beforeEventBuffer || 0, "minute").toDate(),
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
const calendarBusyTimes = calendarBusyTimesQuery.data;
|
||||
const endConnectedCalendarsGet = performance.now();
|
||||
logger.debug(
|
||||
`Connected Calendars get took ${
|
||||
endConnectedCalendarsGet - startConnectedCalendarsGet
|
||||
} ms for user ${username}`,
|
||||
JSON.stringify({
|
||||
eventTypeId,
|
||||
startTimeDate,
|
||||
endTimeDate,
|
||||
calendarBusyTimes,
|
||||
})
|
||||
);
|
||||
|
||||
const openSeatsDateRanges = Object.keys(bookingSeatCountMap).map((key) => {
|
||||
const [start, end] = key.split("<>");
|
||||
return {
|
||||
start: dayjs(start),
|
||||
end: dayjs(end),
|
||||
};
|
||||
});
|
||||
|
||||
if (rescheduleUid) {
|
||||
const originalRescheduleBooking = bookings.find((booking) => booking.uid === rescheduleUid);
|
||||
// calendar busy time from original rescheduled booking should not be blocked
|
||||
if (originalRescheduleBooking) {
|
||||
openSeatsDateRanges.push({
|
||||
start: dayjs(originalRescheduleBooking.startTime),
|
||||
end: dayjs(originalRescheduleBooking.endTime),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const result = subtract(
|
||||
calendarBusyTimes.map((value) => ({
|
||||
...value,
|
||||
end: dayjs(value.end),
|
||||
start: dayjs(value.start),
|
||||
})),
|
||||
openSeatsDateRanges
|
||||
);
|
||||
|
||||
busyTimes.push(
|
||||
...result.map((busyTime) => ({
|
||||
...busyTime,
|
||||
start: busyTime.start.subtract(afterEventBuffer || 0, "minute").toDate(),
|
||||
end: busyTime.end.add(beforeEventBuffer || 0, "minute").toDate(),
|
||||
}))
|
||||
);
|
||||
|
||||
/*
|
||||
// TODO: Disabled until we can filter Zoom events by date. Also this is adding too much latency.
|
||||
const videoBusyTimes = (await getBusyVideoTimes(credentials)).filter(notEmpty);
|
||||
|
||||
@@ -54,6 +54,7 @@ const availabilitySchema = z
|
||||
withSource: z.boolean().optional(),
|
||||
returnDateOverrides: z.boolean(),
|
||||
bypassBusyCalendarTimes: z.boolean().optional(),
|
||||
silentlyHandleCalendarFailures: z.boolean().optional(),
|
||||
shouldServeCache: z.boolean().optional(),
|
||||
})
|
||||
.refine((data) => !!data.username || !!data.userId, "Either username or userId should be filled in.");
|
||||
@@ -110,6 +111,7 @@ type GetUserAvailabilityQuery = {
|
||||
duration?: number;
|
||||
returnDateOverrides: boolean;
|
||||
bypassBusyCalendarTimes: boolean;
|
||||
silentlyHandleCalendarFailures?: boolean;
|
||||
shouldServeCache?: boolean;
|
||||
};
|
||||
|
||||
@@ -295,6 +297,7 @@ export class UserAvailabilityService {
|
||||
duration,
|
||||
returnDateOverrides,
|
||||
bypassBusyCalendarTimes = false,
|
||||
silentlyHandleCalendarFailures = false,
|
||||
shouldServeCache,
|
||||
} = availabilitySchema.parse(query);
|
||||
|
||||
@@ -457,6 +460,7 @@ export class UserAvailabilityService {
|
||||
duration,
|
||||
currentBookings: initialData?.currentBookings,
|
||||
bypassBusyCalendarTimes,
|
||||
silentlyHandleCalendarFailures,
|
||||
shouldServeCache,
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -64,6 +64,7 @@ const BookerPlatformWrapperComponent = (
|
||||
onTimeslotsLoaded,
|
||||
startTime: customStartTime,
|
||||
showNoAvailabilityDialog,
|
||||
silentlyHandleCalendarFailures = false,
|
||||
} = props;
|
||||
const layout = BookerLayouts[view];
|
||||
|
||||
@@ -310,6 +311,7 @@ const BookerPlatformWrapperComponent = (
|
||||
Boolean(event?.data?.id),
|
||||
orgSlug: props.entity?.orgSlug ?? undefined,
|
||||
eventTypeSlug: isDynamic ? "dynamic" : eventSlug || "",
|
||||
_silentCalendarFailures: silentlyHandleCalendarFailures,
|
||||
...routingParams,
|
||||
});
|
||||
|
||||
|
||||
@@ -85,6 +85,7 @@ export type BookerPlatformWrapperAtomProps = Omit<
|
||||
onTimeslotsLoaded?: (slots: Record<string, Slot[]>) => void;
|
||||
startTime?: string | Date;
|
||||
roundRobinHideOrgAndTeam?: boolean;
|
||||
silentlyHandleCalendarFailures?: boolean;
|
||||
};
|
||||
|
||||
type VIEW_TYPE = keyof typeof BookerLayouts;
|
||||
|
||||
@@ -136,6 +136,7 @@ export default function Bookings(props: { calUsername: string; calEmail: string
|
||||
: { username: props.calUsername })}
|
||||
hostsLimit={3}
|
||||
allowUpdatingUrlParams={true}
|
||||
silentlyHandleCalendarFailures={false}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -194,6 +194,12 @@ export class GetAvailableSlotsInput_2024_04_15 {
|
||||
@ApiHideProperty()
|
||||
_bypassCalendarBusyTimes?: boolean;
|
||||
|
||||
@Transform(({ value }) => value && value.toLowerCase() === "true")
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@ApiHideProperty()
|
||||
_silentCalendarFailures?: boolean;
|
||||
|
||||
@Transform(({ value }) => (value ? value.toLowerCase() === "true" : false))
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,46 @@
|
||||
import prismaMock from "../../../../../../tests/libs/__mocks__/prismaMock";
|
||||
|
||||
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import { duplicateHandler } from "./duplicate.handler";
|
||||
|
||||
vi.mock("@calcom/prisma", () => ({
|
||||
default: prismaMock,
|
||||
}));
|
||||
vi.mock("@calcom/lib/server/repository/eventTypeRepository");
|
||||
|
||||
describe("duplicateHandler", () => {
|
||||
const ctx = { user: { id: 1, profile: { id: 1 } } } as any;
|
||||
const input = { id: 123, slug: "test-event", title: "Test", description: "Test", length: 30, teamId: null };
|
||||
const eventType = { id: 123, userId: 1, teamId: null, users: [{ id: 1 }] };
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
prismaMock.eventType.findUnique.mockResolvedValue(eventType);
|
||||
});
|
||||
|
||||
it("should throw BAD_REQUEST in case of unique constraint violation", async () => {
|
||||
const { EventTypeRepository } = await import("@calcom/lib/server/repository/eventTypeRepository");
|
||||
vi.mocked(EventTypeRepository).mockImplementation(
|
||||
() =>
|
||||
({
|
||||
create: vi.fn().mockRejectedValue(
|
||||
new PrismaClientKnownRequestError("Unique constraint failed", {
|
||||
code: "P2002",
|
||||
clientVersion: "mockedVersion",
|
||||
})
|
||||
),
|
||||
} as any)
|
||||
);
|
||||
|
||||
await expect(duplicateHandler({ ctx, input })).rejects.toThrow(
|
||||
new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Unique constraint violation while creating a duplicate event.",
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -226,6 +226,13 @@ export const duplicateHandler = async ({ ctx, input }: DuplicateOptions) => {
|
||||
eventType: newEventType,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002") {
|
||||
// unique constraint violation
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Unique constraint violation while creating a duplicate event.",
|
||||
});
|
||||
}
|
||||
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: `Error duplicating event type ${error}` });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -35,6 +35,7 @@ export const getScheduleSchemaObject = z.object({
|
||||
skipContactOwner: z.boolean().nullish(),
|
||||
_enableTroubleshooter: z.boolean().optional(),
|
||||
_bypassCalendarBusyTimes: z.boolean().optional(),
|
||||
_silentCalendarFailures: z.boolean().optional(),
|
||||
_shouldServeCache: z.boolean().optional(),
|
||||
routingFormResponseId: z.number().optional(),
|
||||
queuedFormResponseId: z.string().nullish(),
|
||||
|
||||
@@ -737,6 +737,7 @@ export class AvailableSlotsService {
|
||||
startTime,
|
||||
endTime,
|
||||
bypassBusyCalendarTimes,
|
||||
silentCalendarFailures,
|
||||
shouldServeCache,
|
||||
}: {
|
||||
input: TGetScheduleInputSchema;
|
||||
@@ -753,6 +754,7 @@ export class AvailableSlotsService {
|
||||
startTime: ReturnType<(typeof AvailableSlotsService)["prototype"]["getStartTime"]>;
|
||||
endTime: Dayjs;
|
||||
bypassBusyCalendarTimes: boolean;
|
||||
silentCalendarFailures: boolean;
|
||||
shouldServeCache?: boolean;
|
||||
}) {
|
||||
const usersWithCredentials = this.getUsersWithCredentials({
|
||||
@@ -887,6 +889,7 @@ export class AvailableSlotsService {
|
||||
duration: input.duration || 0,
|
||||
returnDateOverrides: false,
|
||||
bypassBusyCalendarTimes,
|
||||
silentlyHandleCalendarFailures: silentCalendarFailures,
|
||||
shouldServeCache,
|
||||
},
|
||||
initialData: {
|
||||
@@ -958,6 +961,7 @@ export class AvailableSlotsService {
|
||||
const {
|
||||
_enableTroubleshooter: enableTroubleshooter = false,
|
||||
_bypassCalendarBusyTimes: bypassBusyCalendarTimes = false,
|
||||
_silentCalendarFailures: silentCalendarFailures = false,
|
||||
_shouldServeCache,
|
||||
routingFormResponseId,
|
||||
queuedFormResponseId,
|
||||
@@ -1069,6 +1073,7 @@ export class AvailableSlotsService {
|
||||
? this.getStartTime(twoWeeksFromNow.format(), input.timeZone, eventType.minimumBookingNotice)
|
||||
: endTime,
|
||||
bypassBusyCalendarTimes,
|
||||
silentCalendarFailures,
|
||||
shouldServeCache,
|
||||
});
|
||||
|
||||
@@ -1095,6 +1100,7 @@ export class AvailableSlotsService {
|
||||
startTime: dayjs(),
|
||||
endTime: twoWeeksFromNow,
|
||||
bypassBusyCalendarTimes,
|
||||
silentCalendarFailures,
|
||||
shouldServeCache,
|
||||
});
|
||||
if (
|
||||
@@ -1129,6 +1135,7 @@ export class AvailableSlotsService {
|
||||
startTime,
|
||||
endTime,
|
||||
bypassBusyCalendarTimes,
|
||||
silentCalendarFailures,
|
||||
shouldServeCache,
|
||||
}));
|
||||
aggregatedAvailability = getAggregatedAvailability(allUsersAvailability, eventType.schedulingType);
|
||||
|
||||
@@ -2500,7 +2500,7 @@ __metadata:
|
||||
"@axiomhq/winston": ^1.2.0
|
||||
"@calcom/platform-constants": "*"
|
||||
"@calcom/platform-enums": "*"
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.306"
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.311"
|
||||
"@calcom/platform-types": "*"
|
||||
"@calcom/platform-utils": "*"
|
||||
"@calcom/prisma": "*"
|
||||
@@ -3559,13 +3559,13 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.306":
|
||||
version: 0.0.306
|
||||
resolution: "@calcom/platform-libraries@npm:0.0.306"
|
||||
"@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.311":
|
||||
version: 0.0.311
|
||||
resolution: "@calcom/platform-libraries@npm:0.0.311"
|
||||
dependencies:
|
||||
"@calcom/features": "*"
|
||||
"@calcom/lib": "*"
|
||||
checksum: 602b8b446508429096b8e03e9edbe1ff744cf7c7af3fbe8a683aaaf0d3c96d1cd30ff356f72682449ca072beadaae95fdaa5c44b26e30cfce3bb838f519216f9
|
||||
checksum: 7a38e8e0478dfe163a97ddab89285b98d7c7e86a73d3659bc29e31c059782708bfe2466e50657f6861e1c67bcfc66e1e3e44803ab3b5f65fd0fe656e880bb916
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user