test: findTeamMemberEmailFromCrm (#17685)
* We need to choose contact owner when crm app slug is not present * Add unit tests for getTeamMemberEmailFromCrm module
This commit is contained in:
1 parent
22da681f8c
commit
384deab0ce
4 files changed
+431
-27
No files matched your search
@@ -0,0 +1,410 @@
|
||||
import prismock from "../../../../tests/libs/__mocks__/prisma";
|
||||
|
||||
import { v4 } from "uuid";
|
||||
import { describe, expect, it, vi, beforeEach } from "vitest";
|
||||
|
||||
import { getCRMContactOwnerForRRLeadSkip } from "@calcom/app-store/_utils/CRMRoundRobinSkip";
|
||||
import bookingFormHandlers from "@calcom/app-store/routing-forms/appBookingFormHandler";
|
||||
import { ROUTING_FORM_RESPONSE_ID_QUERY_STRING } from "@calcom/app-store/routing-forms/lib/constants";
|
||||
import { RouteActionType } from "@calcom/app-store/routing-forms/zod";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
|
||||
import { getTeamMemberEmailForResponseOrContactUsingUrlQuery } from "../getTeamMemberEmailFromCrm";
|
||||
|
||||
vi.mock("@calcom/app-store/routing-forms/appBookingFormHandler", () => ({
|
||||
default: {
|
||||
salesforce: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@calcom/app-store/_utils/CRMRoundRobinSkip", () => ({
|
||||
getCRMContactOwnerForRRLeadSkip: vi.fn(),
|
||||
}));
|
||||
|
||||
function mockGetCRMContactOwnerForRRLeadSkip({
|
||||
bookerEmail,
|
||||
teamMemberEmail,
|
||||
}: {
|
||||
bookerEmail: string;
|
||||
teamMemberEmail: string;
|
||||
}) {
|
||||
vi.mocked(getCRMContactOwnerForRRLeadSkip).mockImplementation((_bookerEmail, _eventMetadata) => {
|
||||
if (_bookerEmail === bookerEmail) {
|
||||
return Promise.resolve(teamMemberEmail);
|
||||
}
|
||||
return Promise.resolve(undefined);
|
||||
});
|
||||
}
|
||||
|
||||
function mockBookingFormHandler({
|
||||
bookerEmail,
|
||||
teamMemberEmail,
|
||||
}: {
|
||||
bookerEmail: string;
|
||||
teamMemberEmail: string;
|
||||
}) {
|
||||
vi.mocked(bookingFormHandlers.salesforce).mockImplementation(
|
||||
(_bookerEmail, _attributeRoutingConfig, _eventTypeId) => {
|
||||
if (_bookerEmail === bookerEmail) {
|
||||
return Promise.resolve({ email: teamMemberEmail });
|
||||
}
|
||||
return Promise.resolve({ email: null });
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function createRoutingFormWithResponse({
|
||||
formData,
|
||||
}: {
|
||||
formData: {
|
||||
routes: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
queryValue?: any;
|
||||
action: {
|
||||
eventTypeId: number;
|
||||
type: RouteActionType;
|
||||
value: string;
|
||||
};
|
||||
attributeRoutingConfig: {
|
||||
skipContactOwner: boolean;
|
||||
};
|
||||
}[];
|
||||
};
|
||||
}) {
|
||||
const user = await prismock.user.create({
|
||||
data: {
|
||||
name: "Routing Form User",
|
||||
username: "routing-form-user",
|
||||
email: "routing-form-user@example.com",
|
||||
},
|
||||
});
|
||||
|
||||
const form = await prismock.app_RoutingForms_Form.create({
|
||||
data: {
|
||||
...formData,
|
||||
name: "Test Form",
|
||||
routes: formData.routes.map((route) => ({
|
||||
queryValue: {
|
||||
id: v4(),
|
||||
type: "group",
|
||||
children1: [],
|
||||
properties: {},
|
||||
},
|
||||
id: v4(),
|
||||
...route,
|
||||
})),
|
||||
user: {
|
||||
connect: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const responseRecord = await prismock.app_RoutingForms_FormResponse.create({
|
||||
data: {
|
||||
response: {},
|
||||
form: {
|
||||
connect: {
|
||||
id: form.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return { form, responseRecord };
|
||||
}
|
||||
|
||||
async function createHostForEvent({
|
||||
eventTypeData,
|
||||
userData,
|
||||
}: {
|
||||
eventTypeData: { id: number } & Partial<{
|
||||
title: string;
|
||||
slug: string;
|
||||
length: number;
|
||||
}>;
|
||||
userData: {
|
||||
email: string;
|
||||
} & Partial<{
|
||||
name: string;
|
||||
username: string;
|
||||
}>;
|
||||
}) {
|
||||
const user = await prismock.user.create({
|
||||
data: {
|
||||
name: "Test User",
|
||||
username: "testuser",
|
||||
...userData,
|
||||
},
|
||||
});
|
||||
await prismock.eventType.create({
|
||||
data: {
|
||||
title: "Title",
|
||||
slug: "slug",
|
||||
length: 30,
|
||||
...eventTypeData,
|
||||
hosts: {
|
||||
create: {
|
||||
userId: user.id,
|
||||
isFixed: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
describe("getTeamMemberEmailForResponseOrContactUsingUrlQuery", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
const mockEventData = {
|
||||
id: 1,
|
||||
isInstantEvent: false,
|
||||
schedulingType: SchedulingType.ROUND_ROBIN,
|
||||
metadata: null,
|
||||
length: 30,
|
||||
};
|
||||
|
||||
it("should return null when email is not provided in query", async () => {
|
||||
const result = await getTeamMemberEmailForResponseOrContactUsingUrlQuery({
|
||||
query: {},
|
||||
eventData: mockEventData,
|
||||
});
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should return null when scheduling type is not ROUND_ROBIN", async () => {
|
||||
const bookerEmail = "booker@example.com";
|
||||
const teamMemberEmail = "teamMember@example.com";
|
||||
mockGetCRMContactOwnerForRRLeadSkip({ bookerEmail, teamMemberEmail });
|
||||
await createHostForEvent({
|
||||
eventTypeData: {
|
||||
id: mockEventData.id,
|
||||
},
|
||||
userData: {
|
||||
email: teamMemberEmail,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await getTeamMemberEmailForResponseOrContactUsingUrlQuery({
|
||||
query: { email: bookerEmail },
|
||||
eventData: { ...mockEventData, schedulingType: SchedulingType.COLLECTIVE },
|
||||
});
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should return CRM owner email when valid", async () => {
|
||||
const ownerEmail = "owner@example.com";
|
||||
const bookerEmail = "booker@example.com";
|
||||
mockGetCRMContactOwnerForRRLeadSkip({ bookerEmail, teamMemberEmail: ownerEmail });
|
||||
await createHostForEvent({
|
||||
eventTypeData: {
|
||||
id: mockEventData.id,
|
||||
},
|
||||
userData: {
|
||||
email: ownerEmail,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await getTeamMemberEmailForResponseOrContactUsingUrlQuery({
|
||||
query: { email: "booker@example.com" },
|
||||
eventData: mockEventData,
|
||||
});
|
||||
|
||||
expect(result).toBe(ownerEmail);
|
||||
});
|
||||
|
||||
it("should return null when CRM owner is not found", async () => {
|
||||
const bookerEmail = "booker@example.com";
|
||||
const teamMemberEmail = "teamMember@example.com";
|
||||
mockGetCRMContactOwnerForRRLeadSkip({ bookerEmail, teamMemberEmail });
|
||||
|
||||
const result = await getTeamMemberEmailForResponseOrContactUsingUrlQuery({
|
||||
query: { email: bookerEmail },
|
||||
eventData: mockEventData,
|
||||
});
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should return null when CRM owner is not part of event type", async () => {
|
||||
const ownerEmail = "owner@example.com";
|
||||
const bookerEmail = "booker@example.com";
|
||||
mockGetCRMContactOwnerForRRLeadSkip({ bookerEmail, teamMemberEmail: ownerEmail });
|
||||
|
||||
const result = await getTeamMemberEmailForResponseOrContactUsingUrlQuery({
|
||||
query: { email: bookerEmail },
|
||||
eventData: mockEventData,
|
||||
});
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
describe("Booking form handler", () => {
|
||||
it("should return teamMember email through booking form handler when cal.routingFormResponseId and cal.salesforce.xxxx=true is provided", async () => {
|
||||
const bookerEmail = "booker@example.com";
|
||||
const teamMemberEmail = "owner@example.com";
|
||||
mockBookingFormHandler({ bookerEmail, teamMemberEmail });
|
||||
await createHostForEvent({
|
||||
eventTypeData: {
|
||||
id: mockEventData.id,
|
||||
},
|
||||
userData: {
|
||||
email: teamMemberEmail,
|
||||
},
|
||||
});
|
||||
|
||||
const { responseRecord } = await createRoutingFormWithResponse({
|
||||
formData: {
|
||||
routes: [
|
||||
{
|
||||
action: {
|
||||
eventTypeId: 1,
|
||||
type: RouteActionType.EventTypeRedirectUrl,
|
||||
value: "/team/sales",
|
||||
},
|
||||
attributeRoutingConfig: { skipContactOwner: false },
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const result = await getTeamMemberEmailForResponseOrContactUsingUrlQuery({
|
||||
query: {
|
||||
email: bookerEmail,
|
||||
[ROUTING_FORM_RESPONSE_ID_QUERY_STRING]: responseRecord.id.toString(),
|
||||
"cal.salesforce.xxx": "true",
|
||||
},
|
||||
eventData: mockEventData,
|
||||
});
|
||||
|
||||
expect(result).toBe(teamMemberEmail);
|
||||
});
|
||||
|
||||
it("should return null when skipContactOwner is true even when cal.routingFormResponseId and cal.salesforce.xxxx=true is provided", async () => {
|
||||
const bookerEmail = "booker@example.com";
|
||||
const teamMemberEmail = "owner@example.com";
|
||||
mockBookingFormHandler({ bookerEmail, teamMemberEmail });
|
||||
await createHostForEvent({
|
||||
eventTypeData: {
|
||||
id: mockEventData.id,
|
||||
},
|
||||
userData: {
|
||||
email: teamMemberEmail,
|
||||
},
|
||||
});
|
||||
|
||||
const { responseRecord } = await createRoutingFormWithResponse({
|
||||
formData: {
|
||||
routes: [
|
||||
{
|
||||
action: {
|
||||
eventTypeId: 1,
|
||||
type: RouteActionType.EventTypeRedirectUrl,
|
||||
value: "/team/sales",
|
||||
},
|
||||
attributeRoutingConfig: { skipContactOwner: true },
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const result = await getTeamMemberEmailForResponseOrContactUsingUrlQuery({
|
||||
query: {
|
||||
email: bookerEmail,
|
||||
[ROUTING_FORM_RESPONSE_ID_QUERY_STRING]: responseRecord.id.toString(),
|
||||
"cal.salesforce.xxx": "true",
|
||||
},
|
||||
eventData: mockEventData,
|
||||
});
|
||||
|
||||
// Because skipContactOwner is true, the booking form handler should return null
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
|
||||
it("should return null when when cal.routingFormResponseId and cal.salesforce.xxxx=true is provided but the returned email isn't an event member", async () => {
|
||||
const bookerEmail = "booker@example.com";
|
||||
const teamMemberEmail = "owner@example.com";
|
||||
mockBookingFormHandler({ bookerEmail, teamMemberEmail });
|
||||
const SOME_OTHER_EVENT_ID = 200;
|
||||
await createHostForEvent({
|
||||
eventTypeData: {
|
||||
id: SOME_OTHER_EVENT_ID,
|
||||
},
|
||||
userData: {
|
||||
email: teamMemberEmail,
|
||||
},
|
||||
});
|
||||
|
||||
const { responseRecord } = await createRoutingFormWithResponse({
|
||||
formData: {
|
||||
routes: [
|
||||
{
|
||||
action: {
|
||||
eventTypeId: 1,
|
||||
type: RouteActionType.EventTypeRedirectUrl,
|
||||
value: "/team/sales",
|
||||
},
|
||||
attributeRoutingConfig: { skipContactOwner: false },
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const result = await getTeamMemberEmailForResponseOrContactUsingUrlQuery({
|
||||
query: {
|
||||
email: bookerEmail,
|
||||
[ROUTING_FORM_RESPONSE_ID_QUERY_STRING]: responseRecord.id.toString(),
|
||||
"cal.salesforce.xxx": "true",
|
||||
},
|
||||
eventData: mockEventData,
|
||||
});
|
||||
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
|
||||
it("should return null when cal.routingFormResponseId is provided but cal.salesforce.xxxx is not", async () => {
|
||||
const bookerEmail = "booker@example.com";
|
||||
const teamMemberEmail = "owner@example.com";
|
||||
mockBookingFormHandler({ bookerEmail, teamMemberEmail });
|
||||
await createHostForEvent({
|
||||
eventTypeData: {
|
||||
id: mockEventData.id,
|
||||
},
|
||||
userData: {
|
||||
email: teamMemberEmail,
|
||||
},
|
||||
});
|
||||
|
||||
const { responseRecord } = await createRoutingFormWithResponse({
|
||||
formData: {
|
||||
routes: [
|
||||
{
|
||||
action: {
|
||||
eventTypeId: 1,
|
||||
type: RouteActionType.EventTypeRedirectUrl,
|
||||
value: "/team/sales",
|
||||
},
|
||||
attributeRoutingConfig: { skipContactOwner: false },
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const result = await getTeamMemberEmailForResponseOrContactUsingUrlQuery({
|
||||
query: {
|
||||
email: bookerEmail,
|
||||
[ROUTING_FORM_RESPONSE_ID_QUERY_STRING]: responseRecord.id.toString(),
|
||||
"cal.somethingelse.xxx": "true",
|
||||
},
|
||||
eventData: mockEventData,
|
||||
});
|
||||
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -21,6 +21,21 @@ interface EventData {
|
||||
length: number;
|
||||
}
|
||||
|
||||
async function findUserByEmailWhoIsAHostOfEventType({
|
||||
email,
|
||||
eventTypeId,
|
||||
}: {
|
||||
email: string;
|
||||
eventTypeId: number;
|
||||
}) {
|
||||
return prisma.user.findFirst({
|
||||
where: {
|
||||
email,
|
||||
hosts: { some: { eventTypeId } },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function getRoutingFormResponseIdFromQuery(query: ParsedUrlQuery) {
|
||||
const routingFormResponseIdAsNumber = Number(query[ROUTING_FORM_RESPONSE_ID_QUERY_STRING]);
|
||||
const routingFormResponseId = isNaN(routingFormResponseIdAsNumber) ? null : routingFormResponseIdAsNumber;
|
||||
@@ -99,16 +114,11 @@ function getEnabledRoutingFormAppSlugFromQuery(query: ParsedUrlQuery) {
|
||||
async function getOwnerEmailFromCrm(eventData: EventData, email: string): Promise<string | null> {
|
||||
const crmContactOwnerEmail = await getCRMContactOwnerForRRLeadSkip(email, eventData.metadata);
|
||||
if (!crmContactOwnerEmail) return null;
|
||||
|
||||
// Determine if the contactOwner is a part of the event type
|
||||
const contactOwnerQuery = await prisma.user.findFirst({
|
||||
where: {
|
||||
email: crmContactOwnerEmail,
|
||||
hosts: {
|
||||
some: {
|
||||
eventTypeId: eventData.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
const contactOwnerQuery = await findUserByEmailWhoIsAHostOfEventType({
|
||||
email: crmContactOwnerEmail,
|
||||
eventTypeId: eventData.id,
|
||||
});
|
||||
if (!contactOwnerQuery) return null;
|
||||
return crmContactOwnerEmail;
|
||||
@@ -138,7 +148,6 @@ async function getTeamMemberEmailUsingRoutingFormHandler({
|
||||
const appBookingFormHandler = (await import("@calcom/app-store/routing-forms/appBookingFormHandler"))
|
||||
.default;
|
||||
const appHandler = appBookingFormHandler[crmAppSlug];
|
||||
|
||||
if (!appHandler) return nullReturnValue;
|
||||
|
||||
const { email: userEmail } = await appHandler(bookerEmail, attributeRoutingConfig, eventTypeId);
|
||||
@@ -146,16 +155,7 @@ async function getTeamMemberEmailUsingRoutingFormHandler({
|
||||
if (!userEmail) return nullReturnValue;
|
||||
|
||||
// Determine if the user is a part of the event type
|
||||
const userQuery = await prisma.user.findFirst({
|
||||
where: {
|
||||
email: userEmail,
|
||||
hosts: {
|
||||
some: {
|
||||
eventTypeId: eventTypeId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const userQuery = await findUserByEmailWhoIsAHostOfEventType({ email: userEmail, eventTypeId });
|
||||
|
||||
if (!userQuery) return nullReturnValue;
|
||||
|
||||
@@ -164,14 +164,12 @@ async function getTeamMemberEmailUsingRoutingFormHandler({
|
||||
|
||||
async function getTeamMemberEmailForResponseOrContact({
|
||||
bookerEmail,
|
||||
eventTypeId,
|
||||
eventData,
|
||||
routingFormResponseId,
|
||||
chosenRoute,
|
||||
crmAppSlug,
|
||||
}: {
|
||||
bookerEmail: string;
|
||||
eventTypeId: number;
|
||||
eventData: EventData;
|
||||
routingFormResponseId?: number | null;
|
||||
/**
|
||||
@@ -180,6 +178,7 @@ async function getTeamMemberEmailForResponseOrContact({
|
||||
chosenRoute?: LocalRoute;
|
||||
crmAppSlug?: string;
|
||||
}) {
|
||||
const eventTypeId = eventData.id;
|
||||
if (eventData.schedulingType !== SchedulingType.ROUND_ROBIN) return null;
|
||||
|
||||
const attributeRoutingConfigGetterData = routingFormResponseId
|
||||
@@ -214,12 +213,10 @@ async function getTeamMemberEmailForResponseOrContact({
|
||||
|
||||
export async function getTeamMemberEmailForResponseOrContactUsingUrlQuery({
|
||||
query,
|
||||
eventTypeId,
|
||||
eventData,
|
||||
chosenRoute,
|
||||
}: {
|
||||
query: ParsedUrlQuery;
|
||||
eventTypeId: number;
|
||||
eventData: EventData;
|
||||
chosenRoute?: LocalRoute;
|
||||
}) {
|
||||
@@ -236,7 +233,6 @@ export async function getTeamMemberEmailForResponseOrContactUsingUrlQuery({
|
||||
|
||||
return await getTeamMemberEmailForResponseOrContact({
|
||||
bookerEmail: query.email,
|
||||
eventTypeId,
|
||||
eventData,
|
||||
routingFormResponseId,
|
||||
chosenRoute,
|
||||
|
||||
@@ -114,7 +114,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
orgBannerUrl: team.parent?.bannerUrl ?? "",
|
||||
teamMemberEmail: await getTeamMemberEmailForResponseOrContactUsingUrlQuery({
|
||||
query,
|
||||
eventTypeId,
|
||||
eventData,
|
||||
}),
|
||||
},
|
||||
|
||||
-1
@@ -214,7 +214,6 @@ export const findTeamMembersMatchingAttributeLogicHandler = async ({
|
||||
const timeBeforeCrm = performance.now();
|
||||
const contactOwnerEmail = await getTeamMemberEmailForResponseOrContactUsingUrlQuery({
|
||||
query: fromEntriesWithDuplicateKeys(urlSearchParamsToForward.entries()),
|
||||
eventTypeId: eventType.id,
|
||||
eventData: eventType,
|
||||
chosenRoute: route,
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user