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) Cal.com - The open source Calendly alternative | Product Hunt Cal.com - The open source Calendly alternative | Product Hunt Cal.com - The open source Calendly alternative | Product Hunt @@ -340,7 +339,18 @@ Please see our [contributing guide](/CONTRIBUTING.md). ### Good First Issues We have a list of [help wanted](https://github.com/calcom/cal.com/issues?q=is:issue+is:open+label:%22%F0%9F%99%8B%F0%9F%8F%BB%E2%80%8D%E2%99%82%EF%B8%8Fhelp+wanted%22) that contain small features and bugs which have a relatively limited scope. This is a great place to get started, gain experience, and get familiar with our contribution process. + + +### Bounties + + + + + Bounties of cal + + + diff --git a/apps/web/pages/booking/[uid].tsx b/apps/web/pages/booking/[uid].tsx index 9f0dec5cef..e68634a092 100644 --- a/apps/web/pages/booking/[uid].tsx +++ b/apps/web/pages/booking/[uid].tsx @@ -209,7 +209,7 @@ export default function Success(props: SuccessProps) { } const status = props.bookingInfo?.status; const reschedule = props.bookingInfo.status === BookingStatus.ACCEPTED; - const cancellationReason = props.bookingInfo.cancellationReason; + const cancellationReason = props.bookingInfo.cancellationReason || props.bookingInfo.rejectionReason; const attendeeName = typeof props?.bookingInfo?.attendees?.[0]?.name === "string" @@ -440,6 +440,17 @@ export default function Success(props: SuccessProps) {

{getTitle()}

+ {props.paymentStatus && ( +

+ {!props.paymentStatus.success && + !props.paymentStatus.refunded && + t("booking_with_payment_cancelled")} + {props.paymentStatus.success && + !props.paymentStatus.refunded && + t("booking_with_payment_cancelled_already_paid")} + {props.paymentStatus.refunded && t("booking_with_payment_cancelled_refunded")} +

+ )}
{(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 @@ Logo - 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 (
    - {avatars.map((item, enumerator) => { - if (item.image != null) { - if (LENGTH > truncateAfter && enumerator === truncateAfter - 1) { - return ( -
  • -
    -
    - -
    -
    -
    - +{LENGTH - truncateAfter - 1} -
    -
  • - ); - } - // Always display the first Four items items - return ( -
  • - -
  • - ); - } - })} + {displayedAvatars.map((item, idx) => ( +
  • + +
  • + ))} + {numTruncatedAvatars > 0 && ( +
  • + + +{numTruncatedAvatars} + +
  • + )}
); }; 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 ? ( Cal @@ -10,6 +10,6 @@ export default function Logo({ small, icon }: { small?: boolean; icon?: boolean Cal )} -

+ ); }