Merge branch 'v2/teams-billing' of https://github.com/calcom/cal.com into v2/teams-billing
This commit is contained in:
parent
a4f435b9fb
commit
e650112a35
|
@ -92,11 +92,10 @@ TWILIO_MESSAGING_SID=
|
||||||
NEXT_PUBLIC_IS_E2E=
|
NEXT_PUBLIC_IS_E2E=
|
||||||
|
|
||||||
# Used for internal billing system
|
# Used for internal billing system
|
||||||
STRIPE_TEAM_MONTHLY_PRICE_ID=
|
STRIPE_TEAM_MONTHLY_PRICE_ID=
|
||||||
STRIPE_TEAM_YEARLY_PRICE_ID=
|
STRIPE_PRIVATE_KEY=
|
||||||
STRIPE_PRIVATE_KEY=
|
STRIPE_WEBHOOK_SECRET=
|
||||||
STRIPE_WEBHOOK_SECRET=
|
STRIPE_CLIENT_ID=
|
||||||
STRIPE_CLIENT_ID=
|
|
||||||
|
|
||||||
# Use for internal Public API Keys and optional
|
# Use for internal Public API Keys and optional
|
||||||
API_KEY_PREFIX=cal_
|
API_KEY_PREFIX=cal_
|
||||||
|
|
|
@ -1,73 +0,0 @@
|
||||||
import { useRef, useState } from "react";
|
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
|
||||||
import { Button } from "@calcom/ui";
|
|
||||||
import { Icon } from "@calcom/ui/Icon";
|
|
||||||
import { Alert } from "@calcom/ui/v2/core/Alert";
|
|
||||||
import { Dialog, DialogContent, DialogFooter } from "@calcom/ui/v2/core/Dialog";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function TeamCreate(props: Props) {
|
|
||||||
const { t } = useLocale();
|
|
||||||
const utils = trpc.useContext();
|
|
||||||
const [errorMessage, setErrorMessage] = useState<null | string>(null);
|
|
||||||
const nameRef = useRef<HTMLInputElement>() as React.MutableRefObject<HTMLInputElement>;
|
|
||||||
|
|
||||||
const createTeamMutation = trpc.useMutation("viewer.teams.create", {
|
|
||||||
onSuccess: () => {
|
|
||||||
utils.invalidateQueries(["viewer.teams.list"]);
|
|
||||||
props.onClose();
|
|
||||||
},
|
|
||||||
onError: (e) => {
|
|
||||||
setErrorMessage(e?.message || t("something_went_wrong"));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const createTeam = () => {
|
|
||||||
createTeamMutation.mutate({ name: nameRef?.current?.value });
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Dialog open={props.isOpen} onOpenChange={props.onClose}>
|
|
||||||
<DialogContent type="creation" actionText={t("create_new_team")} actionOnClick={createTeam}>
|
|
||||||
<div className="mb-4 sm:flex sm:items-start">
|
|
||||||
<div className="bg-brand text-brandcontrast dark:bg-darkmodebrand dark:text-darkmodebrandcontrast mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-opacity-5 sm:mx-0 sm:h-10 sm:w-10">
|
|
||||||
<Icon.FiUsers className="text-brandcontrast h-6 w-6" />
|
|
||||||
</div>
|
|
||||||
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
|
||||||
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">
|
|
||||||
{t("create_new_team")}
|
|
||||||
</h3>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-gray-400">{t("create_new_team_description")}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<form>
|
|
||||||
<div className="mb-4">
|
|
||||||
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
|
|
||||||
{t("name")}
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
ref={nameRef}
|
|
||||||
type="text"
|
|
||||||
name="name"
|
|
||||||
id="name"
|
|
||||||
placeholder="Acme Inc."
|
|
||||||
required
|
|
||||||
className="mt-1 block w-full rounded-sm border border-gray-300 px-3 py-2 text-sm"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{errorMessage && <Alert severity="error" title={errorMessage} />}
|
|
||||||
</form>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Icon } from "@calcom/ui/Icon";
|
import { Icon } from "@calcom/ui/Icon";
|
||||||
|
@ -9,12 +10,10 @@ import Button from "@calcom/ui/v2/core/Button";
|
||||||
import EmptyScreen from "@calcom/ui/v2/core/EmptyScreen";
|
import EmptyScreen from "@calcom/ui/v2/core/EmptyScreen";
|
||||||
|
|
||||||
import SkeletonLoaderTeamList from "@components/team/SkeletonloaderTeamList";
|
import SkeletonLoaderTeamList from "@components/team/SkeletonloaderTeamList";
|
||||||
import TeamCreateModal from "@components/team/TeamCreateModal";
|
|
||||||
import TeamList from "@components/team/TeamList";
|
import TeamList from "@components/team/TeamList";
|
||||||
|
|
||||||
function Teams() {
|
function Teams() {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const [showCreateTeamModal, setShowCreateTeamModal] = useState(false);
|
|
||||||
const [errorMessage, setErrorMessage] = useState("");
|
const [errorMessage, setErrorMessage] = useState("");
|
||||||
|
|
||||||
const { data, isLoading } = trpc.useQuery(["viewer.teams.list"], {
|
const { data, isLoading } = trpc.useQuery(["viewer.teams.list"], {
|
||||||
|
@ -25,22 +24,22 @@ function Teams() {
|
||||||
|
|
||||||
const teams = data?.filter((m) => m.accepted) || [];
|
const teams = data?.filter((m) => m.accepted) || [];
|
||||||
const invites = data?.filter((m) => !m.accepted) || [];
|
const invites = data?.filter((m) => !m.accepted) || [];
|
||||||
|
const handleNewTeam = () => {
|
||||||
|
// Hey
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Shell
|
<Shell
|
||||||
heading={t("teams")}
|
heading={t("teams")}
|
||||||
subtitle={t("create_manage_teams_collaborative")}
|
subtitle={t("create_manage_teams_collaborative")}
|
||||||
CTA={
|
CTA={
|
||||||
<Button type="button" onClick={() => setShowCreateTeamModal(true)}>
|
<Button type="button" href={`${WEBAPP_URL}/settings/teams/new`}>
|
||||||
<Icon.FiPlus className="inline-block h-3.5 w-3.5 text-white group-hover:text-black ltr:mr-2 rtl:ml-2" />
|
<Icon.FiPlus className="inline-block h-3.5 w-3.5 text-white group-hover:text-black ltr:mr-2 rtl:ml-2" />
|
||||||
{t("new")}
|
{t("new")}
|
||||||
</Button>
|
</Button>
|
||||||
}>
|
}>
|
||||||
<>
|
<>
|
||||||
{!!errorMessage && <Alert severity="error" title={errorMessage} />}
|
{!!errorMessage && <Alert severity="error" title={errorMessage} />}
|
||||||
{showCreateTeamModal && (
|
|
||||||
<TeamCreateModal isOpen={showCreateTeamModal} onClose={() => setShowCreateTeamModal(false)} />
|
|
||||||
)}
|
|
||||||
{invites.length > 0 && (
|
{invites.length > 0 && (
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<h1 className="mb-2 text-lg font-medium">{t("open_invitations")}</h1>
|
<h1 className="mb-2 text-lg font-medium">{t("open_invitations")}</h1>
|
||||||
|
@ -54,11 +53,10 @@ function Teams() {
|
||||||
headline={t("no_teams")}
|
headline={t("no_teams")}
|
||||||
description={t("no_teams_description")}
|
description={t("no_teams_description")}
|
||||||
buttonRaw={
|
buttonRaw={
|
||||||
<Button color="secondary" onClick={() => setShowCreateTeamModal(true)}>
|
<Button color="secondary" href={`${WEBAPP_URL}/settings/teams/new`}>
|
||||||
{t("create_team")}
|
{t("create_team")}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
buttonOnClick={() => setShowCreateTeamModal(true)}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{teams.length > 0 && <TeamList teams={teams} />}
|
{teams.length > 0 && <TeamList teams={teams} />}
|
||||||
|
|
|
@ -2,7 +2,6 @@ import DailyIframe from "@daily-co/daily-js";
|
||||||
import { NextPageContext } from "next";
|
import { NextPageContext } from "next";
|
||||||
import { getSession } from "next-auth/react";
|
import { getSession } from "next-auth/react";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import Link from "next/link";
|
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
import { SEO_IMG_OGIMG_VIDEO, WEBSITE_URL } from "@calcom/lib/constants";
|
import { SEO_IMG_OGIMG_VIDEO, WEBSITE_URL } from "@calcom/lib/constants";
|
||||||
|
@ -64,20 +63,15 @@ export default function JoinCall(props: JoinCallPageProps) {
|
||||||
<meta property="twitter:description" content={t("quick_video_meeting")} />
|
<meta property="twitter:description" content={t("quick_video_meeting")} />
|
||||||
</Head>
|
</Head>
|
||||||
<div style={{ zIndex: 2, position: "relative" }}>
|
<div style={{ zIndex: 2, position: "relative" }}>
|
||||||
<Link href="/" passHref>
|
<img
|
||||||
{
|
className="h-5·w-auto fixed z-10 hidden sm:inline-block"
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
src={`${WEBSITE_URL}/cal-logo-word-dark.svg`}
|
||||||
<img
|
alt="Cal.com Logo"
|
||||||
className="h-5·w-auto fixed z-10 hidden sm:inline-block"
|
style={{
|
||||||
src={`${WEBSITE_URL}/cal-logo-word-dark.svg`}
|
top: 46,
|
||||||
alt="Cal.com Logo"
|
left: 24,
|
||||||
style={{
|
}}
|
||||||
top: 46,
|
/>
|
||||||
left: 24,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -49,7 +49,8 @@ export async function scheduleTrigger(
|
||||||
|
|
||||||
export async function cancelScheduledJobs(
|
export async function cancelScheduledJobs(
|
||||||
booking: { uid: string; scheduledJobs?: string[] },
|
booking: { uid: string; scheduledJobs?: string[] },
|
||||||
appId?: string | null
|
appId?: string | null,
|
||||||
|
isReschedule?: boolean
|
||||||
) {
|
) {
|
||||||
let scheduledJobs = booking.scheduledJobs || [];
|
let scheduledJobs = booking.scheduledJobs || [];
|
||||||
|
|
||||||
|
@ -70,14 +71,16 @@ export async function cancelScheduledJobs(
|
||||||
scheduledJobs = [];
|
scheduledJobs = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
await prisma.booking.update({
|
if (!isReschedule) {
|
||||||
where: {
|
await prisma.booking.update({
|
||||||
uid: booking.uid,
|
where: {
|
||||||
},
|
uid: booking.uid,
|
||||||
data: {
|
},
|
||||||
scheduledJobs: scheduledJobs,
|
data: {
|
||||||
},
|
scheduledJobs: scheduledJobs,
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -887,7 +887,7 @@ async function handler(req: NextApiRequest & { userId?: number | undefined }) {
|
||||||
|
|
||||||
subscribersMeetingEnded.forEach((subscriber) => {
|
subscribersMeetingEnded.forEach((subscriber) => {
|
||||||
if (rescheduleUid && originalRescheduledBooking) {
|
if (rescheduleUid && originalRescheduledBooking) {
|
||||||
cancelScheduledJobs(originalRescheduledBooking);
|
cancelScheduledJobs(originalRescheduledBooking, undefined, true);
|
||||||
}
|
}
|
||||||
if (booking && booking.status === BookingStatus.ACCEPTED) {
|
if (booking && booking.status === BookingStatus.ACCEPTED) {
|
||||||
scheduleTrigger(booking, subscriber.subscriberUrl, subscriber);
|
scheduleTrigger(booking, subscriber.subscriberUrl, subscriber);
|
||||||
|
|
|
@ -2,14 +2,12 @@ import { useRouter } from "next/router";
|
||||||
import { Suspense, useState } from "react";
|
import { Suspense, useState } from "react";
|
||||||
|
|
||||||
import MemberInvitationModal from "@calcom/features/ee/teams/components/MemberInvitationModal";
|
import MemberInvitationModal from "@calcom/features/ee/teams/components/MemberInvitationModal";
|
||||||
import { BillingFrequency } from "@calcom/features/ee/teams/payments";
|
|
||||||
import { classNames } from "@calcom/lib";
|
import { classNames } from "@calcom/lib";
|
||||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Icon } from "@calcom/ui";
|
import { Icon } from "@calcom/ui";
|
||||||
import { Avatar, Badge, Button, showToast } from "@calcom/ui/v2/core";
|
import { Avatar, Badge, Button, showToast } from "@calcom/ui/v2/core";
|
||||||
import { Label } from "@calcom/ui/v2/core/form";
|
|
||||||
import { SkeletonContainer, SkeletonText } from "@calcom/ui/v2/core/skeleton";
|
import { SkeletonContainer, SkeletonText } from "@calcom/ui/v2/core/skeleton";
|
||||||
|
|
||||||
const AddNewTeamMemberSkeleton = () => {
|
const AddNewTeamMemberSkeleton = () => {
|
||||||
|
@ -53,7 +51,6 @@ const AddNewTeamMembers = ({ teamId }: { teamId: number }) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const [memberInviteModal, setMemberInviteModal] = useState(false);
|
const [memberInviteModal, setMemberInviteModal] = useState(false);
|
||||||
const [billingFrequency, setBillingFrequency] = useState<BillingFrequency>("monthly");
|
|
||||||
|
|
||||||
if (isLoading) return <AddNewTeamMemberSkeleton />;
|
if (isLoading) return <AddNewTeamMemberSkeleton />;
|
||||||
|
|
||||||
|
@ -124,34 +121,6 @@ const AddNewTeamMembers = ({ teamId }: { teamId: number }) => {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<>
|
|
||||||
<Label className="font-sm mt-8 text-gray-900">
|
|
||||||
<>{t("billing_frequency")}</>
|
|
||||||
</Label>
|
|
||||||
<div className="flex rounded-md border">
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
"px-1/2 w-1/2 rounded-md py-2.5 text-center font-medium text-gray-900",
|
|
||||||
billingFrequency === "monthly" && "bg-gray-200"
|
|
||||||
)}
|
|
||||||
onClick={() => {
|
|
||||||
setBillingFrequency("monthly");
|
|
||||||
}}>
|
|
||||||
<p>{t("monthly")}</p>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
"px-1/2 w-1/2 rounded-md py-2.5 text-center font-medium text-gray-900",
|
|
||||||
billingFrequency === "yearly" && "bg-gray-200"
|
|
||||||
)}
|
|
||||||
onClick={() => {
|
|
||||||
setBillingFrequency("yearly");
|
|
||||||
}}>
|
|
||||||
<p>{t("yearly")}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
|
|
||||||
<hr className="my-6 border-neutral-200" />
|
<hr className="my-6 border-neutral-200" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
@ -159,7 +128,7 @@ const AddNewTeamMembers = ({ teamId }: { teamId: number }) => {
|
||||||
className="mt-6 w-full justify-center"
|
className="mt-6 w-full justify-center"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (team) {
|
if (team) {
|
||||||
teamCheckoutMutation.mutate({ teamId, billingFrequency, seats: team.members.length });
|
teamCheckoutMutation.mutate({ teamId, seats: team.members.length });
|
||||||
} else {
|
} else {
|
||||||
showToast(t("error_creating_team"), "error");
|
showToast(t("error_creating_team"), "error");
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,27 +1,20 @@
|
||||||
import { Prisma } from "@prisma/client";
|
|
||||||
|
|
||||||
import stripe from "@calcom/app-store/stripepayment/lib/server";
|
import stripe from "@calcom/app-store/stripepayment/lib/server";
|
||||||
import { CAL_URL } from "@calcom/lib/constants";
|
import { CAL_URL } from "@calcom/lib/constants";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
export type BillingFrequency = "monthly" | "yearly";
|
export type BillingFrequency = "monthly" | "yearly";
|
||||||
|
|
||||||
export const purchaseTeamSubscription = async (
|
export const purchaseTeamSubscription = async (input: { teamId: number; seats: number; email: string }) => {
|
||||||
teamId: number,
|
const { teamId, seats, email } = input;
|
||||||
billingFrequency: "monthly" | "yearly",
|
|
||||||
seats: number,
|
|
||||||
email: string
|
|
||||||
) => {
|
|
||||||
return await stripe.checkout.sessions.create({
|
return await stripe.checkout.sessions.create({
|
||||||
mode: "subscription",
|
mode: "subscription",
|
||||||
success_url: `${CAL_URL}/settings/teams/${teamId}/profile`,
|
success_url: `${CAL_URL}/settings/teams/${teamId}/profile`,
|
||||||
cancel_url: `${CAL_URL}/settings/profile`,
|
cancel_url: `${CAL_URL}/settings/profile`,
|
||||||
|
locale: "en",
|
||||||
line_items: [
|
line_items: [
|
||||||
{
|
{
|
||||||
price:
|
/** We only need to set the base price and we can upsell it directly on Stripe's checkout */
|
||||||
billingFrequency === "monthly"
|
price: process.env.STRIPE_TEAM_MONTHLY_PRICE_ID,
|
||||||
? process.env.STRIPE_TEAM_MONTHLY_PRICE_ID
|
|
||||||
: process.env.STRIPE_TEAM_YEARLY_PRICE_ID,
|
|
||||||
quantity: seats,
|
quantity: seats,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -48,29 +41,23 @@ export const getStripeIdsForTeam = async (teamId: number) => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (teamQuery?.metadata) {
|
const teamStripeIds = { ...teamQuery.metadata };
|
||||||
const teamStripeIds = teamQuery.metadata as Prisma.JsonObject;
|
|
||||||
return teamStripeIds;
|
return teamStripeIds;
|
||||||
} else {
|
|
||||||
throw new Error(`Team ${teamId} not found`);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteTeamFromStripe = async (teamId: number) => {
|
export const deleteTeamFromStripe = async (teamId: number) => {
|
||||||
const teamQuery = await prisma.team.findFirst({
|
const stripeCustomerId = await prisma.team.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id: teamId,
|
id: teamId,
|
||||||
},
|
},
|
||||||
select: { metadata: true },
|
select: { metadata: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!teamQuery?.metadata) throw new Error(`Team ${teamId} not found`);
|
if (stripeCustomerId?.metadata?.stripeCustomerId) {
|
||||||
const teamStripeIds = teamQuery.metadata as Prisma.JsonObject;
|
await stripe.customers.del(stripeCustomerId.metadata.stripeCustomerId);
|
||||||
|
|
||||||
if (teamStripeIds.stripeCustomerId) {
|
|
||||||
await stripe.customers.del(teamStripeIds.stripeCustomerId as string);
|
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Team not found");
|
console.error(`Couldn't deleteTeamFromStripe, Team id: ${teamId} didn't have a stripeCustomerId`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -187,23 +187,49 @@ export default abstract class BaseCalendarService implements Calendar {
|
||||||
additionalInfo: {},
|
additionalInfo: {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
let calendarEvent: CalendarEventType;
|
||||||
const eventsToUpdate = events.filter((e) => e.uid === uid);
|
const eventsToUpdate = events.filter((e) => e.uid === uid);
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
eventsToUpdate.map((e) => {
|
eventsToUpdate.map((eventItem) => {
|
||||||
|
calendarEvent = eventItem;
|
||||||
return updateCalendarObject({
|
return updateCalendarObject({
|
||||||
calendarObject: {
|
calendarObject: {
|
||||||
url: e.url,
|
url: calendarEvent.url,
|
||||||
data: iCalString,
|
// ensures compliance with standard iCal string (known as iCal2.0 by some) required by various providers
|
||||||
etag: e?.etag,
|
data: iCalString?.replace(/METHOD:[^\r\n]+\r\n/g, ""),
|
||||||
|
etag: calendarEvent?.etag,
|
||||||
},
|
},
|
||||||
headers: this.headers,
|
headers: this.headers,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
).then((p) => p.map((r) => r.json() as unknown as NewCalendarEventType));
|
).then((responses) =>
|
||||||
|
responses.map((response) => {
|
||||||
|
if (response.status >= 200 && response.status < 300) {
|
||||||
|
return {
|
||||||
|
uid,
|
||||||
|
type: this.credentials.type,
|
||||||
|
id: typeof calendarEvent.uid === "string" ? calendarEvent.uid : "-1",
|
||||||
|
password: "",
|
||||||
|
url: calendarEvent.url,
|
||||||
|
additionalInfo:
|
||||||
|
typeof event.additionalInformation === "string" ? event.additionalInformation : {},
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
this.log.error("Error: Status Code", response.status);
|
||||||
|
return {
|
||||||
|
uid,
|
||||||
|
type: event.type,
|
||||||
|
id: typeof event.uid === "string" ? event.uid : "-1",
|
||||||
|
password: "",
|
||||||
|
url: typeof event.location === "string" ? event.location : "-1",
|
||||||
|
additionalInfo:
|
||||||
|
typeof event.additionalInformation === "string" ? event.additionalInformation : {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
} catch (reason) {
|
} catch (reason) {
|
||||||
this.log.error(reason);
|
this.log.error(reason);
|
||||||
|
|
||||||
throw reason;
|
throw reason;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -213,7 +239,6 @@ export default abstract class BaseCalendarService implements Calendar {
|
||||||
const events = await this.getEventsByUID(uid);
|
const events = await this.getEventsByUID(uid);
|
||||||
|
|
||||||
const eventsToDelete = events.filter((event) => event.uid === uid);
|
const eventsToDelete = events.filter((event) => event.uid === uid);
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
eventsToDelete.map((event) => {
|
eventsToDelete.map((event) => {
|
||||||
return deleteCalendarObject({
|
return deleteCalendarObject({
|
||||||
|
@ -275,7 +300,7 @@ export default abstract class BaseCalendarService implements Calendar {
|
||||||
const events: { start: string; end: string }[] = [];
|
const events: { start: string; end: string }[] = [];
|
||||||
|
|
||||||
objects.forEach((object) => {
|
objects.forEach((object) => {
|
||||||
if (object.data == null) return;
|
if (object.data == null || JSON.stringify(object.data) == "{}") return;
|
||||||
|
|
||||||
const jcalData = ICAL.parse(sanitizeCalendarObject(object));
|
const jcalData = ICAL.parse(sanitizeCalendarObject(object));
|
||||||
const vcalendar = new ICAL.Component(jcalData);
|
const vcalendar = new ICAL.Component(jcalData);
|
||||||
|
@ -330,7 +355,7 @@ export default abstract class BaseCalendarService implements Calendar {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error && error.message !== currentError) {
|
if (error instanceof Error && error.message !== currentError) {
|
||||||
currentError = error.message;
|
currentError = error.message;
|
||||||
console.log("error", error);
|
this.log.error("error", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!currentEvent) return;
|
if (!currentEvent) return;
|
||||||
|
|
|
@ -11,9 +11,9 @@ import {
|
||||||
import { getUserAvailability } from "@calcom/core/getUserAvailability";
|
import { getUserAvailability } from "@calcom/core/getUserAvailability";
|
||||||
import { sendTeamInviteEmail } from "@calcom/emails";
|
import { sendTeamInviteEmail } from "@calcom/emails";
|
||||||
import { deleteTeamFromStripe, purchaseTeamSubscription } from "@calcom/features/ee/teams/payments";
|
import { deleteTeamFromStripe, purchaseTeamSubscription } from "@calcom/features/ee/teams/payments";
|
||||||
import { HOSTED_CAL_FEATURES, WEBAPP_URL, IS_STRIPE_ENABLED } from "@calcom/lib/constants";
|
import { HOSTED_CAL_FEATURES, IS_STRIPE_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||||
import { getTeamWithMembers, isTeamAdmin, isTeamOwner, isTeamMember } from "@calcom/lib/server/queries/teams";
|
import { getTeamWithMembers, isTeamAdmin, isTeamMember, isTeamOwner } from "@calcom/lib/server/queries/teams";
|
||||||
import slugify from "@calcom/lib/slugify";
|
import slugify from "@calcom/lib/slugify";
|
||||||
import {
|
import {
|
||||||
closeComDeleteTeam,
|
closeComDeleteTeam,
|
||||||
|
@ -78,34 +78,34 @@ export const viewerTeamsRouter = createProtectedRouter()
|
||||||
.mutation("create", {
|
.mutation("create", {
|
||||||
input: z.object({
|
input: z.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
slug: z.string(),
|
slug: z.string().transform((val) => slugify(val.trim())),
|
||||||
logo: z.string().optional().nullable(),
|
logo: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.nullable()
|
||||||
|
.transform((v) => v || null),
|
||||||
}),
|
}),
|
||||||
async resolve({ ctx, input }) {
|
async resolve({ ctx, input }) {
|
||||||
const slug = input.slug || slugify(input.name);
|
const { slug, name, logo } = input;
|
||||||
|
|
||||||
const nameCollisions = await ctx.prisma.team.findFirst({
|
const nameCollisions = await ctx.prisma.team.findFirst({
|
||||||
where: {
|
where: { OR: [{ name }, { slug }] },
|
||||||
OR: [{ name: input.name }, { slug: slug }],
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (nameCollisions) throw new TRPCError({ code: "BAD_REQUEST", message: "Team name already taken." });
|
if (nameCollisions) throw new TRPCError({ code: "BAD_REQUEST", message: "Team name already taken." });
|
||||||
|
|
||||||
const createTeam = await ctx.prisma.team.create({
|
const createTeam = await ctx.prisma.team.create({
|
||||||
data: {
|
data: {
|
||||||
name: input.name,
|
name,
|
||||||
slug: slug,
|
slug,
|
||||||
logo: input.logo || null,
|
logo,
|
||||||
},
|
members: {
|
||||||
});
|
create: {
|
||||||
|
userId: ctx.user.id,
|
||||||
await ctx.prisma.membership.create({
|
role: MembershipRole.OWNER,
|
||||||
data: {
|
accepted: true,
|
||||||
teamId: createTeam.id,
|
},
|
||||||
userId: ctx.user.id,
|
},
|
||||||
role: MembershipRole.OWNER,
|
|
||||||
accepted: true,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -482,18 +482,12 @@ export const viewerTeamsRouter = createProtectedRouter()
|
||||||
.mutation("purchaseTeamSubscription", {
|
.mutation("purchaseTeamSubscription", {
|
||||||
input: z.object({
|
input: z.object({
|
||||||
teamId: z.number(),
|
teamId: z.number(),
|
||||||
billingFrequency: z.union([z.literal("monthly"), z.literal("yearly")]),
|
|
||||||
seats: z.number(),
|
seats: z.number(),
|
||||||
}),
|
}),
|
||||||
async resolve({ ctx, input }) {
|
async resolve({ ctx, input }) {
|
||||||
if (!IS_STRIPE_ENABLED)
|
if (!IS_STRIPE_ENABLED)
|
||||||
throw new TRPCError({ code: "FORBIDDEN", message: "Team billing is not enabled" });
|
throw new TRPCError({ code: "FORBIDDEN", message: "Team billing is not enabled" });
|
||||||
return await purchaseTeamSubscription(
|
return await purchaseTeamSubscription({ ...input, email: ctx.user.email });
|
||||||
input.teamId,
|
|
||||||
input.billingFrequency,
|
|
||||||
input.seats,
|
|
||||||
ctx.user.email
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.query("getTeamSeats", {
|
.query("getTeamSeats", {
|
||||||
|
|
|
@ -40,7 +40,6 @@
|
||||||
"$STRIPE_PREMIUM_PLAN_PRODUCT_ID",
|
"$STRIPE_PREMIUM_PLAN_PRODUCT_ID",
|
||||||
"$STRIPE_FREE_PLAN_PRODUCT_ID",
|
"$STRIPE_FREE_PLAN_PRODUCT_ID",
|
||||||
"$STRIPE_TEAM_MONTHLY_PRICE_ID",
|
"$STRIPE_TEAM_MONTHLY_PRICE_ID",
|
||||||
"$STRIPE_TEAM_YEARLY_PRICE_ID",
|
|
||||||
"$NEXT_PUBLIC_STRIPE_PUBLIC_KEY",
|
"$NEXT_PUBLIC_STRIPE_PUBLIC_KEY",
|
||||||
"$NEXT_PUBLIC_WEBAPP_URL",
|
"$NEXT_PUBLIC_WEBAPP_URL",
|
||||||
"$NEXT_PUBLIC_WEBSITE_URL"
|
"$NEXT_PUBLIC_WEBSITE_URL"
|
||||||
|
|
Loading…
Reference in New Issue
Block a user