feat: virtual queues tab in insights (#18260)

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: sean-brydon <sean@cal.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
This commit is contained in:
Carina Wollendorfer
2024-12-23 19:16:38 +00:00
committed by GitHub
co-authored by CarinaWolli sean-brydon sean-brydon Peer Richelsen Omar López
parent 5fe4e09975
commit cfb09a1b21
39 changed files with 538 additions and 195 deletions
@@ -0,0 +1,17 @@
import { withAppDirSsr } from "app/WithAppDirSsr";
import { _generateMetadata } from "app/_utils";
import { WithLayout } from "app/layoutHOC";
import { getServerSideProps } from "@lib/insights/getServerSideProps";
import InsightsVirtualQueuesPage from "~/insights/insights-virtual-queues-view";
export const generateMetadata = async () =>
await _generateMetadata(
(t) => t("insights"),
(t) => t("insights_subtitle")
);
const getData = withAppDirSsr(getServerSideProps);
export default WithLayout({ getLayout: null, getData, Page: InsightsVirtualQueuesPage });
@@ -18,6 +18,7 @@ import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { Team } from "@calcom/prisma/client";
import type { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
import { eventTypeMetaDataSchemaWithTypedApps } from "@calcom/prisma/zod-utils";
import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { trpc } from "@calcom/trpc/react";
import type { AppMeta } from "@calcom/types/App";
@@ -243,15 +244,20 @@ const OnboardingPage = ({
const promises = group.eventTypes
.filter((eventType) => eventType.selected)
.map((value: TEventType) => {
// Prevent two payment apps to be enabled
// Ok to cast type here because this metadata will be updated as the event type metadata
if (
checkForMultiplePaymentApps(value.metadata as z.infer<typeof EventTypeMetaDataSchema>)
)
throw new Error(t("event_setup_multiple_payment_apps_error"));
if (value.metadata?.apps?.stripe?.paymentOption === "HOLD" && value.seatsPerTimeSlot) {
throw new Error(t("seats_and_no_show_fee_error"));
if (value.metadata) {
const metadata = eventTypeMetaDataSchemaWithTypedApps.parse(value.metadata);
// Prevent two payment apps to be enabled
// Ok to cast type here because this metadata will be updated as the event type metadata
if (checkForMultiplePaymentApps(metadata))
throw new Error(t("event_setup_multiple_payment_apps_error"));
if (
value.metadata?.apps?.stripe?.paymentOption === "HOLD" &&
value.seatsPerTimeSlot
) {
throw new Error(t("seats_and_no_show_fee_error"));
}
}
let updateObject: TUpdateObject = { id: value.id };
if (isConferencing) {
updateObject = {
@@ -48,7 +48,7 @@ import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
import { getIs24hClockFromLocalStorage, isBrowserLocale24h } from "@calcom/lib/timeFormat";
import { localStorage } from "@calcom/lib/webstorage";
import { BookingStatus, SchedulingType } from "@calcom/prisma/enums";
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
import { bookingMetadataSchema, eventTypeMetaDataSchemaWithTypedApps } from "@calcom/prisma/zod-utils";
import { trpc } from "@calcom/trpc/react";
import {
Alert,
@@ -61,6 +61,7 @@ import {
showToast,
EmptyScreen,
Icon,
HeadSeo,
} from "@calcom/ui";
import CancelBooking from "@calcom/web/components/booking/CancelBooking";
import EventReservationSchema from "@calcom/web/components/schemas/EventReservationSchema";
@@ -229,7 +230,13 @@ export default function Success(props: PageProps) {
t,
};
const giphyAppData = getEventTypeAppData(eventType, "giphy");
const giphyAppData = getEventTypeAppData(
{
...eventType,
metadata: eventTypeMetaDataSchemaWithTypedApps.parse(eventType.metadata),
},
"giphy"
);
const giphyImage = giphyAppData?.thankYouPage;
const isRoundRobin = eventType.schedulingType === SchedulingType.ROUND_ROBIN;
@@ -433,7 +440,10 @@ export default function Success(props: PageProps) {
</Link>
</div>
)}
<BookingPageTagManager eventType={eventType} />
<HeadSeo origin={getOrgFullOrigin(orgSlug)} title={title} description={title} />
<BookingPageTagManager
eventType={{ ...eventType, metadata: eventTypeMetaDataSchemaWithTypedApps.parse(eventType.metadata) }}
/>
<main className={classNames(shouldAlignCentrally ? "mx-auto" : "", isEmbed ? "" : "max-w-3xl")}>
<div className={classNames("overflow-y-auto", isEmbed ? "" : "z-50 ")}>
<div
@@ -0,0 +1,49 @@
"use client";
import { useState } from "react";
import { TestForm } from "@calcom/app-store/routing-forms/components/SingleForm";
import type { RoutingForm } from "@calcom/app-store/routing-forms/types/types";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc";
import { Label, Select } from "@calcom/ui";
import InsightsLayout from "./layout";
export default function InsightsVirtualQueuesPage() {
const { t } = useLocale();
const { data: routingForms, isLoading: isRoutingFormsLoading } =
trpc.viewer.insights.getUserRelevantTeamRoutingForms.useQuery();
const [selectedForm, setSelectedForm] = useState<RoutingForm | undefined>(
routingForms && routingForms.length > 0 ? routingForms[0] : undefined
);
if (routingForms && !selectedForm && routingForms.length > 0) {
setSelectedForm(routingForms[0]);
}
return (
<InsightsLayout>
<Label>{t("routing_form")}</Label>
<Select
placeholder="Select project"
options={routingForms?.map((form) => ({ label: form.name, value: form.id })) ?? []}
isLoading={isRoutingFormsLoading}
className="w-60"
onChange={(e) => {
if (e && routingForms) {
const form = routingForms.find((form) => form.id === e.value);
setSelectedForm(form);
}
}}
value={selectedForm ? { label: selectedForm.name, value: selectedForm.id } : undefined}
/>
<div className="mt-10">
{selectedForm ? <TestForm form={selectedForm} showAllData={false} /> : <></>}
</div>
</InsightsLayout>
);
return <></>;
}
+3
View File
@@ -128,6 +128,9 @@ vi.mock("@calcom/prisma/zod-utils", () => ({
EventTypeMetaDataSchema: {
parse: vi.fn(),
},
eventTypeMetaDataSchemaWithTypedApps: {
parse: vi.fn(),
},
bookingMetadataSchema: {
parse: vi.fn(),
},
@@ -3,7 +3,7 @@ import type Prisma from "@prisma/client";
import prisma from "@calcom/prisma";
import { SchedulingType } from "@calcom/prisma/enums";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { eventTypeMetaDataSchemaWithTypedApps } from "@calcom/prisma/zod-utils";
import { test, todo } from "./lib/fixtures";
import type { Fixtures } from "./lib/fixtures";
@@ -51,7 +51,7 @@ test.describe("Stripe integration skip true", () => {
},
});
const metadata = EventTypeMetaDataSchema.parse(eventTypeMetadata?.metadata);
const metadata = eventTypeMetaDataSchemaWithTypedApps.parse(eventTypeMetadata?.metadata);
const stripeAppMetadata = metadata?.apps?.stripe;
@@ -93,7 +93,7 @@ test.describe("Stripe integration skip true", () => {
},
});
const metadata = EventTypeMetaDataSchema.parse(eventTypeMetadata?.metadata);
const metadata = eventTypeMetaDataSchemaWithTypedApps.parse(eventTypeMetadata?.metadata);
const stripeAppMetadata = metadata?.apps?.stripe;
@@ -291,7 +291,7 @@ test.describe("Stripe integration with the new app install flow skip false", ()
});
for (const eventTypeMetadata of eventTypeMetadatas) {
const metadata = EventTypeMetaDataSchema.parse(eventTypeMetadata?.metadata);
const metadata = eventTypeMetaDataSchemaWithTypedApps.parse(eventTypeMetadata?.metadata);
const stripeAppMetadata = metadata?.apps?.stripe;
expect(stripeAppMetadata).toHaveProperty("credentialId");
expect(typeof stripeAppMetadata?.credentialId).toBe("number");
@@ -328,7 +328,7 @@ test.describe("Stripe integration with the new app install flow skip false", ()
},
});
const metadata = EventTypeMetaDataSchema.parse(eventTypeMetadata?.metadata);
const metadata = eventTypeMetaDataSchemaWithTypedApps.parse(eventTypeMetadata?.metadata);
const stripeAppMetadata = metadata?.apps?.stripe;
@@ -2895,5 +2895,12 @@
"reassigned": "Reassigned",
"rerouted": "Rerouted",
"salesforce_assigned": "Salesforce assignment",
"router_position": "Router Position",
"show_matching_hosts": "Show matching hosts",
"no_active_queues": "No active queues for this input",
"weight": "Weight",
"calibration": "Calibration",
"shortfall": "Shortfall",
"routing_form": "Routing Form",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -1,9 +1,11 @@
import type { z } from "zod";
import type { BookerEvent } from "@calcom/features/bookings/types";
import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import type { eventTypeMetaDataSchemaWithTypedApps } from "@calcom/prisma/zod-utils";
export type EventTypeApps = NonNullable<NonNullable<z.infer<typeof EventTypeMetaDataSchema>>["apps"]>;
export type EventTypeApps = NonNullable<
NonNullable<z.infer<typeof eventTypeMetaDataSchemaWithTypedApps>>["apps"]
>;
export type EventTypeAppsList = keyof EventTypeApps;
export const getEventTypeAppData = <T extends EventTypeAppsList>(
@@ -1,6 +1,6 @@
import type z from "zod";
import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import type { eventTypeAppMetadataOptionalSchema } from "@calcom/prisma/zod-utils";
import type { appDataSchemas } from "../../apps.schemas.generated";
@@ -11,7 +11,12 @@ import type { appDataSchemas } from "../../apps.schemas.generated";
* @returns boolean
*/
const checkForMultiplePaymentApps = (
metadata: z.infer<typeof EventTypeMetaDataSchema>,
metadata:
| {
apps?: z.infer<typeof eventTypeAppMetadataOptionalSchema>;
}
| null
| undefined,
inclusive = false
) => {
let enabledPaymentApps = 0;
@@ -255,9 +255,11 @@ type MembersMatchResultType = {
const TeamMembersMatchResult = ({
membersMatchResult,
chosenRouteName,
showAllData,
}: {
membersMatchResult: MembersMatchResultType;
chosenRouteName: string;
showAllData: boolean;
}) => {
const { t } = useLocale();
if (!membersMatchResult) return null;
@@ -284,6 +286,7 @@ const TeamMembersMatchResult = ({
const renderQueue = () => {
if (isNoLogicFound(membersMatchResult.teamMembersMatchingAttributeLogic)) {
if (!showAllData) return <div className="mt-4">{t("no_active_queues")}asdf</div>;
if (membersMatchResult.checkedFallback) {
return (
<span className="font-semibold">
@@ -311,12 +314,14 @@ const TeamMembersMatchResult = ({
<thead>
<tr className="border-b text-left">
<th className="py-2 pr-4">#</th>
<th className="py-2 pr-4">Email</th>
<th className="py-2 pr-4">Bookings</th>
{membersMatchResult.perUserData.weights ? <th className="py-2">Weight</th> : null}
{membersMatchResult.perUserData.calibrations ? <th className="py-2">Calibration</th> : null}
<th className="py-2 pr-4">{t("email")}</th>
<th className="py-2 pr-4">{t("bookings")}</th>
{membersMatchResult.perUserData.weights ? <th className="py-2">{t("weight")}</th> : null}
{membersMatchResult.perUserData.calibrations ? (
<th className="py-2">{t("calibration")}</th>
) : null}
{membersMatchResult.perUserData.bookingShortfalls ? (
<th className="border-l py-2 pl-2">Shortfall</th>
<th className="border-l py-2 pl-2">{t("shortfall")}</th>
) : null}
</tr>
</thead>
@@ -353,41 +358,61 @@ const TeamMembersMatchResult = ({
return (
<div className="text-default mt-2 space-y-2">
<div data-testid="chosen-route">
{t("chosen_route")}: <span className="font-semibold">{chosenRouteName}</span>
</div>
<div data-testid="attribute-logic-matched" className={cn(hasMainWarnings && "text-error")}>
{t("attribute_logic_matched")}: <span className="font-semibold">{renderMainLogicStatus()}</span>
{hasMainWarnings && (
<Alert className="mt-2" severity="warning" title={membersMatchResult.mainWarnings?.join(", ")} />
)}
</div>
<div data-testid="attribute-logic-fallback-matched" className={cn(hasFallbackWarnings && "text-error")}>
{t("attribute_logic_fallback_matched")}:{" "}
<span className="font-semibold">{renderFallbackLogicStatus()}</span>
{hasFallbackWarnings && (
<Alert
className="mt-2"
severity="warning"
title={membersMatchResult.fallbackWarnings?.join(", ")}
/>
)}
</div>
{showAllData ? (
<>
<div data-testid="chosen-route">
{t("chosen_route")}: <span className="font-semibold">{chosenRouteName}</span>
</div>
<div data-testid="attribute-logic-matched" className={cn(hasMainWarnings && "text-error")}>
{t("attribute_logic_matched")}: <span className="font-semibold">{renderMainLogicStatus()}</span>
{hasMainWarnings && (
<Alert
className="mt-2"
severity="warning"
title={membersMatchResult.mainWarnings?.join(", ")}
/>
)}
</div>
<div
data-testid="attribute-logic-fallback-matched"
className={cn(hasFallbackWarnings && "text-error")}>
{t("attribute_logic_fallback_matched")}:{" "}
<span className="font-semibold">{renderFallbackLogicStatus()}</span>
{hasFallbackWarnings && (
<Alert
className="mt-2"
severity="warning"
title={membersMatchResult.fallbackWarnings?.join(", ")}
/>
)}
</div>
</>
) : (
<></>
)}
<div className="mt-4">
{membersMatchResult.contactOwnerEmail ? (
<div data-testid="contact-owner-email">
{t("contact_owner")}:{" "}
<span className="font-semibold">{membersMatchResult.contactOwnerEmail}</span>
</div>
) : (
) : showAllData ? (
<div data-testid="contact-owner-email">
{t("contact_owner")}: <span className="font-semibold">Not found</span>
</div>
) : (
<></>
)}
<div className="mt-2" data-testid="matching-members">
{membersMatchResult.isUsingAttributeWeights
? t("matching_members_queue_using_attribute_weights")
: t("matching_members_queue_using_event_assignee_weights")}
{showAllData ? (
<>
{membersMatchResult.isUsingAttributeWeights
? t("matching_members_queue_using_attribute_weights")
: t("matching_members_queue_using_event_assignee_weights")}
</>
) : (
<></>
)}
{renderQueue()}
</div>
</div>
@@ -410,14 +435,14 @@ type UptoDateForm = Brand<
"UptoDateForm"
>;
export const TestFormDialog = ({
export const TestForm = ({
form,
isTestPreviewOpen,
setIsTestPreviewOpen,
showAllData = true,
renderFooter,
}: {
form: UptoDateForm;
isTestPreviewOpen: boolean;
setIsTestPreviewOpen: (value: boolean) => void;
form: UptoDateForm | RoutingForm;
showAllData?: boolean;
renderFooter?: (onClose: () => void) => React.ReactNode;
}) => {
const { t } = useLocale();
const [response, setResponse] = useState<FormResponse>({});
@@ -458,12 +483,15 @@ export const TestFormDialog = ({
let eventTypeRedirectUrl: string | null = null;
if (route?.action?.type === "eventTypeRedirectUrl") {
eventTypeRedirectUrl = getAbsoluteEventTypeRedirectUrl({
eventTypeRedirectUrl: route.action.value,
form,
allURLSearchParams: new URLSearchParams(),
});
setEventTypeUrlWithoutParams(eventTypeRedirectUrl);
// only needed in routing form testing (type UptoDateForm)
if ("team" in form) {
eventTypeRedirectUrl = getAbsoluteEventTypeRedirectUrl({
eventTypeRedirectUrl: route.action.value,
form,
allURLSearchParams: new URLSearchParams(),
});
setEventTypeUrlWithoutParams(eventTypeRedirectUrl);
}
}
setChosenRoute(route || null);
@@ -481,7 +509,7 @@ export const TestFormDialog = ({
}
}
const renderTestResult = () => {
const renderTestResult = (showAllData: boolean) => {
if (!form.routes || !chosenRoute) return null;
const chosenRouteIndex = form.routes.findIndex((route) => route.id === chosenRoute.id);
@@ -493,6 +521,33 @@ export const TestFormDialog = ({
return `Route ${chosenRouteIndex + 1}`;
};
const renderTeamMembersMatchResult = (showAllData: boolean, isPending: boolean) => {
if (!isTeamForm) return null;
if (isPending) return <div>Loading...</div>;
return (
<div>
<TeamMembersMatchResult
chosenRouteName={chosenRouteName()}
membersMatchResult={membersMatchResult}
showAllData={showAllData}
/>
</div>
);
};
if (!showAllData) {
if (
chosenRoute.action.type !== "customPageMessage" &&
chosenRoute.action.type !== "externalRedirectUrl"
) {
{
return renderTeamMembersMatchResult(false, findTeamMembersMatchingAttributeLogicMutation.isPending);
}
}
return <div className="mt-4">{t("no_active_queues")}</div>;
}
return (
<div className="bg-subtle text-default mt-5 rounded-md p-3">
<div className="font-bold ">{t("route_to")}:</div>
@@ -539,18 +594,10 @@ export const TestFormDialog = ({
{chosenRoute.action.value}
</a>
</span>
{isTeamForm ? (
!findTeamMembersMatchingAttributeLogicMutation.isPending ? (
<div>
<TeamMembersMatchResult
chosenRouteName={chosenRouteName()}
membersMatchResult={membersMatchResult}
/>
</div>
) : (
<div>Loading...</div>
)
) : null}
{renderTeamMembersMatchResult(
showAllData,
findTeamMembersMatchingAttributeLogicMutation.isPending
)}
</div>
)}
</div>
@@ -558,36 +605,68 @@ export const TestFormDialog = ({
);
};
const onClose = () => {
setChosenRoute(null);
setResponse({});
};
return (
<form
onSubmit={(e) => {
e.preventDefault();
resetMembersMatchResult();
testRouting();
}}>
<div className="px-1">
{form && <FormInputFields form={form} response={response} setResponse={setResponse} />}
</div>
{!renderFooter ? (
<div className="mt-4">
<Button type="submit">{t("show_matching_hosts")}</Button>
</div>
) : (
<></>
)}
<div>{renderTestResult(showAllData)}</div>
{renderFooter?.(onClose)}
</form>
);
};
export const TestFormDialog = ({
form,
isTestPreviewOpen,
setIsTestPreviewOpen,
}: {
form: UptoDateForm;
isTestPreviewOpen: boolean;
setIsTestPreviewOpen: (value: boolean) => void;
}) => {
const { t } = useLocale();
return (
<Dialog open={isTestPreviewOpen} onOpenChange={setIsTestPreviewOpen}>
<DialogContent size="md" enableOverflow>
<DialogHeader title={t("test_routing_form")} subtitle={t("test_preview_description")} />
<div>
<form
onSubmit={(e) => {
e.preventDefault();
resetMembersMatchResult();
testRouting();
}}>
<div className="px-1">
{form && <FormInputFields form={form} response={response} setResponse={setResponse} />}
</div>
<div>{renderTestResult()}</div>
<DialogFooter>
<DialogClose
color="secondary"
onClick={() => {
setIsTestPreviewOpen(false);
setChosenRoute(null);
setResponse({});
}}>
{t("close")}
</DialogClose>
<Button type="submit" data-testid="test-routing">
{t("test_routing")}
</Button>
</DialogFooter>
</form>
<TestForm
form={form}
renderFooter={(onClose) => (
<DialogFooter>
<DialogClose
color="secondary"
onClick={() => {
setIsTestPreviewOpen(false);
onClose();
}}>
{t("close")}
</DialogClose>
<Button type="submit" data-testid="test-routing">
{t("test_routing")}
</Button>
</DialogFooter>
)}
/>
</div>
</DialogContent>
</Dialog>
+1 -4
View File
@@ -27,10 +27,7 @@ export const appDataSchema = eventTypeAppCardZod.extend({
.nativeEnum(SalesforceRecordEnum)
.default(SalesforceRecordEnum.CONTACT)
.optional(),
// Another way to define z.boolean().optional() through z.any()
// Required to prevent tsc crash. I have no clue why it started crashing suddenly.
// But we need to improve the schemas of all apps that are combined into apps.schemas.generated.ts
ifFreeEmailDomainSkipOwnerCheck: optionalBooleanOnlyRunTimeValidation,
ifFreeEmailDomainSkipOwnerCheck: z.boolean().optional(),
skipContactCreation: z.boolean().optional(),
createEventOn: z.nativeEnum(SalesforceRecordEnum).default(SalesforceRecordEnum.CONTACT).optional(),
createNewContactUnderAccount: z.boolean().optional(),
@@ -3,6 +3,7 @@ import type z from "zod";
import { UserRepository } from "@calcom/lib/server/repository/user";
import prisma from "@calcom/prisma";
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
import { eventTypeAppMetadataOptionalSchema } from "@calcom/prisma/zod-utils";
import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import type { CredentialPayload } from "@calcom/types/Credential";
@@ -76,8 +77,7 @@ export const getAllCredentials = async (
}
// Only return CRM credentials that are enabled on the event type
const eventTypeAppMetadata = eventType?.metadata?.apps;
console.log(eventTypeAppMetadata);
const eventTypeAppMetadata = eventTypeAppMetadataOptionalSchema.parse(eventType?.metadata?.apps);
// Will be [credentialId]: { enabled: boolean }]
const eventTypeCrmCredentials: Record<number, { enabled: boolean }> = {};
@@ -24,7 +24,7 @@ import type { PrismaClient } from "@calcom/prisma";
import type { SchedulingType } from "@calcom/prisma/enums";
import { BookingStatus, WebhookTriggerEvents } from "@calcom/prisma/enums";
import type { PlatformClientParams } from "@calcom/prisma/zod-utils";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { EventTypeMetaDataSchema, eventTypeAppMetadataOptionalSchema } from "@calcom/prisma/zod-utils";
import { getAllWorkflowsFromEventType } from "@calcom/trpc/server/routers/viewer/workflows/util";
import type { AdditionalInformation, CalendarEvent } from "@calcom/types/Calendar";
@@ -84,7 +84,8 @@ export async function handleConfirmation(args: {
} = args;
const eventType = booking.eventType;
const eventTypeMetadata = EventTypeMetaDataSchema.parse(eventType?.metadata || {});
const eventManager = new EventManager(user, eventTypeMetadata?.apps);
const apps = eventTypeAppMetadataOptionalSchema.parse(eventTypeMetadata?.apps);
const eventManager = new EventManager(user, apps);
const scheduleResult = await eventManager.create(evt);
const results = scheduleResult.results;
const metadata: AdditionalInformation = {};
@@ -68,6 +68,10 @@ import { WorkflowRepository } from "@calcom/lib/server/repository/workflow";
import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
import prisma from "@calcom/prisma";
import { BookingStatus, SchedulingType, WebhookTriggerEvents } from "@calcom/prisma/enums";
import {
eventTypeAppMetadataOptionalSchema,
eventTypeMetaDataSchemaWithTypedApps,
} from "@calcom/prisma/zod-utils";
import type { PlatformClientParams } from "@calcom/prisma/zod-utils";
import { userMetadata as userMetadataSchema } from "@calcom/prisma/zod-utils";
import { getAllWorkflowsFromEventType } from "@calcom/trpc/server/routers/viewer/workflows/util";
@@ -421,7 +425,10 @@ async function handler(
const isTeamEventType =
!!eventType.schedulingType && ["COLLECTIVE", "ROUND_ROBIN"].includes(eventType.schedulingType);
const paymentAppData = getPaymentAppData(eventType);
const paymentAppData = getPaymentAppData({
...eventType,
metadata: eventTypeMetaDataSchemaWithTypedApps.parse(eventType.metadata),
});
loggerWithEventDetails.info(
`Booking eventType ${eventTypeId} started`,
safeStringify({
@@ -1051,7 +1058,13 @@ async function handler(
oAuthClientId: platformClientId,
};
const workflows = await getAllWorkflowsFromEventType(eventType, organizerUser.id);
const workflows = await getAllWorkflowsFromEventType(
{
...eventType,
metadata: eventTypeMetaDataSchemaWithTypedApps.parse(eventType.metadata),
},
organizerUser.id
);
if (isTeamEventType) {
evt.team = {
@@ -1275,8 +1288,9 @@ async function handler(
// After polling videoBusyTimes, credentials might have been changed due to refreshment, so query them again.
const credentials = await monitorCallbackAsync(refreshCredentials, allCredentials);
const apps = eventTypeAppMetadataOptionalSchema.parse(eventType?.metadata?.apps);
const eventManager = !isDryRun
? new EventManager({ ...organizerUser, credentials }, eventType?.metadata?.apps)
? new EventManager({ ...organizerUser, credentials }, apps)
: buildDryRunEventManager();
let videoCallUrl;
@@ -14,6 +14,7 @@ import { HttpError } from "@calcom/lib/http-error";
import { handlePayment } from "@calcom/lib/payment/handlePayment";
import prisma from "@calcom/prisma";
import { BookingStatus } from "@calcom/prisma/enums";
import { eventTypeAppMetadataOptionalSchema } from "@calcom/prisma/zod-utils";
import { findBookingQuery } from "../../handleNewBooking/findBookingQuery";
import type { IEventTypePaymentCredentialType } from "../../handleNewBooking/types";
@@ -147,7 +148,8 @@ const createNewSeat = async (
);
}
const credentials = await refreshCredentials(allCredentials);
const eventManager = new EventManager({ ...organizerUser, credentials }, eventType?.metadata?.apps);
const apps = eventTypeAppMetadataOptionalSchema.parse(eventType?.metadata?.apps);
const eventManager = new EventManager({ ...organizerUser, credentials }, apps);
await eventManager.updateCalendarAttendees(evt, seatedBooking);
const foundBooking = await findBookingQuery(seatedBooking.id);
@@ -14,7 +14,8 @@ import { bookingMinimalSelect, prisma } from "@calcom/prisma";
import { AppCategories, BookingStatus } from "@calcom/prisma/enums";
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
import type { EventTypeAppMetadataSchema, EventTypeMetadata } from "@calcom/prisma/zod-utils";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { eventTypeMetaDataSchemaWithTypedApps } from "@calcom/prisma/zod-utils";
import { EventTypeMetaDataSchema, eventTypeAppMetadataOptionalSchema } from "@calcom/prisma/zod-utils";
import { userMetadata as userMetadataSchema } from "@calcom/prisma/zod-utils";
type App = {
@@ -151,9 +152,11 @@ const handleDeleteCredential = async ({
if (credential.app?.categories.includes(AppCategories.crm)) {
const metadata = EventTypeMetaDataSchema.parse(eventType.metadata);
const appSlugToDelete = credential.app?.slug;
const apps = eventTypeAppMetadataOptionalSchema.parse(metadata?.apps);
if (appSlugToDelete) {
const appMetadata = removeAppFromEventTypeMetadata(appSlugToDelete, metadata);
const appMetadata = removeAppFromEventTypeMetadata(appSlugToDelete, {
apps,
});
await prisma.$transaction(async () => {
await prisma.eventType.update({
@@ -179,7 +182,10 @@ const handleDeleteCredential = async ({
const metadata = EventTypeMetaDataSchema.parse(eventType.metadata);
const appSlug = credential.app?.slug;
if (appSlug) {
const appMetadata = removeAppFromEventTypeMetadata(appSlug, metadata);
const apps = eventTypeAppMetadataOptionalSchema.parse(metadata?.apps);
const appMetadata = removeAppFromEventTypeMetadata(appSlug, {
apps,
});
await prisma.$transaction(async () => {
await prisma.eventType.update({
@@ -358,7 +364,7 @@ const handleDeleteCredential = async ({
} else if (
appStoreMetadata[credential.app?.slug as keyof typeof appStoreMetadata]?.extendsFeature === "EventType"
) {
const metadata = EventTypeMetaDataSchema.parse(eventType.metadata);
const metadata = eventTypeMetaDataSchemaWithTypedApps.parse(eventType.metadata);
const appSlug = credential.app?.slug;
if (appSlug) {
await prisma.eventType.update({
@@ -456,7 +462,9 @@ const handleDeleteCredential = async ({
const removeAppFromEventTypeMetadata = (
appSlugToDelete: string,
eventTypeMetadata: z.infer<typeof EventTypeMetaDataSchema>
eventTypeMetadata: {
apps: z.infer<typeof eventTypeAppMetadataOptionalSchema>;
}
) => {
const appMetadata = eventTypeMetadata?.apps
? Object.entries(eventTypeMetadata.apps).reduce((filteredApps, [appName, appData]) => {
+3 -1
View File
@@ -17,6 +17,7 @@ import { handlePaymentSuccess } from "@calcom/lib/payment/handlePaymentSuccess";
import { safeStringify } from "@calcom/lib/safeStringify";
import { prisma } from "@calcom/prisma";
import { BookingStatus } from "@calcom/prisma/enums";
import { eventTypeMetaDataSchemaWithTypedApps } from "@calcom/prisma/zod-utils";
const log = logger.getSubLogger({ prefix: ["[paymentWebhook]"] });
@@ -72,7 +73,8 @@ const handleSetupSuccess = async (event: Stripe.Event) => {
},
});
if (!requiresConfirmation) {
const eventManager = new EventManager(user, eventType?.metadata?.apps);
const metadata = eventTypeMetaDataSchemaWithTypedApps.parse(eventType?.metadata);
const eventManager = new EventManager(user, metadata?.apps);
const scheduleResult = await eventManager.create(evt);
bookingData.references = { create: scheduleResult.referencesToCreate };
bookingData.status = BookingStatus.ACCEPTED;
@@ -11,6 +11,7 @@ import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import type { baseEventTypeSelect } from "@calcom/prisma";
import { SchedulingType } from "@calcom/prisma/enums";
import type { EventTypeModel } from "@calcom/prisma/zod";
import { eventTypeMetaDataSchemaWithTypedApps } from "@calcom/prisma/zod-utils";
import { Badge } from "@calcom/ui";
export type EventTypeDescriptionProps = {
@@ -39,7 +40,10 @@ export const EventTypeDescription = ({
[eventType.recurringEvent]
);
const paymentAppData = getPaymentAppData(eventType);
const paymentAppData = getPaymentAppData({
...eventType,
metadata: eventTypeMetaDataSchemaWithTypedApps.parse(eventType.metadata),
});
return (
<>
@@ -1,5 +1,6 @@
import type { EventTypeSetupProps } from "@calcom/features/eventtypes/lib/types";
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
import { eventTypeMetaDataSchemaWithTypedApps } from "@calcom/prisma/zod-utils";
import InstantEventController from "./InstantEventController";
@@ -7,7 +8,10 @@ export const EventInstantTab = ({
eventType,
isTeamEvent,
}: Pick<EventTypeSetupProps, "eventType"> & { isTeamEvent: boolean }) => {
const paymentAppData = getPaymentAppData(eventType);
const paymentAppData = getPaymentAppData({
...eventType,
metadata: eventTypeMetaDataSchemaWithTypedApps.parse(eventType.metadata),
});
const requirePayment = paymentAppData.price > 0;
@@ -1,4 +1,5 @@
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
import { eventTypeMetaDataSchemaWithTypedApps } from "@calcom/prisma/zod-utils";
import type { RecurringEventControllerProps } from "./RecurringEventController";
import RecurringEventController from "./RecurringEventController";
@@ -6,7 +7,10 @@ import RecurringEventController from "./RecurringEventController";
export type EventRecurringTabProps = Omit<RecurringEventControllerProps, "paymentEnabled">;
export const EventRecurringTab = ({ eventType, customClassNames }: EventRecurringTabProps) => {
const paymentAppData = getPaymentAppData(eventType);
const paymentAppData = getPaymentAppData({
...eventType,
metadata: eventTypeMetaDataSchemaWithTypedApps.parse(eventType.metadata),
});
const requirePayment = paymentAppData.price > 0;
@@ -20,7 +20,7 @@ import type { Team } from "@calcom/prisma/client";
import type { BookerLayoutSettings } from "@calcom/prisma/zod-utils";
import {
BookerLayouts,
EventTypeMetaDataSchema,
eventTypeMetaDataSchemaWithTypedApps,
bookerLayoutOptions,
bookerLayouts as bookerLayoutsSchema,
customInputSchema,
@@ -368,7 +368,7 @@ export const getPublicEvent = async (
if (!event) return null;
const eventMetaData = EventTypeMetaDataSchema.parse(event.metadata || {});
const eventMetaData = eventTypeMetaDataSchemaWithTypedApps.parse(event.metadata || {});
const teamMetadata = teamMetadataSchema.parse(event.team?.metadata || {});
const usersAsHosts = event.hosts.map((host) => host.user);
@@ -517,7 +517,7 @@ function getProfileFromEvent(event: Event) {
const username = "username" in profile ? profile.username : team?.slug;
const weekStart = hosts?.[0]?.user?.weekStart || owner?.weekStart || "Monday";
const eventMetaData = EventTypeMetaDataSchema.parse(event.metadata || {});
const eventMetaData = eventTypeMetaDataSchemaWithTypedApps.parse(event.metadata || {});
const userMetaData = userMetadataSchema.parse(profile.metadata || {});
return {
+2 -2
View File
@@ -5,7 +5,7 @@ import type { ChildrenEventType } from "@calcom/features/eventtypes/components/C
import type { AttributesQueryValue } from "@calcom/lib/raqb/types";
import type { EventTypeTranslation } from "@calcom/prisma/client";
import type { PeriodType, SchedulingType } from "@calcom/prisma/enums";
import type { BookerLayoutSettings, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import type { BookerLayoutSettings, eventTypeMetaDataSchemaWithTypedApps } from "@calcom/prisma/zod-utils";
import type { customInputSchema } from "@calcom/prisma/zod-utils";
import type { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
import type { eventTypeColor } from "@calcom/prisma/zod-utils";
@@ -122,7 +122,7 @@ export type FormValues = {
beforeEventBuffer: number;
afterEventBuffer: number;
slotInterval: number | null;
metadata: z.infer<typeof EventTypeMetaDataSchema>;
metadata: z.infer<typeof eventTypeMetaDataSchemaWithTypedApps>;
destinationCalendar: {
integration: string;
externalId: string;
@@ -15,6 +15,7 @@ import { TRPCError } from "@trpc/server";
import { EventsInsights } from "./events";
import { RoutingEventsInsights } from "./routing-events";
import { VirtualQueuesInsights } from "./virtual-queues";
const UserBelongsToTeamInput = z.object({
teamId: z.coerce.number().optional().nullable(),
@@ -1796,4 +1797,15 @@ export const insightsRouter = router({
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
}
}),
getUserRelevantTeamRoutingForms: authedProcedure.query(async ({ ctx }) => {
try {
const routingForms = await VirtualQueuesInsights.getUserRelevantTeamRoutingForms({
userId: ctx.user.id,
});
return routingForms;
} catch (e) {
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
}
}),
});
@@ -0,0 +1,73 @@
import { readonlyPrisma as prisma } from "@calcom/prisma";
import { getSerializableForm } from "@calcom/routing-forms/lib/getSerializableForm";
class VirtualQueuesInsights {
static async getUserRelevantTeamRoutingForms({ userId }: { userId: number }) {
type JsonField = { [key: string]: any };
type RoutingFormType = {
id: string;
description: string | null;
position: number;
routes: JsonField | null;
createdAt: Date;
updatedAt: Date;
name: string;
fields: JsonField | null;
userId: number;
teamId: number | null;
disabled: boolean;
settings: JsonField | null;
};
const formsRedirectingToWeightedRR: RoutingFormType[] = await prisma.$queryRaw<RoutingFormType[]>`
WITH RECURSIVE json_array_elements_recursive AS (
SELECT f.id, f."teamId",
jsonb_array_elements(f.routes::jsonb) as route
FROM "App_RoutingForms_Form" f
WHERE f."teamId" IN (
SELECT "teamId"
FROM "Membership"
WHERE "userId" = ${userId}
)
)
SELECT DISTINCT f.*,
jsonb_build_object(
'parentId', t."parentId",
'parent', CASE WHEN p.id IS NOT NULL THEN jsonb_build_object('slug', p.slug) ELSE NULL END,
'metadata', t.metadata
) as team,
jsonb_build_object(
'id', u.id,
'username', u.username,
'movedToProfileId', u."movedToProfileId"
) as "user"
FROM "App_RoutingForms_Form" f
INNER JOIN json_array_elements_recursive r ON f.id = r.id
INNER JOIN "EventType" e ON (r.route->>'action')::jsonb->>'eventTypeId' = e.id::text
INNER JOIN "Team" t ON f."teamId" = t.id
LEFT JOIN "Team" p ON t."parentId" = p.id
INNER JOIN "users" u ON f."userId" = u.id
WHERE e."schedulingType" = 'roundRobin'
AND e."isRRWeightsEnabled" = true
AND EXISTS (
SELECT 1
FROM "Host" h
WHERE h."eventTypeId" = e.id
AND h."userId" = ${userId}
);
`;
// Convert the raw forms to serializable format
const serializableForms = await Promise.all(
formsRedirectingToWeightedRR.map(async (form) => {
const serializedForm = await getSerializableForm({ form });
return serializedForm;
})
);
return serializableForms;
}
}
export { VirtualQueuesInsights };
@@ -4,7 +4,7 @@ import type { eventTypeAppCardZod } from "@calcom/app-store/eventTypeAppCardZod"
import CrmManager from "@calcom/core/crmManager/crmManager";
import logger from "@calcom/lib/logger";
import prisma from "@calcom/prisma";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { eventTypeAppMetadataOptionalSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import type { NoShowAttendees } from "../handleMarkNoShow";
import { noShowEnabledApps } from "./noShowEnabledApps";
@@ -38,7 +38,7 @@ export default async function handleSendingAttendeeNoShowDataToApps(
log.error(`Malformed event type metadata for bookingUid ${bookingUid}`);
return;
}
const eventTypeAppMetadata = eventTypeMetadataParse.data?.apps;
const eventTypeAppMetadata = eventTypeAppMetadataOptionalSchema.parse(eventTypeMetadataParse.data?.apps);
for (const slug in eventTypeAppMetadata) {
if (noShowEnabledApps.includes(slug)) {
@@ -116,6 +116,11 @@ const getNavigationItems = (orgBranding: OrganizationBranding): NavigationItemTy
href: "/insights/routing",
isCurrent: ({ pathname: path }) => path?.startsWith("/insights/routing") ?? false,
},
{
name: "router_position",
href: "/insights/virtual-queues",
isCurrent: ({ pathname: path }) => path?.startsWith("/insights/virtual-queues") ?? false,
},
],
},
];
+2 -1
View File
@@ -5,6 +5,7 @@ import slugify from "@calcom/lib/slugify";
import { PeriodType, SchedulingType } from "@calcom/prisma/enums";
import type { userSelect } from "@calcom/prisma/selects";
import type { CustomInputSchema } from "@calcom/prisma/zod-utils";
import { eventTypeMetaDataSchemaWithTypedApps } from "@calcom/prisma/zod-utils";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import type { CredentialPayload } from "@calcom/types/Credential";
@@ -132,7 +133,7 @@ export const dynamicEvent = {
descriptionAsSafeHTML: "",
position: 0,
...commons,
metadata: EventTypeMetaDataSchema.parse({ multipleDuration: [15, 30, 45, 60, 90] }),
metadata: eventTypeMetaDataSchemaWithTypedApps.parse({ multipleDuration: [15, 30, 45, 60, 90] }),
};
export const defaultEvents = [dynamicEvent];
+2 -2
View File
@@ -11,7 +11,7 @@ import { EventTypeRepository } from "@calcom/lib/server/repository/eventType";
import { UserRepository } from "@calcom/lib/server/repository/user";
import type { PrismaClient } from "@calcom/prisma";
import { SchedulingType, MembershipRole } from "@calcom/prisma/enums";
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { customInputSchema, eventTypeMetaDataSchemaWithTypedApps } from "@calcom/prisma/zod-utils";
import { TRPCError } from "@trpc/server";
@@ -58,7 +58,7 @@ export const getEventTypeById = async ({
}
const { locations, metadata, ...restEventType } = rawEventType;
const newMetadata = EventTypeMetaDataSchema.parse(metadata || {}) || {};
const newMetadata = eventTypeMetaDataSchemaWithTypedApps.parse(metadata || {}) || {};
const apps = newMetadata?.apps || {};
const eventTypeWithParsedMetadata = { ...rawEventType, metadata: newMetadata };
const eventTeamMembershipsWithUserProfile = [];
@@ -15,7 +15,7 @@ import { ProfileRepository } from "@calcom/lib/server/repository/profile";
import { UserRepository } from "@calcom/lib/server/repository/user";
import { MembershipRole, SchedulingType } from "@calcom/prisma/enums";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { eventTypeMetaDataSchemaWithoutApps } from "@calcom/prisma/zod-utils";
import { TRPCError } from "@trpc/server";
@@ -116,7 +116,7 @@ export const getEventTypesByViewer = async (user: User, filters?: Filters, forRo
})
)
),
metadata: eventType.metadata ? EventTypeMetaDataSchema.parse(eventType.metadata) : null,
metadata: eventType.metadata ? eventTypeMetaDataSchemaWithoutApps.parse(eventType.metadata) : null,
children: await Promise.all(
(eventType.children || []).map(async (c) => ({
...c,
+10 -2
View File
@@ -5,12 +5,20 @@ import type { appDataSchemas } from "@calcom/app-store/apps.schemas.generated";
import type { appDataSchema, paymentOptionEnum } from "@calcom/app-store/stripepayment/zod";
import type { EventTypeAppsList } from "@calcom/app-store/utils";
import type { BookerEvent } from "@calcom/features/bookings/types";
import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { eventTypeMetaDataSchemaWithTypedApps } from "@calcom/prisma/zod-utils";
export default function getPaymentAppData(
eventType: Pick<BookerEvent, "price" | "currency" | "metadata">,
_eventType: Pick<BookerEvent, "price" | "currency"> & {
metadata: z.infer<typeof EventTypeMetaDataSchema>;
},
forcedGet?: boolean
) {
const metadataApps = eventType?.metadata?.apps as unknown as EventTypeAppsList;
const eventType = {
..._eventType,
metadata: eventTypeMetaDataSchemaWithTypedApps.parse(_eventType.metadata),
};
const metadataApps = eventType.metadata?.apps;
if (!metadataApps) {
return { enabled: false, price: 0, currency: "usd", appId: null };
}
+7 -6
View File
@@ -3,6 +3,7 @@ import type { AppCategories, Prisma } from "@prisma/client";
import appStore from "@calcom/app-store";
import type { EventTypeAppsList } from "@calcom/app-store/utils";
import type { CompleteEventType } from "@calcom/prisma/zod";
import { eventTypeAppMetadataOptionalSchema } from "@calcom/prisma/zod-utils";
import type { CalendarEvent } from "@calcom/types/Calendar";
import type { IAbstractPaymentService, PaymentApp } from "@calcom/types/PaymentService";
@@ -40,15 +41,15 @@ const handlePayment = async (
const paymentInstance = new PaymentService(paymentAppCredentials) as IAbstractPaymentService;
const paymentOption =
selectedEventType?.metadata?.apps?.[paymentAppCredentials.appId].paymentOption || "ON_BOOKING";
const apps = eventTypeAppMetadataOptionalSchema.parse(selectedEventType?.metadata?.apps);
const paymentOption = apps?.[paymentAppCredentials.appId].paymentOption || "ON_BOOKING";
let paymentData;
if (paymentOption === "HOLD") {
paymentData = await paymentInstance.collectCard(
{
amount: selectedEventType?.metadata?.apps?.[paymentAppCredentials.appId].price,
currency: selectedEventType?.metadata?.apps?.[paymentAppCredentials.appId].currency,
amount: apps?.[paymentAppCredentials.appId].price,
currency: apps?.[paymentAppCredentials.appId].currency,
},
booking.id,
paymentOption,
@@ -58,8 +59,8 @@ const handlePayment = async (
} else {
paymentData = await paymentInstance.create(
{
amount: selectedEventType?.metadata?.apps?.[paymentAppCredentials.appId].price,
currency: selectedEventType?.metadata?.apps?.[paymentAppCredentials.appId].currency,
amount: apps?.[paymentAppCredentials.appId].price,
currency: apps?.[paymentAppCredentials.appId].currency,
},
booking.id,
booking.userId,
+3 -1
View File
@@ -9,6 +9,7 @@ import { HttpError as HttpCode } from "@calcom/lib/http-error";
import { getBooking } from "@calcom/lib/payment/getBooking";
import prisma from "@calcom/prisma";
import { BookingStatus } from "@calcom/prisma/enums";
import { eventTypeAppMetadataOptionalSchema } from "@calcom/prisma/zod-utils";
import logger from "../logger";
@@ -26,7 +27,8 @@ export async function handlePaymentSuccess(paymentId: number, bookingId: number)
const isConfirmed = booking.status === BookingStatus.ACCEPTED;
if (isConfirmed) {
const eventManager = new EventManager(userWithCredentials, eventType?.metadata?.apps);
const apps = eventTypeAppMetadataOptionalSchema.parse(eventType?.metadata?.apps);
const eventManager = new EventManager(userWithCredentials, apps);
const scheduleResult = await eventManager.create(evt);
bookingData.references = { create: scheduleResult.referencesToCreate };
}
+3 -1
View File
@@ -89,7 +89,9 @@ export function getPiiFreeDestinationCalendar(destinationCalendar: Partial<Desti
};
}
export function getPiiFreeEventType(eventType: Partial<Omit<EventType, "recurringEvent">>) {
export function getPiiFreeEventType(
eventType: Partial<Pick<EventType, "id" | "schedulingType" | "seatsPerTimeSlot">>
) {
return {
id: eventType.id,
schedulingType: eventType.schedulingType,
+3 -3
View File
@@ -2,7 +2,7 @@ import type { Prisma } from "@prisma/client";
import type { appDataSchemas } from "@calcom/app-store/apps.schemas.generated";
import { prisma } from "@calcom/prisma";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { EventTypeMetaDataSchema, EventTypeAppMetadataSchema } from "@calcom/prisma/zod-utils";
export class EventTypeService {
static async getEventTypeAppDataFromId(eventTypeId: number, appSlug: keyof typeof appDataSchemas) {
@@ -26,8 +26,8 @@ export class EventTypeService {
if (!parseEventTypeAppMetadata.success || !parseEventTypeAppMetadata.data?.apps) return null;
const eventTypeAppMetadata = parseEventTypeAppMetadata.data.apps;
const appMetadata = eventTypeAppMetadata[appSlug];
const apps = EventTypeAppMetadataSchema.parse(eventTypeAppMetadata);
const appMetadata = apps[appSlug];
return appMetadata;
}
@@ -20,7 +20,6 @@ import { validateIntervalLimitOrder } from "@calcom/lib";
import { locationsResolver } from "@calcom/lib/event-types/utils/locationsResolver";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { validateBookerLayouts } from "@calcom/lib/validateBookerLayouts";
import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { eventTypeBookingFields as eventTypeBookingFieldsSchema } from "@calcom/prisma/zod-utils";
type Fields = z.infer<typeof eventTypeBookingFieldsSchema>;
@@ -325,8 +324,7 @@ export const useEventTypeForm = ({
// Prevent two payment apps to be enabled
// Ok to cast type here because this metadata will be updated as the event type metadata
if (checkForMultiplePaymentApps(metadata as z.infer<typeof EventTypeMetaDataSchema>))
throw new Error(t("event_setup_multiple_payment_apps_error"));
if (checkForMultiplePaymentApps(metadata)) throw new Error(t("event_setup_multiple_payment_apps_error"));
if (metadata?.apps?.stripe?.paymentOption === "HOLD" && seatsPerTimeSlot) {
throw new Error(t("seats_and_no_show_fee_error"));
@@ -15,6 +15,7 @@ import type {
} from "@calcom/features/eventtypes/lib/types";
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { eventTypeMetaDataSchemaWithTypedApps } from "@calcom/prisma/zod-utils";
import type { VerticalTabItemProps } from "@calcom/ui";
type Props = {
@@ -55,7 +56,10 @@ export const useTabsNavigations = ({
eventTypeApps?.items.find((app) => app.slug === appId)?.isInstalled && appData.enabled
).length;
}
const paymentAppData = getPaymentAppData(eventType);
const paymentAppData = getPaymentAppData({
...eventType,
metadata: eventTypeMetaDataSchemaWithTypedApps.parse(eventType.metadata),
});
const requirePayment = paymentAppData.price > 0;
+56 -42
View File
@@ -71,50 +71,64 @@ export type BookerLayoutSettings = z.infer<typeof bookerLayouts>;
export const RequiresConfirmationThresholdUnits: z.ZodType<UnitTypeLongPlural> = z.enum(["hours", "minutes"]);
export const EventTypeAppMetadataSchema = z.object(appDataSchemas).partial();
export const eventTypeAppMetadataOptionalSchema = EventTypeAppMetadataSchema.optional();
export const EventTypeMetaDataSchema = z
.object({
smartContractAddress: z.string().optional(),
blockchainId: z.number().optional(),
multipleDuration: z.number().array().optional(),
giphyThankYouPage: z.string().optional(),
apps: EventTypeAppMetadataSchema.optional(),
additionalNotesRequired: z.boolean().optional(),
disableSuccessPage: z.boolean().optional(),
disableStandardEmails: z
.object({
all: z
.object({
host: z.boolean().optional(),
attendee: z.boolean().optional(),
})
.optional(),
confirmation: z
.object({
host: z.boolean().optional(),
attendee: z.boolean().optional(),
})
.optional(),
})
.optional(),
managedEventConfig: z
.object({
unlockedFields: z.custom<{ [k in keyof Omit<Prisma.EventTypeSelect, "id">]: true }>().optional(),
})
.optional(),
requiresConfirmationThreshold: z
.object({
time: z.number(),
unit: RequiresConfirmationThresholdUnits,
})
.optional(),
config: z
.object({
useHostSchedulesForTeamEvent: z.boolean().optional(),
})
.optional(),
bookerLayouts: bookerLayouts.optional(),
const _eventTypeMetaDataSchemaWithoutApps = z.object({
smartContractAddress: z.string().optional(),
blockchainId: z.number().optional(),
multipleDuration: z.number().array().optional(),
giphyThankYouPage: z.string().optional(),
additionalNotesRequired: z.boolean().optional(),
disableSuccessPage: z.boolean().optional(),
disableStandardEmails: z
.object({
all: z
.object({
host: z.boolean().optional(),
attendee: z.boolean().optional(),
})
.optional(),
confirmation: z
.object({
host: z.boolean().optional(),
attendee: z.boolean().optional(),
})
.optional(),
})
.optional(),
managedEventConfig: z
.object({
unlockedFields: z.custom<{ [k in keyof Omit<Prisma.EventTypeSelect, "id">]: true }>().optional(),
})
.optional(),
requiresConfirmationThreshold: z
.object({
time: z.number(),
unit: RequiresConfirmationThresholdUnits,
})
.optional(),
config: z
.object({
useHostSchedulesForTeamEvent: z.boolean().optional(),
})
.optional(),
bookerLayouts: bookerLayouts.optional(),
});
export const eventTypeMetaDataSchemaWithUntypedApps = _eventTypeMetaDataSchemaWithoutApps.merge(
z.object({
apps: z.unknown().optional(),
})
);
export const EventTypeMetaDataSchema = eventTypeMetaDataSchemaWithUntypedApps.nullable();
export const eventTypeMetaDataSchemaWithoutApps = _eventTypeMetaDataSchemaWithoutApps.nullable();
export const eventTypeMetaDataSchemaWithTypedApps = _eventTypeMetaDataSchemaWithoutApps
.merge(
z.object({
apps: eventTypeAppMetadataOptionalSchema,
})
)
.nullable();
export type EventTypeMetadata = z.infer<typeof EventTypeMetaDataSchema>;
@@ -27,6 +27,7 @@ import {
handleCustomInputs,
handlePeriodType,
} from "./util";
import { eventTypeAppMetadataOptionalSchema } from "@calcom/prisma/zod-utils";
type SessionUser = NonNullable<TrpcSessionUser>;
type User = {
@@ -382,8 +383,9 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
}
}
for (const appKey in input.metadata?.apps) {
const app = input.metadata?.apps[appKey as keyof typeof appDataSchemas];
const apps = eventTypeAppMetadataOptionalSchema.parse(input.metadata?.apps);
for (const appKey in apps) {
const app = apps[appKey as keyof typeof appDataSchemas];
// There should only be one enabled payment app in the metadata
if (app.enabled && app.price && app.currency) {
data.price = app.price;
@@ -1,4 +1,5 @@
import { isTeamMember } from "@calcom/lib/server/queries";
import { eventTypeMetaDataSchemaWithTypedApps } from "@calcom/prisma/zod-utils";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import { TRPCError } from "@trpc/server";
@@ -40,7 +41,13 @@ export const getAllActiveWorkflowsHandler = async ({ input, ctx }: GetAllActiveW
if (!team) throw new TRPCError({ code: "UNAUTHORIZED" });
}
const allActiveWorkflows = await getAllWorkflowsFromEventType(completeEventType, eventType.userId);
const allActiveWorkflows = await getAllWorkflowsFromEventType(
{
...completeEventType,
metadata: eventTypeMetaDataSchemaWithTypedApps.parse(eventType.metadata),
},
eventType.userId
);
return allActiveWorkflows;
};