diff --git a/.github/workflows/todo-to-issue.yml b/.github/workflows/todo-to-issue.yml
index 666d9ee3e3..b2caf8e54d 100644
--- a/.github/workflows/todo-to-issue.yml
+++ b/.github/workflows/todo-to-issue.yml
@@ -1,7 +1,5 @@
name: TODO to Issue
on:
- push:
- branches: [add-todo-to-issue-action] # FIXME: Remove this after merged in main
pull_request_target: # So we can test on forks
branches:
- main
diff --git a/README.md b/README.md
index 4b8ffab14e..5531563572 100644
--- a/README.md
+++ b/README.md
@@ -39,6 +39,7 @@
+
@@ -76,8 +77,6 @@ That's where Cal.com comes in. Self-hosted or hosted by us. White-label by desig
/>
-
-
#### [Product Hunt](https://www.producthunt.com/posts/calendso?utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-calendso)
{(isCancelled || reschedule) && cancellationReason && (
@@ -990,6 +1001,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
metadata: true,
cancellationReason: true,
responses: true,
+ rejectionReason: true,
user: {
select: {
id: true,
@@ -1095,6 +1107,20 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
});
}
+ const payment = await prisma.payment.findFirst({
+ where: {
+ bookingId: bookingInfo.id,
+ },
+ select: {
+ success: true,
+ refunded: true,
+ },
+ });
+ const paymentStatus = {
+ success: payment?.success,
+ refunded: payment?.refunded,
+ };
+
return {
props: {
hideBranding: eventType.team ? eventType.team.hideBranding : eventType.users[0].hideBranding,
@@ -1104,6 +1130,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
trpcState: ssr.dehydrate(),
dynamicEventName: bookingInfo?.eventType?.eventName || "",
bookingInfo,
+ paymentStatus: payment ? paymentStatus : null,
},
};
}
diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json
index 5f7fd22f6a..6a60e0110a 100644
--- a/apps/web/public/static/locales/en/common.json
+++ b/apps/web/public/static/locales/en/common.json
@@ -1595,5 +1595,9 @@
"booking_questions_title": "Booking questions",
"booking_questions_description": "Customize the questions asked on the booking page",
"add_a_booking_question": "Add a question",
- "duplicate_email": "Email is duplicate"
+ "duplicate_email": "Email is duplicate",
+ "booking_with_payment_cancelled": "Paying for this event is no longer possible",
+ "booking_with_payment_cancelled_already_paid": "A refund for this booking payment it's on the way.",
+ "booking_with_payment_cancelled_refunded": "This booking payment has been refunded.",
+ "booking_confirmation_failed": "Booking confirmation failed"
}
diff --git a/apps/web/test/lib/getSchedule.test.ts b/apps/web/test/lib/getSchedule.test.ts
index 24b17b9360..9c7025e680 100644
--- a/apps/web/test/lib/getSchedule.test.ts
+++ b/apps/web/test/lib/getSchedule.test.ts
@@ -183,14 +183,14 @@ type InputEventType = {
length?: number;
slotInterval?: number;
minimumBookingNotice?: number;
- users: { id: number }[];
+ users?: { id: number }[];
schedulingType?: SchedulingType;
beforeEventBuffer?: number;
afterEventBuffer?: number;
};
type InputBooking = {
- userId: number;
+ userId?: number;
eventTypeId: number;
startTime: string;
endTime: string;
@@ -198,6 +198,13 @@ type InputBooking = {
status: BookingStatus;
};
+type InputHost = {
+ id: number;
+ userId: number;
+ eventTypeId: number;
+ isFixed: boolean;
+};
+
const cleanup = async () => {
await prisma.eventType.deleteMany();
await prisma.user.deleteMany();
@@ -223,6 +230,7 @@ describe("getSchedule", () => {
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
const scenarioData = {
+ hosts: [],
eventTypes: [
{
id: 1,
@@ -337,6 +345,7 @@ describe("getSchedule", () => {
endTime: `${plus2DateString}T06:15:00.000Z`,
},
],
+ hosts: [],
});
// Day Plus 2 is completely free - It only has non accepted bookings
@@ -434,6 +443,7 @@ describe("getSchedule", () => {
schedules: [TestData.schedules.IstWorkHours],
},
],
+ hosts: [],
});
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
@@ -530,6 +540,7 @@ describe("getSchedule", () => {
schedules: [TestData.schedules.IstWorkHours],
},
],
+ hosts: [],
});
const { dateString: todayDateString } = getDate();
const { dateString: minus1DateString } = getDate({ dateIncrement: -1 });
@@ -606,6 +617,7 @@ describe("getSchedule", () => {
selectedCalendars: [TestData.selectedCalendars.google],
},
],
+ hosts: [],
apps: [TestData.apps.googleCalendar],
};
@@ -681,6 +693,7 @@ describe("getSchedule", () => {
},
],
apps: [TestData.apps.googleCalendar],
+ hosts: [],
};
createBookingScenario(scenarioData);
@@ -740,6 +753,7 @@ describe("getSchedule", () => {
schedules: [TestData.schedules.IstWorkHoursWithDateOverride(plus2DateString)],
},
],
+ hosts: [],
};
createBookingScenario(scenarioData);
@@ -762,6 +776,87 @@ describe("getSchedule", () => {
}
);
});
+
+ test("that a user is considered busy when there's a booking they host", async () => {
+ const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
+ const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
+
+ createBookingScenario({
+ eventTypes: [
+ // A Collective Event Type hosted by this user
+ {
+ id: 1,
+ slotInterval: 45,
+ schedulingType: "COLLECTIVE",
+ },
+ // A default Event Type which this user owns
+ {
+ id: 2,
+ slotInterval: 45,
+ users: [{ id: 101 }],
+ },
+ ],
+ users: [
+ {
+ ...TestData.users.example,
+ id: 101,
+ schedules: [TestData.schedules.IstWorkHours],
+ },
+ ],
+ bookings: [
+ // Create a booking on our Collective Event Type
+ {
+ // userId: XX, <- No owner since this is a Collective Event Type
+ eventTypeId: 1,
+ status: "ACCEPTED",
+ startTime: `${plus2DateString}T04:00:00.000Z`,
+ endTime: `${plus2DateString}T04:15:00.000Z`,
+ },
+ ],
+ hosts: [
+ // This user is a host of our Collective event
+ {
+ id: 1,
+ eventTypeId: 1,
+ userId: 101,
+ isFixed: true,
+ },
+ ],
+ });
+
+ // Requesting this user's availability for their
+ // individual Event Type
+ const thisUserAvailability = await getSchedule(
+ {
+ eventTypeId: 2,
+ eventTypeSlug: "",
+ startTime: `${plus1DateString}T18:30:00.000Z`,
+ endTime: `${plus2DateString}T18:29:59.999Z`,
+ timeZone: Timezones["+5:30"],
+ },
+ ctx
+ );
+
+ expect(thisUserAvailability).toHaveTimeSlots(
+ [
+ // `04:00:00.000Z`, // <- This slot should be occupied by the Collective Event
+ `04:45:00.000Z`,
+ `05:30:00.000Z`,
+ `06:15:00.000Z`,
+ `07:00:00.000Z`,
+ `07:45:00.000Z`,
+ `08:30:00.000Z`,
+ `09:15:00.000Z`,
+ `10:00:00.000Z`,
+ `10:45:00.000Z`,
+ `11:30:00.000Z`,
+ `12:15:00.000Z`,
+ ],
+ {
+ dateString: plus2DateString,
+ }
+ );
+ });
});
describe("Team Event", () => {
@@ -826,6 +921,7 @@ describe("getSchedule", () => {
endTime: `${plus2DateString}T05:45:00.000Z`,
},
],
+ hosts: [],
});
const scheduleForTeamEventOnADayWithNoBooking = await getSchedule(
@@ -963,6 +1059,7 @@ describe("getSchedule", () => {
endTime: `${plus3DateString}T04:15:00.000Z`,
},
],
+ hosts: [],
});
const scheduleForTeamEventOnADayWithOneBookingForEachUserButOnDifferentTimeslots = await getSchedule(
{
@@ -1064,9 +1161,10 @@ function addEventTypes(eventTypes: InputEventType[], usersStore: InputUser[]) {
throw new Error(`eventTypes[number]: id ${eventType.id} is not unique`);
}
foundEvents[eventType.id] = true;
- const users = eventType.users.map((userWithJustId) => {
- return usersStore.find((user) => user.id === userWithJustId.id);
- });
+ const users =
+ eventType.users?.map((userWithJustId) => {
+ return usersStore.find((user) => user.id === userWithJustId.id);
+ }) || [];
return {
...baseEventType,
...eventType,
@@ -1099,11 +1197,32 @@ async function addBookings(bookings: InputBooking[], eventTypes: InputEventType[
bookings
// We can improve this filter to support the entire where clause but that isn't necessary yet. So, handle what we know we pass to `findMany` and is needed
.filter((booking) => {
+ /**
+ * A user is considered busy within a given time period if there
+ * is a booking they own OR host. This function mocks some of the logic
+ * for each condition. For details see the following ticket:
+ * https://github.com/calcom/cal.com/issues/6374
+ */
+
+ // ~~ FIRST CONDITION ensures that this booking is owned by this user
+ // and that the status is what we want
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
- const statusIn = where.status?.in || [];
- // Return bookings passing status prisma where
- return statusIn.includes(booking.status) && booking.userId === where.userId;
+ const statusIn = where.OR[0].status?.in || [];
+ const firstConditionMatches =
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ statusIn.includes(booking.status) && booking.userId === where.OR[0].userId;
+
+ // ~~ SECOND CONDITION checks whether this user is a host of this Event Type
+ // and that booking.status is a match for the returned query
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ let secondConditionMatches = where.OR[1].eventTypeId.in.includes(booking.eventTypeId);
+ secondConditionMatches = secondConditionMatches && statusIn.includes(booking.status);
+
+ // We return this booking if either condition is met
+ return firstConditionMatches || secondConditionMatches;
})
.map((booking) => ({
uid: uuidv4(),
@@ -1116,6 +1235,10 @@ async function addBookings(bookings: InputBooking[], eventTypes: InputEventType[
});
}
+function addHosts(hosts: InputHost[]) {
+ prismaMock.host.findMany.mockResolvedValue(hosts);
+}
+
function addUsers(users: InputUser[]) {
prismaMock.user.findMany.mockResolvedValue(
users.map((user) => {
@@ -1131,6 +1254,7 @@ type ScenarioData = {
// TODO: Support multiple bookings and add tests with that.
bookings?: InputBooking[];
users: InputUser[];
+ hosts: InputHost[];
credentials?: InputCredential[];
apps?: App[];
selectedCalendars?: InputSelectedCalendar[];
@@ -1146,6 +1270,8 @@ function createBookingScenario(data: ScenarioData) {
addUsers(data.users);
+ addHosts(data.hosts);
+
const eventType = addEventTypes(data.eventTypes, data.users);
if (data.apps) {
prismaMock.app.findMany.mockResolvedValue(data.apps as PrismaApp[]);
diff --git a/packages/app-store/ee/routing-forms/components/FormInputFields.tsx b/packages/app-store/ee/routing-forms/components/FormInputFields.tsx
index f05547fac0..bae6fc581e 100644
--- a/packages/app-store/ee/routing-forms/components/FormInputFields.tsx
+++ b/packages/app-store/ee/routing-forms/components/FormInputFields.tsx
@@ -50,6 +50,7 @@ export default function FormInputFields(props: Props) {
setUserChangedIdentifier(e.target.value)}
/>
+
+
+
- bookings.map(({ startTime, endTime, title, id, eventType }) => ({
- start: dayjs(startTime)
- .subtract((eventType?.beforeEventBuffer || 0) + (afterEventBuffer || 0), "minute")
- .toDate(),
- end: dayjs(endTime)
- .add((eventType?.afterEventBuffer || 0) + (beforeEventBuffer || 0), "minute")
- .toDate(),
- title,
- source: `eventType-${eventType?.id}-booking-${id}`,
- }))
- );
+ select: {
+ eventTypeId: true,
+ },
+ })
+
+ // Converting the response object into an array
+ .then((thisUserHostedEvents) => thisUserHostedEvents.map((e) => e.eventTypeId))
+
+ // Finding all bookings owned OR hosted by this user
+ .then((thisUserHostedEventIds) => {
+ // This gets applied to both conditions
+ const sharedQuery = {
+ startTime: { gte: new Date(startTime) },
+ endTime: { lte: new Date(endTime) },
+ status: {
+ in: [BookingStatus.ACCEPTED],
+ },
+ };
+
+ return prisma.booking.findMany({
+ where: {
+ OR: [
+ // Bookings owned by this user
+ {
+ ...sharedQuery,
+ userId,
+ },
+
+ // Bookings with an EventType ID that's hosted by this user
+ {
+ ...sharedQuery,
+ eventTypeId: {
+ in: thisUserHostedEventIds,
+ },
+ },
+ ],
+ },
+ select: {
+ id: true,
+ startTime: true,
+ endTime: true,
+ title: true,
+ eventType: {
+ select: {
+ id: true,
+ afterEventBuffer: true,
+ beforeEventBuffer: true,
+ },
+ },
+ },
+ });
+ })
+ .then((bookings) =>
+ bookings.map(({ startTime, endTime, title, id, eventType }) => ({
+ start: dayjs(startTime)
+ .subtract((eventType?.beforeEventBuffer || 0) + (afterEventBuffer || 0), "minute")
+ .toDate(),
+ end: dayjs(endTime)
+ .add((eventType?.afterEventBuffer || 0) + (beforeEventBuffer || 0), "minute")
+ .toDate(),
+ title,
+ source: `eventType-${eventType?.id}-booking-${id}`,
+ }))
+ );
logger.silly(`Busy Time from Cal Bookings ${JSON.stringify(busyTimes)}`);
performance.mark("prismaBookingGetEnd");
performance.measure(`prisma booking get took $1'`, "prismaBookingGetStart", "prismaBookingGetEnd");
diff --git a/packages/core/getUserAvailability.ts b/packages/core/getUserAvailability.ts
index 02de43a249..79b1fd400a 100644
--- a/packages/core/getUserAvailability.ts
+++ b/packages/core/getUserAvailability.ts
@@ -231,7 +231,7 @@ export async function getUserAvailability(
? { ...eventType?.schedule }
: {
...userSchedule,
- availability: userSchedule.availability.map((a) => ({
+ availability: userSchedule?.availability.map((a) => ({
...a,
userId: currentUser.id,
})),
diff --git a/packages/features/ee/README.md b/packages/features/ee/README.md
index d796cb266c..de4eefc9fb 100644
--- a/packages/features/ee/README.md
+++ b/packages/features/ee/README.md
@@ -4,16 +4,16 @@
- Get Started with Enterprise
+ Get a License Key
# Enterprise Edition
Welcome to the Enterprise Edition ("/ee") of Cal.com.
-The [/ee](https://github.com/calcom/cal.com/tree/main/packages/features/ee) subfolder is the place for all the **Pro** features from our [hosted](https://cal.com/pricing) plan and [enterprise-grade](https://cal.com/enterprise) features such as SSO, SAML, ADFS, OIDC, SCIM, SIEM, HRIS and much more.
+The [/ee](https://github.com/calcom/cal.com/tree/main/packages/features/ee) subfolder is the place for all the **Enterprise Edition** features from our [hosted](https://cal.com/pricing) plan and enterprise-grade features for [Ultimate](https://cal.com/ultimate) such as SSO, SAML, OIDC, SCIM, SIEM and much more or [Platform](https://cal.com/platform) plan to build a marketplace.
-> _❗ WARNING: This repository is copyrighted (unlike our [main repo](https://github.com/calcom/cal.com)). You are not allowed to use this code to host your own version of app.cal.com without obtaining a proper [license](https://cal.com/pricing?infra) first❗_
+> _❗ WARNING: This repository is copyrighted (unlike our [main repo](https://github.com/calcom/cal.com)). You are not allowed to use this code to host your own version of app.cal.com without obtaining a proper [license](https://console.cal.com/) first❗_
## Setting up Stripe
diff --git a/packages/features/ee/payments/pages/payment.tsx b/packages/features/ee/payments/pages/payment.tsx
index e4513f9041..fc2839700d 100644
--- a/packages/features/ee/payments/pages/payment.tsx
+++ b/packages/features/ee/payments/pages/payment.tsx
@@ -8,6 +8,8 @@ import type { inferSSRProps } from "@calcom/types/inferSSRProps";
import { ssrInit } from "@server/lib/ssr";
+import { BookingStatus } from ".prisma/client";
+
export type PaymentPageProps = inferSSRProps
;
const querySchema = z.object({
@@ -44,6 +46,9 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
},
eventTypeId: true,
location: true,
+ status: true,
+ rejectionReason: true,
+ cancellationReason: true,
eventType: {
select: {
id: true,
@@ -104,6 +109,19 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
hideBranding: eventType.team?.hideBranding || user?.hideBranding || null,
};
+ if (
+ ([BookingStatus.CANCELLED, BookingStatus.REJECTED] as BookingStatus[]).includes(
+ booking.status as BookingStatus
+ )
+ ) {
+ return {
+ redirect: {
+ destination: `/booking/${booking.uid}`,
+ permanent: false,
+ },
+ };
+ }
+
return {
props: {
user,
diff --git a/packages/features/ee/workflows/lib/reminders/emailReminderManager.ts b/packages/features/ee/workflows/lib/reminders/emailReminderManager.ts
index dad511401f..14ffc89358 100644
--- a/packages/features/ee/workflows/lib/reminders/emailReminderManager.ts
+++ b/packages/features/ee/workflows/lib/reminders/emailReminderManager.ts
@@ -204,6 +204,11 @@ export const deleteScheduledEmailReminder = async (referenceId: string) => {
status: "cancel",
},
});
+
+ await client.request({
+ url: `/v3/user/scheduled_sends/${referenceId}`,
+ method: "DELETE",
+ });
} catch (error) {
console.log(`Error canceling reminder with error ${error}`);
}
diff --git a/packages/features/ee/workflows/pages/workflow.tsx b/packages/features/ee/workflows/pages/workflow.tsx
index e36bd61a42..59737a7f83 100644
--- a/packages/features/ee/workflows/pages/workflow.tsx
+++ b/packages/features/ee/workflows/pages/workflow.tsx
@@ -100,7 +100,7 @@ function WorkflowPage() {
data: workflow,
isError,
error,
- dataUpdatedAt,
+ isLoading,
} = trpc.viewer.workflows.get.useQuery(
{ id: +workflowId },
{
@@ -111,7 +111,7 @@ function WorkflowPage() {
const { data: verifiedNumbers } = trpc.viewer.workflows.getVerifiedNumbers.useQuery();
useEffect(() => {
- if (workflow && (workflow.steps.length === 0 || workflow.steps[0].stepNumber === 1)) {
+ if (workflow && !isLoading) {
setSelectedEventTypes(
workflow.activeOn.map((active) => ({
value: String(active.eventType.id),
@@ -155,7 +155,7 @@ function WorkflowPage() {
form.setValue("activeOn", activeOn || []);
setIsAllDataLoaded(true);
}
- }, [dataUpdatedAt]);
+ }, [isLoading]);
const updateMutation = trpc.viewer.workflows.update.useMutation({
onSuccess: async ({ workflow }) => {
diff --git a/packages/features/shell/Shell.tsx b/packages/features/shell/Shell.tsx
index 3d9dc31e8d..58b763f97b 100644
--- a/packages/features/shell/Shell.tsx
+++ b/packages/features/shell/Shell.tsx
@@ -797,13 +797,13 @@ export function ShellMain(props: LayoutProps) {
{props.HeadingLeftIcon && {props.HeadingLeftIcon}
}
{props.heading && (
-
{!isLocaleReady ? : props.heading}
-
+
)}
{props.subtitle && (
diff --git a/packages/features/tips/Tips.tsx b/packages/features/tips/Tips.tsx
index f4dd234d59..b573a4fb90 100644
--- a/packages/features/tips/Tips.tsx
+++ b/packages/features/tips/Tips.tsx
@@ -70,6 +70,14 @@ export const tips = [
description: "Make time work for you and automate tasks",
href: "https://go.cal.com/workflows",
},
+ {
+ id: 9,
+ thumbnailUrl: "https://img.youtube.com/vi/93iOmzHieCU/0.jpg",
+ mediaLink: "https://go.cal.com/round-robin",
+ title: "Round-Robin",
+ description: "Create advanced group meetings with round-robin",
+ href: "https://go.cal.com/round-robin",
+ },
];
export default function Tips() {
diff --git a/packages/features/webhooks/components/WebhookTestDisclosure.tsx b/packages/features/webhooks/components/WebhookTestDisclosure.tsx
index 9744637c32..62bbbe3e33 100644
--- a/packages/features/webhooks/components/WebhookTestDisclosure.tsx
+++ b/packages/features/webhooks/components/WebhookTestDisclosure.tsx
@@ -31,7 +31,7 @@ export default function WebhookTestDisclosure() {
{t("ping_test")}
-
+
{t("webhook_response")}
@@ -45,7 +45,7 @@ export default function WebhookTestDisclosure() {
{!mutation.data &&
{t("no_data_yet")}
}
{mutation.status === "success" && (
-
{JSON.stringify(mutation.data, null, 4)}
+
{JSON.stringify(mutation.data, null, 4)}
)}
diff --git a/packages/trpc/server/routers/viewer/bookings.tsx b/packages/trpc/server/routers/viewer/bookings.tsx
index a06178f083..cd1013ddc0 100644
--- a/packages/trpc/server/routers/viewer/bookings.tsx
+++ b/packages/trpc/server/routers/viewer/bookings.tsx
@@ -10,6 +10,8 @@ import {
Workflow,
WorkflowsOnEventTypes,
WorkflowStep,
+ PrismaPromise,
+ WorkflowMethods,
} from "@prisma/client";
import type { TFunction } from "next-i18next";
import { z } from "zod";
@@ -18,11 +20,14 @@ import appStore from "@calcom/app-store";
import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
import { DailyLocationType } from "@calcom/app-store/locations";
import { scheduleTrigger } from "@calcom/app-store/zapier/lib/nodeScheduler";
+import { cancelScheduledJobs } from "@calcom/app-store/zapier/lib/nodeScheduler";
import EventManager from "@calcom/core/EventManager";
import { CalendarEventBuilder } from "@calcom/core/builders/CalendarEvent/builder";
import { CalendarEventDirector } from "@calcom/core/builders/CalendarEvent/director";
import { deleteMeeting } from "@calcom/core/videoClient";
import dayjs from "@calcom/dayjs";
+import { deleteScheduledEmailReminder } from "@calcom/ee/workflows/lib/reminders/emailReminderManager";
+import { deleteScheduledSMSReminder } from "@calcom/ee/workflows/lib/reminders/smsReminderManager";
import {
sendDeclinedEmails,
sendLocationChangeEmails,
@@ -405,6 +410,8 @@ export const bookingsRouter = router({
dynamicGroupSlugRef: true,
destinationCalendar: true,
smsReminderNumber: true,
+ scheduledJobs: true,
+ workflowReminders: true,
},
where: {
uid: bookingId,
@@ -472,6 +479,30 @@ export const bookingsRouter = router({
},
});
+ // delete scheduled jobs of cancelled bookings
+ cancelScheduledJobs(bookingToReschedule);
+
+ //cancel workflow reminders
+ const remindersToDelete: PrismaPromise
[] = [];
+
+ bookingToReschedule.workflowReminders.forEach((reminder) => {
+ if (reminder.scheduled && reminder.referenceId) {
+ if (reminder.method === WorkflowMethods.EMAIL) {
+ deleteScheduledEmailReminder(reminder.referenceId);
+ } else if (reminder.method === WorkflowMethods.SMS) {
+ deleteScheduledSMSReminder(reminder.referenceId);
+ }
+ }
+ const reminderToDelete = prisma.workflowReminder.deleteMany({
+ where: {
+ id: reminder.id,
+ },
+ });
+ remindersToDelete.push(reminderToDelete);
+ });
+
+ await Promise.all(remindersToDelete);
+
const [mainAttendee] = bookingToReschedule.attendees;
// @NOTE: Should we assume attendees language?
const tAttendees = await getTranslation(mainAttendee.locale ?? "en", "common");
@@ -772,12 +803,8 @@ export const bookingsRouter = router({
const isConfirmed = booking.status === BookingStatus.ACCEPTED;
if (isConfirmed) throw new TRPCError({ code: "BAD_REQUEST", message: "Booking already confirmed" });
- /** When a booking that requires payment its being confirmed but doesn't have any payment,
- * we shouldn’t save it on DestinationCalendars
- *
- * FIXME: This can cause unintended confirmations on rejection.
- */
- if (booking.payment.length > 0 && !booking.paid) {
+ // If booking requires payment and is not paid, we don't allow confirmation
+ if (confirmed && booking.payment.length > 0 && !booking.paid) {
await prisma.booking.update({
where: {
id: bookingId,
@@ -789,6 +816,7 @@ export const bookingsRouter = router({
return { message: "Booking confirmed", status: BookingStatus.ACCEPTED };
}
+
const attendeesListPromises = booking.attendees.map(async (attendee) => {
return {
name: attendee.name,
@@ -1083,66 +1111,66 @@ export const bookingsRouter = router({
if (!!booking.payment.length) {
const successPayment = booking.payment.find((payment) => payment.success);
if (!successPayment) {
- throw new Error("Cannot reject a booking without a successful payment");
- }
+ // Disable paymentLink for this booking
+ } else {
+ let eventTypeOwnerId;
+ if (booking.eventType?.owner) {
+ eventTypeOwnerId = booking.eventType.owner.id;
+ } else if (booking.eventType?.teamId) {
+ const teamOwner = await prisma.membership.findFirst({
+ where: {
+ teamId: booking.eventType.teamId,
+ role: MembershipRole.OWNER,
+ },
+ select: {
+ userId: true,
+ },
+ });
+ eventTypeOwnerId = teamOwner?.userId;
+ }
- let eventTypeOwnerId;
- if (booking.eventType?.owner) {
- eventTypeOwnerId = booking.eventType.owner.id;
- } else if (booking.eventType?.teamId) {
- const teamOwner = await prisma.membership.findFirst({
+ if (!eventTypeOwnerId) {
+ throw new Error("Event Type owner not found for obtaining payment app credentials");
+ }
+
+ const paymentAppCredentials = await prisma.credential.findMany({
where: {
- teamId: booking.eventType.teamId,
- role: MembershipRole.OWNER,
+ userId: eventTypeOwnerId,
+ appId: successPayment.appId,
},
select: {
- userId: true,
- },
- });
- eventTypeOwnerId = teamOwner?.userId;
- }
-
- if (!eventTypeOwnerId) {
- throw new Error("Event Type owner not found for obtaining payment app credentials");
- }
-
- const paymentAppCredentials = await prisma.credential.findMany({
- where: {
- userId: eventTypeOwnerId,
- appId: successPayment.appId,
- },
- select: {
- key: true,
- appId: true,
- app: {
- select: {
- categories: true,
- dirName: true,
+ key: true,
+ appId: true,
+ app: {
+ select: {
+ categories: true,
+ dirName: true,
+ },
},
},
- },
- });
+ });
- const paymentAppCredential = paymentAppCredentials.find((credential) => {
- return credential.appId === successPayment.appId;
- });
+ const paymentAppCredential = paymentAppCredentials.find((credential) => {
+ return credential.appId === successPayment.appId;
+ });
- if (!paymentAppCredential) {
- throw new Error("Payment app credentials not found");
- }
+ if (!paymentAppCredential) {
+ throw new Error("Payment app credentials not found");
+ }
- // Posible to refactor TODO:
- const paymentApp = appStore[paymentAppCredential?.app?.dirName as keyof typeof appStore];
- if (!(paymentApp && "lib" in paymentApp && "PaymentService" in paymentApp.lib)) {
- console.warn(`payment App service of type ${paymentApp} is not implemented`);
- return null;
- }
+ // Posible to refactor TODO:
+ const paymentApp = appStore[paymentAppCredential?.app?.dirName as keyof typeof appStore];
+ if (!(paymentApp && "lib" in paymentApp && "PaymentService" in paymentApp.lib)) {
+ console.warn(`payment App service of type ${paymentApp} is not implemented`);
+ return null;
+ }
- const PaymentService = paymentApp.lib.PaymentService;
- const paymentInstance = new PaymentService(paymentAppCredential);
- const paymentData = await paymentInstance.refund(successPayment.id);
- if (!paymentData.refunded) {
- throw new Error("Payment could not be refunded");
+ const PaymentService = paymentApp.lib.PaymentService;
+ const paymentInstance = new PaymentService(paymentAppCredential);
+ const paymentData = await paymentInstance.refund(successPayment.id);
+ if (!paymentData.refunded) {
+ throw new Error("Payment could not be refunded");
+ }
}
}
// end handle refunds.
diff --git a/packages/trpc/server/routers/viewer/workflows.tsx b/packages/trpc/server/routers/viewer/workflows.tsx
index ca65fa4195..b443cdd32f 100644
--- a/packages/trpc/server/routers/viewer/workflows.tsx
+++ b/packages/trpc/server/routers/viewer/workflows.tsx
@@ -877,7 +877,11 @@ export const workflowsRouter = router({
eventType: true,
},
},
- steps: true,
+ steps: {
+ orderBy: {
+ stepNumber: "asc",
+ },
+ },
},
});
diff --git a/packages/ui/components/avatar/AvatarGroup.tsx b/packages/ui/components/avatar/AvatarGroup.tsx
index 91cdb952d8..b32d3a40ff 100644
--- a/packages/ui/components/avatar/AvatarGroup.tsx
+++ b/packages/ui/components/avatar/AvatarGroup.tsx
@@ -16,48 +16,45 @@ export type AvatarGroupProps = {
};
export const AvatarGroup = function AvatarGroup(props: AvatarGroupProps) {
- const avatars = props.items.slice(0, 4);
const LENGTH = props.items.length;
const truncateAfter = props.truncateAfter || 4;
+ /**
+ * First, filter all the avatars object that have image
+ * Then, slice it until before `truncateAfter` index
+ */
+ const displayedAvatars = props.items.filter((avatar) => avatar.image).slice(0, truncateAfter);
+ const numTruncatedAvatars = LENGTH - displayedAvatars.length;
return (
);
};
diff --git a/packages/ui/components/logo/Logo.tsx b/packages/ui/components/logo/Logo.tsx
index 7733fff725..7ec2664462 100644
--- a/packages/ui/components/logo/Logo.tsx
+++ b/packages/ui/components/logo/Logo.tsx
@@ -2,7 +2,7 @@ import { LOGO_ICON, LOGO } from "@calcom/lib/constants";
export default function Logo({ small, icon }: { small?: boolean; icon?: boolean }) {
return (
-
+
{icon ? (
@@ -10,6 +10,6 @@ export default function Logo({ small, icon }: { small?: boolean; icon?: boolean
)}
-
+
);
}