first commit
This commit is contained in:
14
calcom/packages/app-store/closecom/DESCRIPTION.md
Normal file
14
calcom/packages/app-store/closecom/DESCRIPTION.md
Normal file
@ -0,0 +1,14 @@
|
||||
---
|
||||
items:
|
||||
- 1.jpg
|
||||
- 2.jpg
|
||||
- 3.jpg
|
||||
- 4.jpg
|
||||
- 5.jpg
|
||||
---
|
||||
|
||||
- Close is a modern CRM with build-in sales communication tools for email, phone, SMS, and meetings.
|
||||
- Automate the sales process by enrolling leads and contacts into sequences.
|
||||
- Track the entire sales process and performance with powerful activity and funnel reporting
|
||||
|
||||
Close.com subscriptions range from $29 - $149 per month. <a href="https://www.close.com/pricing" target="_blank">See pricing page</a>
|
13
calcom/packages/app-store/closecom/api/_getAdd.ts
Normal file
13
calcom/packages/app-store/closecom/api/_getAdd.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import checkSession from "../../_utils/auth";
|
||||
import { checkInstalled } from "../../_utils/installation";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const session = checkSession(req);
|
||||
await checkInstalled("closecom", session.user?.id);
|
||||
|
||||
const returnTo = req.query.returnTo;
|
||||
|
||||
return res.status(200).json({ url: `/apps/closecom/setup${returnTo ? `?returnTo=${returnTo}` : ""}` });
|
||||
}
|
45
calcom/packages/app-store/closecom/api/_postAdd.ts
Normal file
45
calcom/packages/app-store/closecom/api/_postAdd.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { symmetricEncrypt } from "@calcom/lib/crypto";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import checkSession from "../../_utils/auth";
|
||||
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
|
||||
import appConfig from "../config.json";
|
||||
|
||||
export async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const session = checkSession(req);
|
||||
|
||||
const { api_key } = req.body;
|
||||
if (!api_key) throw new HttpError({ statusCode: 400, message: "No Api Key provoided to check" });
|
||||
|
||||
const encrypted = symmetricEncrypt(JSON.stringify({ api_key }), process.env.CALENDSO_ENCRYPTION_KEY || "");
|
||||
|
||||
const data = {
|
||||
type: appConfig.type,
|
||||
key: { encrypted },
|
||||
userId: session.user?.id,
|
||||
appId: appConfig.slug,
|
||||
};
|
||||
|
||||
try {
|
||||
await prisma.credential.create({
|
||||
data,
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
} catch (reason) {
|
||||
logger.error("Could not add Close.com app", reason);
|
||||
return res.status(500).json({ message: "Could not add Close.com app" });
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
url: req.query.returnTo ? req.query.returnTo : getInstalledAppPath({ variant: "crm", slug: "closecom" }),
|
||||
});
|
||||
}
|
||||
|
||||
export default defaultResponder(getHandler);
|
29
calcom/packages/app-store/closecom/api/_postCheck.ts
Normal file
29
calcom/packages/app-store/closecom/api/_postCheck.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import CloseCom from "@calcom/lib/CloseCom";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
|
||||
import checkSession from "../../_utils/auth";
|
||||
|
||||
export async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { api_key } = req.body;
|
||||
if (!api_key) throw new HttpError({ statusCode: 400, message: "No Api Key provoided to check" });
|
||||
|
||||
checkSession(req);
|
||||
|
||||
const closeCom: CloseCom = new CloseCom(api_key);
|
||||
|
||||
try {
|
||||
const userInfo = await closeCom.me();
|
||||
if (userInfo.first_name) {
|
||||
return res.status(200).end();
|
||||
} else {
|
||||
return res.status(404).end();
|
||||
}
|
||||
} catch (e) {
|
||||
return res.status(500).json({ message: e });
|
||||
}
|
||||
}
|
||||
|
||||
export default defaultResponder(getHandler);
|
6
calcom/packages/app-store/closecom/api/add.ts
Normal file
6
calcom/packages/app-store/closecom/api/add.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { defaultHandler } from "@calcom/lib/server";
|
||||
|
||||
export default defaultHandler({
|
||||
GET: import("./_getAdd"),
|
||||
POST: import("./_postAdd"),
|
||||
});
|
5
calcom/packages/app-store/closecom/api/check.ts
Normal file
5
calcom/packages/app-store/closecom/api/check.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { defaultHandler } from "@calcom/lib/server";
|
||||
|
||||
export default defaultHandler({
|
||||
POST: import("./_postCheck"),
|
||||
});
|
2
calcom/packages/app-store/closecom/api/index.ts
Normal file
2
calcom/packages/app-store/closecom/api/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { default as add } from "./add";
|
||||
export { default as check } from "./check";
|
@ -0,0 +1,27 @@
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
import AppCard from "@calcom/app-store/_components/AppCard";
|
||||
import useIsAppEnabled from "@calcom/app-store/_utils/useIsAppEnabled";
|
||||
import type { EventTypeAppCardComponent } from "@calcom/app-store/types";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
|
||||
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) {
|
||||
const pathname = usePathname();
|
||||
|
||||
const { enabled, updateEnabled } = useIsAppEnabled(app);
|
||||
|
||||
return (
|
||||
<AppCard
|
||||
returnTo={`${WEBAPP_URL}${pathname}?tabName=apps`}
|
||||
app={app}
|
||||
teamId={eventType.team?.id || undefined}
|
||||
switchOnClick={(e) => {
|
||||
updateEnabled(e);
|
||||
}}
|
||||
switchChecked={enabled}
|
||||
hideAppCardOptions
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default EventTypeAppCard;
|
17
calcom/packages/app-store/closecom/config.json
Normal file
17
calcom/packages/app-store/closecom/config.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"/*": "Don't modify slug - If required, do it using cli edit command",
|
||||
"name": "Close.com",
|
||||
"title": "Close.com",
|
||||
"slug": "closecom",
|
||||
"type": "closecom_crm",
|
||||
"logo": "icon.svg",
|
||||
"url": "https://cal.com/",
|
||||
"variant": "crm",
|
||||
"categories": ["crm"],
|
||||
"publisher": "Cal.com, Inc.",
|
||||
"extendsFeature": "EventType",
|
||||
"email": "help@cal.com",
|
||||
"description": "Close is the inside sales CRM of choice for startups and SMBs. Make more calls, send more emails and close more deals starting today.",
|
||||
"__createdUsingCli": true,
|
||||
"isOAuth": false
|
||||
}
|
2
calcom/packages/app-store/closecom/index.ts
Normal file
2
calcom/packages/app-store/closecom/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * as api from "./api";
|
||||
export * as lib from "./lib";
|
180
calcom/packages/app-store/closecom/lib/CrmService.ts
Normal file
180
calcom/packages/app-store/closecom/lib/CrmService.ts
Normal file
@ -0,0 +1,180 @@
|
||||
import z from "zod";
|
||||
|
||||
import type { CloseComFieldOptions } from "@calcom/lib/CloseCom";
|
||||
import CloseCom from "@calcom/lib/CloseCom";
|
||||
import { getCustomActivityTypeInstanceData } from "@calcom/lib/CloseComeUtils";
|
||||
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
import type { CredentialPayload } from "@calcom/types/Credential";
|
||||
import type { CRM, ContactCreateInput, CrmEvent, Contact } from "@calcom/types/CrmService";
|
||||
|
||||
const apiKeySchema = z.object({
|
||||
encrypted: z.string(),
|
||||
});
|
||||
|
||||
const CALENDSO_ENCRYPTION_KEY = process.env.CALENDSO_ENCRYPTION_KEY || "";
|
||||
|
||||
// Cal.com Custom Activity Fields
|
||||
const calComCustomActivityFields: CloseComFieldOptions = [
|
||||
// Field name, field type, required?, multiple values?
|
||||
["Attendees", "contact", false, true],
|
||||
["Date & Time", "datetime", true, false],
|
||||
["Time zone", "text", true, false],
|
||||
["Organizer", "contact", true, false],
|
||||
["Additional notes", "text", false, false],
|
||||
];
|
||||
|
||||
/**
|
||||
* Authentication
|
||||
* Close.com requires Basic Auth for any request to their APIs, which is far from
|
||||
* ideal considering that such a strategy requires generating an API Key by the
|
||||
* user and input it in our system. A Setup page was created when trying to install
|
||||
* Close.com App in order to instruct how to create such resource and to obtain it.
|
||||
*
|
||||
* Meeting creation
|
||||
* Close.com does not expose a "Meeting" API, it may be available in the future.
|
||||
*
|
||||
* Per Close.com documentation (https://developer.close.com/resources/custom-activities):
|
||||
* "To work with Custom Activities, you will need to create a Custom Activity Type and
|
||||
* likely add one or more Activity Custom Fields to that type. Once the Custom Activity
|
||||
* Type is defined, you can create Custom Activity instances of that type as you would
|
||||
* any other activity."
|
||||
*
|
||||
* Contact creation
|
||||
* Every contact in Close.com need to belong to a Lead. When creating a contact in
|
||||
* Close.com as part of this integration, a new generic Lead will be created in order
|
||||
* to assign every contact created by this process, and it is named "From Cal.com"
|
||||
*/
|
||||
export default class CloseComCRMService implements CRM {
|
||||
private integrationName = "";
|
||||
private closeCom: CloseCom;
|
||||
private log: typeof logger;
|
||||
|
||||
constructor(credential: CredentialPayload) {
|
||||
this.integrationName = "closecom_other_calendar";
|
||||
this.log = logger.getSubLogger({ prefix: [`[[lib] ${this.integrationName}`] });
|
||||
|
||||
const parsedCredentialKey = apiKeySchema.safeParse(credential.key);
|
||||
|
||||
let decrypted;
|
||||
if (parsedCredentialKey.success) {
|
||||
decrypted = symmetricDecrypt(parsedCredentialKey.data.encrypted, CALENDSO_ENCRYPTION_KEY);
|
||||
const { api_key } = JSON.parse(decrypted);
|
||||
this.closeCom = new CloseCom(api_key);
|
||||
} else {
|
||||
throw Error(
|
||||
`No API Key found for userId ${credential.userId} and appId ${credential.appId}: ${parsedCredentialKey.error}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
closeComUpdateCustomActivity = async (uid: string, event: CalendarEvent) => {
|
||||
const customActivityTypeInstanceData = await getCustomActivityTypeInstanceData(
|
||||
event,
|
||||
calComCustomActivityFields,
|
||||
this.closeCom
|
||||
);
|
||||
// Create Custom Activity type instance
|
||||
const customActivityTypeInstance = await this.closeCom.activity.custom.create(
|
||||
customActivityTypeInstanceData
|
||||
);
|
||||
return this.closeCom.activity.custom.update(uid, customActivityTypeInstance);
|
||||
};
|
||||
|
||||
closeComDeleteCustomActivity = async (uid: string) => {
|
||||
return this.closeCom.activity.custom.delete(uid);
|
||||
};
|
||||
|
||||
async createEvent(event: CalendarEvent): Promise<CrmEvent> {
|
||||
const customActivityTypeInstanceData = await getCustomActivityTypeInstanceData(
|
||||
event,
|
||||
calComCustomActivityFields,
|
||||
this.closeCom
|
||||
);
|
||||
// Create Custom Activity type instance
|
||||
const customActivityTypeInstance = await this.closeCom.activity.custom.create(
|
||||
customActivityTypeInstanceData
|
||||
);
|
||||
return Promise.resolve({
|
||||
uid: customActivityTypeInstance.id,
|
||||
id: customActivityTypeInstance.id,
|
||||
type: this.integrationName,
|
||||
password: "",
|
||||
url: "",
|
||||
additionalInfo: {
|
||||
customActivityTypeInstanceData,
|
||||
},
|
||||
success: true,
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
async updateEvent(uid: string, event: CalendarEvent): Promise<CrmEvent> {
|
||||
const updatedEvent = await this.closeComUpdateCustomActivity(uid, event);
|
||||
return {
|
||||
id: updatedEvent.id,
|
||||
};
|
||||
}
|
||||
|
||||
async deleteEvent(uid: string): Promise<void> {
|
||||
await this.closeComDeleteCustomActivity(uid);
|
||||
}
|
||||
|
||||
async getContacts(emails: string | string[]): Promise<Contact[]> {
|
||||
const contactsQuery = await this.closeCom.contact.search({
|
||||
emails: Array.isArray(emails) ? emails : [emails],
|
||||
});
|
||||
|
||||
return contactsQuery.data.map((contact) => {
|
||||
return {
|
||||
id: contact.id,
|
||||
email: contact.emails[0].email,
|
||||
name: contact.name,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async createContacts(contactsToCreate: ContactCreateInput[]): Promise<Contact[]> {
|
||||
// In Close.com contacts need to be attached to a lead
|
||||
// Assume all attendees in an event belong under a lead
|
||||
|
||||
const contacts = [];
|
||||
|
||||
// Create main lead
|
||||
const lead = await this.closeCom.lead.create({
|
||||
contactName: contactsToCreate[0].name,
|
||||
contactEmail: contactsToCreate[0].email,
|
||||
});
|
||||
|
||||
contacts.push({
|
||||
id: lead.contacts[0].id,
|
||||
email: lead.contacts[0].emails[0].email,
|
||||
});
|
||||
|
||||
// Check if we need to crate more contacts under the lead
|
||||
if (contactsToCreate.length > 1) {
|
||||
const createContactPromise = [];
|
||||
for (const contact of contactsToCreate) {
|
||||
createContactPromise.push(
|
||||
this.closeCom.contact.create({
|
||||
leadId: lead.id,
|
||||
person: {
|
||||
email: contact.email,
|
||||
name: contact.name,
|
||||
},
|
||||
})
|
||||
);
|
||||
const createdContacts = await Promise.all(createContactPromise);
|
||||
for (const createdContact of createdContacts) {
|
||||
contacts.push({
|
||||
id: createdContact.id,
|
||||
email: createdContact.emails[0].email,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return contacts;
|
||||
}
|
||||
}
|
1
calcom/packages/app-store/closecom/lib/index.ts
Normal file
1
calcom/packages/app-store/closecom/lib/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default as CrmService } from "./CrmService";
|
19
calcom/packages/app-store/closecom/package.json
Normal file
19
calcom/packages/app-store/closecom/package.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"name": "@calcom/closecom",
|
||||
"version": "0.0.0",
|
||||
"main": "./index.ts",
|
||||
"description": "Close is the inside sales CRM of choice for startups and SMBs. Make more calls, send more emails and close more deals starting today.",
|
||||
"scripts": {
|
||||
"test": "vitest",
|
||||
"test:coverage": "vitest run --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*",
|
||||
"@calcom/prisma": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
}
|
||||
}
|
160
calcom/packages/app-store/closecom/pages/setup/index.tsx
Normal file
160
calcom/packages/app-store/closecom/pages/setup/index.tsx
Normal file
@ -0,0 +1,160 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
import z from "zod";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
|
||||
import { Button, Form, showToast, TextField } from "@calcom/ui";
|
||||
|
||||
const formSchema = z.object({
|
||||
api_key: z.string(),
|
||||
});
|
||||
|
||||
export default function CloseComSetup() {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const query = useRouterQuery();
|
||||
const [testPassed, setTestPassed] = useState<boolean | undefined>(undefined);
|
||||
const [testLoading, setTestLoading] = useState<boolean>(false);
|
||||
|
||||
const form = useForm<{
|
||||
api_key: string;
|
||||
}>({
|
||||
resolver: zodResolver(formSchema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
if (testPassed === false) {
|
||||
setTestPassed(undefined);
|
||||
}
|
||||
}, 3000);
|
||||
return () => clearTimeout(timer);
|
||||
}, [testPassed]);
|
||||
|
||||
return (
|
||||
<div className="bg-emphasis flex h-screen">
|
||||
<div className="bg-default m-auto rounded p-5 md:w-[520px] md:p-10">
|
||||
<div className="flex flex-col space-y-5 md:flex-row md:space-x-5 md:space-y-0">
|
||||
<div>
|
||||
{/* eslint-disable @next/next/no-img-element */}
|
||||
<img
|
||||
src="/api/app-store/closecom/icon.svg"
|
||||
alt="Apple Calendar"
|
||||
className="h-12 w-12 max-w-2xl"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-default">{t("provide_api_key")}</h1>
|
||||
|
||||
<div className="mt-1 text-sm">
|
||||
{t("generate_api_key_description", { appName: "Close.com" })}{" "}
|
||||
<a
|
||||
className="text-indigo-400"
|
||||
href="https://app.close.com/settings/api/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
Close.com
|
||||
</a>
|
||||
. {t("it_stored_encrypted")}
|
||||
</div>
|
||||
<div className="my-2 mt-3">
|
||||
<Form
|
||||
form={form}
|
||||
handleSubmit={async (values) => {
|
||||
const { returnTo } = query;
|
||||
const res = await fetch(
|
||||
`/api/integrations/closecom/add${returnTo ? `?returnTo=${returnTo}` : ""}`,
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify(values),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
const json = await res.json();
|
||||
|
||||
if (res.ok) {
|
||||
router.push(json.url);
|
||||
} else {
|
||||
showToast(json.message, "error");
|
||||
}
|
||||
}}>
|
||||
<fieldset className="space-y-2" disabled={form.formState.isSubmitting}>
|
||||
<Controller
|
||||
name="api_key"
|
||||
control={form.control}
|
||||
render={({ field: { onBlur, onChange } }) => (
|
||||
<TextField
|
||||
className="my-0"
|
||||
onBlur={onBlur}
|
||||
disabled={testPassed === true}
|
||||
name="api_key"
|
||||
placeholder="api_xyz..."
|
||||
onChange={async (e) => {
|
||||
onChange(e.target.value);
|
||||
form.setValue("api_key", e.target.value);
|
||||
await form.trigger("api_key");
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</fieldset>
|
||||
<div className="mt-5 justify-end space-x-2 rtl:space-x-reverse sm:mt-4 sm:flex">
|
||||
<Button type="button" color="secondary" onClick={() => router.back()}>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
loading={testLoading}
|
||||
disabled={testPassed === true}
|
||||
StartIcon={testPassed === undefined ? undefined : testPassed ? "check" : "x"}
|
||||
className={
|
||||
testPassed !== undefined
|
||||
? testPassed
|
||||
? " !bg-success hover:bg-success !text-green-700"
|
||||
: "bg-error hover:bg-error !border-red-700 !text-red-700"
|
||||
: "secondary"
|
||||
}
|
||||
color={testPassed === true ? "minimal" : "secondary"}
|
||||
onClick={async () => {
|
||||
const check = await form.trigger("api_key");
|
||||
if (!check) return;
|
||||
const api_key = form.getValues("api_key");
|
||||
setTestLoading(true);
|
||||
const res = await fetch("/api/integrations/closecom/check", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ api_key }),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status === 200) {
|
||||
setTestPassed(true);
|
||||
} else {
|
||||
setTestPassed(false);
|
||||
}
|
||||
setTestLoading(false);
|
||||
}}>
|
||||
{t(
|
||||
testPassed !== undefined ? (testPassed ? "test_passed" : "test_failed") : "test_api_key"
|
||||
)}
|
||||
</Button>
|
||||
<Button type="submit" loading={form.formState.isSubmitting}>
|
||||
{t("save")}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Toaster position="bottom-right" />
|
||||
</div>
|
||||
);
|
||||
}
|
BIN
calcom/packages/app-store/closecom/static/1.jpg
Normal file
BIN
calcom/packages/app-store/closecom/static/1.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 98 KiB |
BIN
calcom/packages/app-store/closecom/static/2.jpg
Normal file
BIN
calcom/packages/app-store/closecom/static/2.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 107 KiB |
BIN
calcom/packages/app-store/closecom/static/3.jpg
Normal file
BIN
calcom/packages/app-store/closecom/static/3.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 135 KiB |
BIN
calcom/packages/app-store/closecom/static/4.jpg
Normal file
BIN
calcom/packages/app-store/closecom/static/4.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 120 KiB |
BIN
calcom/packages/app-store/closecom/static/5.jpg
Normal file
BIN
calcom/packages/app-store/closecom/static/5.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 79 KiB |
27
calcom/packages/app-store/closecom/static/icon.svg
Normal file
27
calcom/packages/app-store/closecom/static/icon.svg
Normal file
@ -0,0 +1,27 @@
|
||||
<svg viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<path d="M63.9,33.3 L63.9,30.6 C63.9,28.3 63.0,26.2 61.3,24.2 C60.4,23.2 59.2,22.1 57.8,21.2 C57.1,20.7 56.3,20.2 55.4,19.7 C54.7,19.4 54.1,19.0 53.4,18.7 C51.5,17.9 49.5,17.2 47.3,16.6 C47.6,17.5 47.8,18.5 48.0,19.4 C48.2,20.4 48.4,21.4 48.6,22.4 C49.7,22.8 50.9,23.2 51.9,23.7 C52.4,23.9 52.9,24.1 53.4,24.4 C53.6,24.5 53.9,24.6 54.1,24.7 C55.0,25.2 55.7,25.7 56.4,26.2 C59.0,27.9 60.6,29.9 61.1,31.9 C60.6,34.0 59.0,36.0 56.4,37.7 C56.7,38.5 57.0,39.2 57.2,39.9 C57.5,40.8 57.7,41.8 57.8,42.7 C59.2,41.8 60.4,40.7 61.3,39.7 C63.0,37.7 63.9,35.6 63.9,33.3" id="Fill-1" fill="#2F577E"></path>
|
||||
<path d="M24.9,48.9 C23.8,48.0 22.7,47.1 21.7,46.1 C21.9,46.9 22.1,47.8 22.4,48.6 C23.2,48.7 24.1,48.8 24.9,48.9" id="Fill-2" fill="#2F577E"></path>
|
||||
<path d="M30.7,46.6 C31.1,46.9 31.5,47.2 31.9,47.5 C32.8,48.0 33.6,48.6 34.4,49.0 C34.5,49.1 34.6,49.2 34.7,49.2 C36.1,49.2 37.6,49.1 39.0,48.9 C39.8,48.8 40.7,48.7 41.5,48.6 C42.5,48.4 43.5,48.2 44.5,48.0 C45.4,47.8 46.4,47.6 47.3,47.3 C49.5,46.7 51.5,46.0 53.4,45.2 C53.2,44.3 53.0,43.4 52.7,42.5 C52.6,42.3 52.6,42.0 52.5,41.7 C52.3,41.2 52.1,40.7 51.9,40.2 C50.9,40.7 49.7,41.1 48.6,41.5 C47.8,41.8 46.9,42.0 46.1,42.2 C46.0,42.3 45.9,42.3 45.8,42.3 C44.8,42.6 43.9,42.8 42.9,42.9 C42.4,43.0 41.9,43.1 41.4,43.2 C39.9,43.5 38.4,43.6 36.8,43.8 C35.2,43.9 33.6,44.0 31.9,44.0 C30.3,44.0 28.7,43.9 27.1,43.8 C28.2,44.8 29.5,45.7 30.7,46.6" id="Fill-3" fill="#2F577E"></path>
|
||||
<path d="M11.2,21.4 C11.2,21.6 11.3,21.9 11.4,22.2 C11.6,22.7 11.8,23.2 12.0,23.7 C13.0,23.2 14.2,22.8 15.3,22.4 C16.1,22.1 17.0,21.9 17.8,21.7 C17.9,21.6 18.0,21.6 18.1,21.6 C19.1,21.4 20.0,21.1 21.0,21.0 C21.5,20.9 22.0,20.8 22.5,20.7 C24.0,20.5 25.5,20.3 27.1,20.1 C28.7,20.0 30.3,20.0 31.9,20.0 C33.6,20.0 35.2,20.0 36.8,20.1 C35.7,19.1 34.4,18.2 33.2,17.3 C32.8,17.0 32.4,16.7 31.9,16.4 C31.1,15.9 30.3,15.3 29.5,14.9 C29.4,14.8 29.3,14.7 29.2,14.7 C27.7,14.7 26.3,14.9 24.9,15.0 C24.1,15.1 23.2,15.2 22.4,15.3 C21.4,15.5 20.4,15.7 19.4,15.9 C18.5,16.1 17.5,16.3 16.6,16.6 C14.4,17.2 12.4,17.9 10.5,18.7 C10.7,19.6 10.9,20.5 11.2,21.4" id="Fill-4" fill="#2F577E"></path>
|
||||
<path d="M39.0,15.0 C40.1,15.9 41.2,16.8 42.2,17.8 C42.0,17.0 41.8,16.1 41.5,15.3 C40.7,15.2 39.8,15.1 39.0,15.0" id="Fill-5" fill="#2F577E"></path>
|
||||
<path d="M2.8,31.9 C3.3,29.9 4.9,27.9 7.5,26.2 C7.2,25.4 6.9,24.7 6.7,24.0 C6.4,23.1 6.2,22.1 6.0,21.2 C4.7,22.1 3.5,23.2 2.6,24.2 C0.9,26.2 6.7e-14,28.3 6.7e-14,30.6 L6.7e-14,33.3 C6.7e-14,35.6 0.9,37.7 2.6,39.7 C3.5,40.7 4.7,41.7 6.0,42.7 C6.8,43.2 7.6,43.7 8.5,44.2 C9.2,44.5 9.8,44.9 10.5,45.2 C12.4,46.0 14.4,46.7 16.6,47.3 C16.3,46.4 16.1,45.4 15.9,44.5 C15.7,43.5 15.5,42.5 15.3,41.5 C14.2,41.1 13.0,40.7 12.0,40.2 C11.5,40.0 11.0,39.8 10.5,39.5 C10.3,39.4 10.0,39.3 9.8,39.2 C8.9,38.7 8.2,38.2 7.5,37.7 C4.9,36.0 3.3,34.0 2.8,31.9" id="Fill-6" fill="#2F577E"></path>
|
||||
<path d="M39.0,48.9 C37.6,49.0 36.1,49.2 34.7,49.2 C35.4,49.7 36.2,50.1 36.9,50.4 C37.6,49.9 38.3,49.4 39.0,48.9" id="Fill-7" fill="#F4B563"></path>
|
||||
<path d="M55.5,10.3 L53.6,8.4 C52.0,6.8 49.8,5.9 47.2,5.7 C45.8,5.6 44.3,5.8 42.7,6.1 C41.8,6.2 40.8,6.5 39.9,6.7 C39.2,6.9 38.5,7.2 37.7,7.5 C35.9,8.2 33.9,9.1 31.9,10.2 C32.8,10.7 33.6,11.2 34.5,11.8 C35.3,12.3 36.1,12.9 36.9,13.5 C38.0,12.9 39.1,12.4 40.2,12.0 C40.7,11.8 41.2,11.6 41.7,11.4 C42.0,11.3 42.3,11.3 42.5,11.2 C43.4,10.9 44.3,10.7 45.2,10.5 C48.2,10.0 50.8,10.3 52.6,11.3" id="Fill-8" fill="#F4B563"></path>
|
||||
<path d="M52.6,11.3 C53.6,13.1 53.9,15.7 53.4,18.7 C54.1,19.0 54.7,19.4 55.4,19.7 C56.3,20.2 57.1,20.7 57.8,21.2 C58.1,19.6 58.3,18.1 58.2,16.7 C58.0,14.1 57.1,11.9 55.5,10.3 C55.2,9.9 54.9,9.7 54.5,9.4" id="Fill-9" fill="#F4B563"></path>
|
||||
<path d="M41.4,43.2 C41.9,43.1 42.4,43.0 42.9,42.9 C43.9,42.8 44.8,42.6 45.8,42.3 C45.9,42.3 46.0,42.3 46.1,42.2 C47.1,41.2 48.0,40.1 48.9,39.0 C49.4,38.3 50.0,37.6 50.4,36.9 C51.0,36.1 51.6,35.3 52.2,34.5 C52.7,33.6 53.2,32.8 53.7,32.0 C54.8,30.0 55.7,28.0 56.4,26.2 C55.7,25.7 55.0,25.2 54.1,24.7 C53.9,24.6 53.6,24.5 53.4,24.4 C52.9,24.1 52.4,23.9 51.9,23.7 C51.5,24.8 51.0,25.9 50.4,27.0 C50.1,27.7 49.7,28.5 49.2,29.2 C49.2,29.3 49.1,29.4 49.0,29.5 C48.6,30.3 48.0,31.1 47.5,32.0 C47.2,32.4 46.9,32.8 46.6,33.2 C45.7,34.4 44.8,35.7 43.8,36.8 C42.7,38.1 41.6,39.3 40.4,40.4 C39.3,41.6 38.1,42.7 36.8,43.8 C38.4,43.6 39.9,43.5 41.4,43.2" id="Fill-10" fill="#F4B563"></path>
|
||||
<path d="M24.9,15.0 C26.3,14.8 27.7,14.7 29.2,14.7 C28.5,14.2 27.7,13.8 27.0,13.5 C26.3,13.9 25.6,14.5 24.9,15.0" id="Fill-11" fill="#F4B563"></path>
|
||||
<path d="M11.3,52.6 C10.3,50.8 10.0,48.2 10.5,45.2 C9.8,44.9 9.2,44.5 8.5,44.2 C7.6,43.7 6.8,43.2 6.0,42.7 C5.8,44.3 5.6,45.8 5.7,47.2 C5.9,49.8 6.8,52.0 8.4,53.6 L10.3,55.5 C11.9,57.1 14.1,58.0 16.7,58.2 C18.1,58.3 19.6,58.1 21.2,57.9 C22.1,57.7 23.1,57.5 24.0,57.2 C24.7,57.0 25.4,56.7 26.2,56.4 C28.0,55.7 30.0,54.8 31.9,53.7 C31.1,53.2 30.3,52.7 29.4,52.2 C28.6,51.6 27.8,51.1 27.0,50.5 C25.9,51.0 24.8,51.5 23.7,51.9 C23.2,52.1 22.7,52.3 22.2,52.5 C21.9,52.6 21.6,52.7 21.4,52.7 C20.5,53.0 19.6,53.2 18.7,53.4 C15.7,53.9 13.1,53.6 11.3,52.6" id="Fill-12" fill="#F4B563"></path>
|
||||
<path d="M9.8,39.2 C10.0,39.3 10.3,39.4 10.5,39.5 C11.0,39.8 11.5,40.0 12.0,40.2 C12.4,39.1 12.9,38.0 13.5,36.9 C13.8,36.2 14.2,35.4 14.7,34.7 C14.7,34.6 14.8,34.5 14.9,34.4 C15.3,33.6 15.9,32.8 16.4,31.9 C16.7,31.5 17.0,31.1 17.3,30.7 C18.2,29.5 19.1,28.2 20.1,27.1 C21.2,25.8 22.3,24.6 23.5,23.5 C24.6,22.3 25.8,21.2 27.1,20.1 C25.5,20.3 24.0,20.4 22.5,20.7 C22.0,20.8 21.5,20.9 21.0,21.0 C20.0,21.1 19.1,21.4 18.1,21.6 C18.0,21.6 17.9,21.6 17.8,21.7 C16.8,22.7 15.9,23.8 15.0,24.9 C14.5,25.6 13.9,26.3 13.4,27.0 C12.9,27.8 12.3,28.6 11.7,29.4 C11.2,30.3 10.7,31.1 10.2,31.9 C9.1,33.9 8.2,35.9 7.5,37.7 C8.2,38.2 8.9,38.7 9.8,39.2" id="Fill-13" fill="#F4B563"></path>
|
||||
<path d="M15.0,39.0 C14.8,37.6 14.7,36.1 14.7,34.7 C14.2,35.4 13.8,36.2 13.5,36.9 C13.9,37.6 14.5,38.3 15.0,39.0" id="Fill-14" fill="#5CAFCD"></path>
|
||||
<path d="M11.3,11.3 C13.1,10.3 15.7,10.0 18.7,10.5 C19.0,9.8 19.4,9.2 19.7,8.5 C20.2,7.6 20.7,6.8 21.2,6.1 C19.6,5.8 18.1,5.6 16.7,5.7 C14.1,5.9 11.9,6.8 10.3,8.4 C9.9,8.7 9.7,9.0 9.4,9.4 C9.7,9.0 9.9,8.7 10.3,8.4 L8.4,10.3 C6.8,11.9 5.9,14.1 5.7,16.7 C5.6,18.1 5.8,19.6 6.0,21.2 C6.2,22.1 6.4,23.1 6.7,24.0 C6.9,24.7 7.2,25.4 7.5,26.2 C8.2,28.0 9.1,30.0 10.2,32.0 C10.7,31.1 11.2,30.3 11.7,29.4 C12.3,28.6 12.9,27.8 13.4,27.0 C12.9,25.9 12.4,24.8 12.0,23.7 C11.8,23.2 11.6,22.7 11.4,22.2 C11.3,21.9 11.2,21.6 11.2,21.4 C10.9,20.5 10.7,19.6 10.5,18.7 C10.0,15.7 10.3,13.1 11.3,11.3" id="Fill-15" fill="#5CAFCD"></path>
|
||||
<path d="M48.9,24.9 C49.0,26.3 49.2,27.8 49.2,29.2 C49.7,28.5 50.1,27.7 50.4,27.0 C50.0,26.3 49.4,25.6 48.9,24.9" id="Fill-16" fill="#5CAFCD"></path>
|
||||
<path d="M43.2,22.5 C43.1,22.0 43.0,21.5 42.9,21.0 C42.8,20.0 42.5,19.1 42.3,18.1 C42.3,18.0 42.3,17.9 42.2,17.8 C41.2,16.8 40.1,15.9 39.0,15.0 C38.3,14.5 37.6,13.9 36.9,13.5 C36.1,12.9 35.3,12.3 34.5,11.7 C33.6,11.2 32.8,10.7 31.9,10.2 C30.0,9.1 28.0,8.2 26.2,7.5 C25.7,8.2 25.2,8.9 24.7,9.8 C24.6,10.0 24.5,10.3 24.4,10.5 C24.1,11.0 23.9,11.5 23.7,12.0 C24.8,12.4 25.9,12.9 27.0,13.5 C27.7,13.8 28.5,14.2 29.2,14.7 C29.3,14.7 29.4,14.8 29.5,14.9 C30.3,15.3 31.1,15.9 31.9,16.4 C32.4,16.7 32.8,17.0 33.2,17.3 C34.4,18.2 35.7,19.1 36.8,20.1 C38.1,21.2 39.3,22.3 40.4,23.5 C41.6,24.6 42.7,25.8 43.8,27.1 C43.6,25.5 43.4,24.0 43.2,22.5" id="Fill-17" fill="#5CAFCD"></path>
|
||||
<path d="M20.7,41.4 C20.8,41.9 20.9,42.4 21.0,42.9 C21.1,43.9 21.4,44.8 21.6,45.8 C21.6,45.9 21.6,46.0 21.7,46.1 C22.7,47.1 23.8,48.0 24.9,48.9 C25.6,49.4 26.3,50.0 27.0,50.4 C27.8,51.0 28.6,51.6 29.4,52.2 C30.3,52.7 31.1,53.2 31.9,53.7 C33.9,54.8 35.9,55.7 37.7,56.4 C38.2,55.7 38.7,55.0 39.2,54.1 C39.3,53.9 39.4,53.6 39.5,53.4 C39.8,52.9 40.0,52.4 40.2,51.9 C39.1,51.5 38.0,51.0 36.9,50.4 C36.2,50.1 35.4,49.7 34.7,49.2 C34.6,49.2 34.5,49.1 34.4,49.0 C33.6,48.6 32.8,48.0 31.9,47.5 C31.5,47.2 31.1,46.9 30.7,46.6 C29.5,45.7 28.2,44.8 27.1,43.8 C25.8,42.7 24.6,41.6 23.5,40.4 C22.3,39.3 21.2,38.1 20.1,36.8 C20.3,38.4 20.4,39.9 20.7,41.4" id="Fill-18" fill="#5CAFCD"></path>
|
||||
<path d="M53.6,55.5 L55.5,53.6 C57.1,52.0 58.0,49.8 58.2,47.2 C58.3,45.8 58.1,44.3 57.8,42.7 C57.7,41.8 57.5,40.8 57.2,39.9 C57.0,39.2 56.7,38.5 56.4,37.7 C55.7,35.9 54.8,33.9 53.7,32 C53.2,32.8 52.7,33.6 52.2,34.5 C51.6,35.3 51.0,36.1 50.4,36.9 C51.0,38.0 51.5,39.1 51.9,40.2 C52.1,40.7 52.3,41.2 52.5,41.7 C52.6,42.0 52.6,42.3 52.7,42.5 C53.0,43.4 53.2,44.3 53.4,45.2 C53.9,48.2 53.6,50.8 52.6,52.6 C50.8,53.6 48.2,53.9 45.2,53.4 C44.9,54.1 44.5,54.7 44.2,55.4 C43.7,56.3 43.2,57.1 42.7,57.8 C44.3,58.1 45.8,58.3 47.2,58.2 C49.8,58.0 52.0,57.1 53.6,55.5" id="Fill-19" fill="#5CAFCD"></path>
|
||||
<path d="M17.3,30.7 C17.0,31.1 16.7,31.5 16.4,32.0 C15.9,32.8 15.3,33.6 14.9,34.4 C14.8,34.5 14.7,34.6 14.7,34.7 C14.7,36.1 14.8,37.6 15.0,39.0 C15.1,39.8 15.2,40.7 15.3,41.5 C15.5,42.5 15.7,43.5 15.9,44.5 C16.1,45.4 16.3,46.4 16.6,47.3 C17.2,49.5 17.9,51.5 18.7,53.4 C19.6,53.2 20.5,53.0 21.4,52.7 C21.6,52.6 21.9,52.6 22.2,52.5 C22.7,52.3 23.2,52.1 23.7,51.9 C23.2,50.9 22.8,49.7 22.4,48.6 C22.1,47.8 21.9,46.9 21.7,46.1 C21.6,46.0 21.6,45.9 21.6,45.8 C21.4,44.8 21.1,43.9 21.0,42.9 C20.9,42.4 20.8,41.9 20.7,41.4 C20.4,39.9 20.3,38.4 20.1,36.8 C20.0,35.2 19.9,33.6 19.9,32.0 C19.9,30.3 20.0,28.7 20.1,27.1 C19.1,28.2 18.2,29.5 17.3,30.7" id="Fill-20" fill="#47B178"></path>
|
||||
<path d="M48.9,39.0 C48.0,40.1 47.1,41.2 46.1,42.2 C46.9,42.0 47.8,41.8 48.6,41.5 C48.7,40.7 48.8,39.8 48.9,39.0" id="Fill-21" fill="#47B178"></path>
|
||||
<path d="M15.0,24.9 C15.9,23.8 16.8,22.8 17.8,21.7 C17.0,21.9 16.1,22.1 15.3,22.4 C15.2,23.2 15.1,24.1 15.0,24.9" id="Fill-22" fill="#47B178"></path>
|
||||
<path d="M46.6,33.2 C46.9,32.8 47.2,32.4 47.5,32.0 C48.0,31.1 48.6,30.3 49.0,29.5 C49.1,29.4 49.2,29.3 49.2,29.2 C49.2,27.8 49.0,26.3 48.9,24.9 C48.8,24.1 48.7,23.2 48.6,22.4 C48.4,21.4 48.2,20.4 48.0,19.4 C47.8,18.5 47.6,17.5 47.3,16.6 C46.7,14.4 46.0,12.4 45.2,10.5 C44.3,10.7 43.4,10.9 42.5,11.2 C42.3,11.3 42.0,11.3 41.7,11.4 C41.2,11.6 40.7,11.8 40.2,12.0 C40.7,13.0 41.1,14.2 41.5,15.3 C41.8,16.1 42.0,17.0 42.2,17.8 C42.3,17.9 42.3,18.0 42.3,18.1 C42.5,19.1 42.8,20.0 42.9,21.0 C43.0,21.5 43.1,22.0 43.2,22.5 C43.4,24.0 43.6,25.5 43.8,27.1 C43.9,28.7 43.9,30.3 43.9,32.0 C43.9,33.6 43.9,35.2 43.8,36.8 C44.8,35.7 45.7,34.4 46.6,33.2" id="Fill-23" fill="#47B178"></path>
|
||||
<path d="M42.7,6.0 C41.7,4.7 40.7,3.5 39.7,2.6 C37.7,0.9 35.6,0 33.3,0 L30.6,0 C28.3,0 26.2,0.9 24.2,2.6 C23.2,3.5 22.1,4.7 21.2,6.0 C20.7,6.8 20.2,7.6 19.7,8.5 C19.4,9.2 19.0,9.8 18.7,10.5 C17.9,12.4 17.2,14.4 16.6,16.6 C17.5,16.3 18.5,16.1 19.4,15.9 C20.4,15.7 21.4,15.5 22.4,15.3 C22.8,14.2 23.2,13.0 23.7,12.0 C23.9,11.5 24.1,11.0 24.4,10.5 C24.5,10.3 24.6,10.0 24.7,9.8 C25.2,8.9 25.7,8.2 26.2,7.5 C27.9,4.9 29.9,3.3 31.9,2.8 C34.0,3.3 36.0,4.9 37.7,7.5 C38.5,7.2 39.2,6.9 39.9,6.7" id="Fill-24" fill="#47B178"></path>
|
||||
<path d="M30.6,63.9 L33.3,63.9 C35.6,63.9 37.7,63.0 39.7,61.3 C40.7,60.4 41.7,59.2 42.7,57.8 C43.2,57.1 43.7,56.3 44.2,55.4 C44.5,54.7 44.9,54.1 45.2,53.4 C46.0,51.5 46.7,49.5 47.3,47.3 L44.5,48.0 C43.5,48.2 42.5,48.4 41.5,48.6 C41.1,49.7 40.7,50.9 40.2,51.9 C40.0,52.4 39.8,52.9 39.5,53.4 C39.4,53.6 39.3,53.9 39.2,54.1 C38.7,55.0 38.2,55.7 37.7,56.4 C36.0,59.0 34.0,60.6 31.9,61.1 C29.9,60.6 27.9,59.0 26.2,56.4 C25.4,56.7 24.7,57.0 24.0,57.2 L21.2,57.8 C22.1,59.2 23.2,60.4 24.2,61.3 C26.2,63.0 28.3,63.9 30.6,63.9" id="Fill-26" fill="#47B178"></path>
|
||||
</svg>
|
After Width: | Height: | Size: 11 KiB |
19
calcom/packages/app-store/closecom/test/globals.ts
Normal file
19
calcom/packages/app-store/closecom/test/globals.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { vi } from "vitest";
|
||||
|
||||
vi.mock("@calcom/lib/logger", () => ({
|
||||
default: {
|
||||
getSubLogger: () => ({
|
||||
debug: vi.fn(),
|
||||
error: vi.fn(),
|
||||
log: vi.fn(),
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@calcom/lib/crypto", () => ({
|
||||
symmetricDecrypt: () => `{
|
||||
"userApiKey": "test"
|
||||
}`,
|
||||
}));
|
||||
|
||||
export {};
|
@ -0,0 +1,275 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { expect, vi, afterEach, test } from "vitest";
|
||||
|
||||
import CloseCom from "@calcom/lib/CloseCom";
|
||||
import {
|
||||
getCloseComContactIds,
|
||||
getCustomActivityTypeInstanceData,
|
||||
getCloseComCustomActivityTypeFieldsIds,
|
||||
getCloseComLeadId,
|
||||
} from "@calcom/lib/CloseComeUtils";
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
|
||||
vi.mock("@calcom/lib/CloseCom", () => ({
|
||||
default: class {
|
||||
constructor() {
|
||||
/* Mock */
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
// getCloseComLeadId
|
||||
test("check generic lead generator: already exists", async () => {
|
||||
CloseCom.prototype.lead = {
|
||||
list: () => ({
|
||||
data: [{ name: "From Cal.com", id: "abc" }],
|
||||
}),
|
||||
} as any;
|
||||
|
||||
const closeCom = new CloseCom("someKey");
|
||||
const id = await getCloseComLeadId(closeCom);
|
||||
expect(id).toEqual("abc");
|
||||
});
|
||||
|
||||
// getCloseComLeadId
|
||||
test("check generic lead generator: doesn't exist", async () => {
|
||||
CloseCom.prototype.lead = {
|
||||
list: () => ({
|
||||
data: [],
|
||||
}),
|
||||
create: () => ({ id: "def" }),
|
||||
} as any;
|
||||
|
||||
const closeCom = new CloseCom("someKey");
|
||||
const id = await getCloseComLeadId(closeCom);
|
||||
expect(id).toEqual("def");
|
||||
});
|
||||
|
||||
// getCloseComContactIds
|
||||
test("retrieve contact IDs: all exist", async () => {
|
||||
const attendees = [
|
||||
{ email: "test1@example.com", id: "test1" },
|
||||
{ email: "test2@example.com", id: "test2" },
|
||||
];
|
||||
|
||||
const event = {
|
||||
attendees,
|
||||
} as { attendees: { email: string; name: string | null; id: string }[] };
|
||||
|
||||
CloseCom.prototype.contact = {
|
||||
search: () => ({ data: attendees }),
|
||||
} as any;
|
||||
|
||||
const closeCom = new CloseCom("someKey");
|
||||
const contactIds = await getCloseComContactIds(event.attendees, closeCom, "leadId");
|
||||
expect(contactIds).toEqual(["test1", "test2"]);
|
||||
});
|
||||
|
||||
// getCloseComContactIds
|
||||
test("retrieve contact IDs: some don't exist", async () => {
|
||||
const attendees = [{ email: "test1@example.com", id: "test1" }, { email: "test2@example.com" }];
|
||||
|
||||
const event = {
|
||||
attendees,
|
||||
} as CalendarEvent;
|
||||
|
||||
CloseCom.prototype.contact = {
|
||||
search: () => ({ data: [{ emails: [{ email: "test1@example.com" }], id: "test1" }] }),
|
||||
create: () => ({ id: "test3" }),
|
||||
} as any;
|
||||
|
||||
const closeCom = new CloseCom("someKey");
|
||||
const contactIds = await getCloseComContactIds(event.attendees, closeCom, "leadId");
|
||||
expect(contactIds).toEqual(["test1", "test3"]);
|
||||
});
|
||||
|
||||
// getCloseComCustomActivityTypeFieldsIds
|
||||
test("retrieve custom fields for custom activity type: type doesn't exist, no field created", async () => {
|
||||
CloseCom.prototype.activity = {
|
||||
type: {
|
||||
get: () => [],
|
||||
},
|
||||
} as any;
|
||||
|
||||
CloseCom.prototype.customActivity = {
|
||||
type: {
|
||||
get: () => ({ data: [] }),
|
||||
create: () => ({ id: "type1" }),
|
||||
},
|
||||
} as any;
|
||||
|
||||
CloseCom.prototype.customField = {
|
||||
activity: {
|
||||
create: (data: { name: string }) => ({ id: `field${data.name.length}${data.name[0]}` }),
|
||||
},
|
||||
} as any;
|
||||
|
||||
const closeCom = new CloseCom("someKey");
|
||||
const contactIds = await getCloseComCustomActivityTypeFieldsIds(
|
||||
[
|
||||
["Attendees", "", true, true],
|
||||
["Date & Time", "", true, true],
|
||||
["Time Zone", "", true, true],
|
||||
],
|
||||
closeCom
|
||||
);
|
||||
expect(contactIds).toEqual({
|
||||
activityType: "type1",
|
||||
fields: ["field9A", "field11D", "field9T"],
|
||||
});
|
||||
});
|
||||
|
||||
// getCloseComCustomActivityTypeFieldsIds
|
||||
test("retrieve custom fields for custom activity type: type exists, no field created", async () => {
|
||||
CloseCom.prototype.activity = {
|
||||
type: {
|
||||
get: () => [],
|
||||
},
|
||||
} as any;
|
||||
|
||||
CloseCom.prototype.customActivity = {
|
||||
type: {
|
||||
get: () => ({ data: [{ id: "typeX", name: "Cal.com Activity" }] }),
|
||||
},
|
||||
} as any;
|
||||
|
||||
CloseCom.prototype.customField = {
|
||||
activity: {
|
||||
get: () => ({ data: [{ id: "fieldY", custom_activity_type_id: "typeX", name: "Attendees" }] }),
|
||||
create: (data: { name: string }) => ({ id: `field${data.name.length}${data.name[0]}` }),
|
||||
},
|
||||
} as any;
|
||||
|
||||
const closeCom = new CloseCom("someKey");
|
||||
const contactIds = await getCloseComCustomActivityTypeFieldsIds(
|
||||
[
|
||||
["Attendees", "", true, true],
|
||||
["Date & Time", "", true, true],
|
||||
["Time Zone", "", true, true],
|
||||
],
|
||||
closeCom
|
||||
);
|
||||
expect(contactIds).toEqual({
|
||||
activityType: "typeX",
|
||||
fields: ["fieldY", "field11D", "field9T"],
|
||||
});
|
||||
});
|
||||
|
||||
// getCustomActivityTypeInstanceData
|
||||
test("prepare data to create custom activity type instance: two attendees, no additional notes", async () => {
|
||||
const attendees = [
|
||||
{ email: "test1@example.com", id: "test1", timeZone: "America/Montevideo" },
|
||||
{ email: "test2@example.com" },
|
||||
];
|
||||
|
||||
const now = new Date();
|
||||
|
||||
const event = {
|
||||
attendees,
|
||||
startTime: now.toISOString(),
|
||||
} as unknown as CalendarEvent;
|
||||
|
||||
CloseCom.prototype.activity = {
|
||||
type: {
|
||||
get: () => [],
|
||||
},
|
||||
} as any;
|
||||
|
||||
CloseCom.prototype.customActivity = {
|
||||
type: {
|
||||
get: () => ({ data: [] }),
|
||||
create: () => ({ id: "type1" }),
|
||||
},
|
||||
} as any;
|
||||
|
||||
CloseCom.prototype.customField = {
|
||||
activity: {
|
||||
create: (data: { name: string }) => ({ id: `field${data.name.length}${data.name[0]}` }),
|
||||
},
|
||||
} as any;
|
||||
|
||||
CloseCom.prototype.lead = {
|
||||
list: () => ({
|
||||
data: [],
|
||||
}),
|
||||
create: () => ({ id: "def" }),
|
||||
} as any;
|
||||
|
||||
const closeCom = new CloseCom("someKey");
|
||||
const data = await getCustomActivityTypeInstanceData(
|
||||
event,
|
||||
[
|
||||
["Attendees", "", true, true],
|
||||
["Date & Time", "", true, true],
|
||||
["Time Zone", "", true, true],
|
||||
],
|
||||
closeCom
|
||||
);
|
||||
expect(data).toEqual({
|
||||
custom_activity_type_id: "type1",
|
||||
lead_id: "def",
|
||||
"custom.field9A": ["test3"],
|
||||
"custom.field11D": now.toISOString(),
|
||||
"custom.field9T": "America/Montevideo",
|
||||
});
|
||||
});
|
||||
|
||||
// getCustomActivityTypeInstanceData
|
||||
test("prepare data to create custom activity type instance: one attendees, with additional notes", async () => {
|
||||
const attendees = [{ email: "test1@example.com", id: "test1", timeZone: "America/Montevideo" }];
|
||||
|
||||
const now = new Date();
|
||||
|
||||
const event = {
|
||||
attendees,
|
||||
startTime: now.toISOString(),
|
||||
additionalNotes: "Some comment!",
|
||||
} as any;
|
||||
|
||||
CloseCom.prototype.activity = {
|
||||
type: {
|
||||
get: () => [],
|
||||
},
|
||||
} as any;
|
||||
|
||||
CloseCom.prototype.customActivity = {
|
||||
type: {
|
||||
get: () => ({ data: [] }),
|
||||
create: () => ({ id: "type1" }),
|
||||
},
|
||||
} as any;
|
||||
|
||||
CloseCom.prototype.customField = {
|
||||
activity: {
|
||||
create: (data: { name: string }) => ({ id: `field${data.name.length}${data.name[0]}` }),
|
||||
},
|
||||
} as any;
|
||||
|
||||
CloseCom.prototype.lead = {
|
||||
list: () => ({
|
||||
data: [{ name: "From Cal.com", id: "abc" }],
|
||||
}),
|
||||
} as any;
|
||||
|
||||
const closeCom = new CloseCom("someKey");
|
||||
const data = await getCustomActivityTypeInstanceData(
|
||||
event,
|
||||
[
|
||||
["Attendees", "", true, true],
|
||||
["Date & Time", "", true, true],
|
||||
["Time Zone", "", true, true],
|
||||
],
|
||||
closeCom
|
||||
);
|
||||
expect(data).toEqual({
|
||||
custom_activity_type_id: "type1",
|
||||
lead_id: "abc",
|
||||
"custom.field9A": null,
|
||||
"custom.field11D": now.toISOString(),
|
||||
"custom.field9T": "America/Montevideo",
|
||||
});
|
||||
});
|
7
calcom/packages/app-store/closecom/zod.ts
Normal file
7
calcom/packages/app-store/closecom/zod.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { eventTypeAppCardZod } from "../eventTypeAppCardZod";
|
||||
|
||||
export const appDataSchema = eventTypeAppCardZod;
|
||||
|
||||
export const appKeysSchema = z.object({});
|
Reference in New Issue
Block a user