* feat: Cal.diy — community-driven MIT-licensed fork of Cal.com This squashed commit contains all Cal.diy changes applied on top of calcom/cal.com main: - Rebrand Cal.com to Cal.diy across the entire codebase - Remove Enterprise Edition (EE) features, license checks, and AGPL restrictions - Switch license from AGPL-3.0 to MIT - Remove docs/ directory (migrated to Nextra at cal.diy) - Remove dead code: org tests, EE tips, platform nav, premium username, SAML/SSO, etc. - Clean up .env.example for self-hosted Cal.diy - Update Docker image references to calcom/cal.diy - Update README, CONTRIBUTING.md, and issue templates for Cal.diy community fork - Add PR welcome bot for Cal.diy contributors - Fix API v2 breaking changes oasdiff ignore entries - Replace Blacksmith CI runners with default GitHub Actions 3893 files changed, 20789 insertions(+), 411020 deletions(-) Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * refactor: remove org-specific /organizations/:orgId endpoints from API v2 atoms controllers (#1701) Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix: revert Cal.diy Inc to Cal.com, Inc. in license files, copyright notices, and package metadata (#1702) Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * rip out org related comments in api v2 --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
274 lines
8.4 KiB
TypeScript
274 lines
8.4 KiB
TypeScript
import dayjs from "@calcom/dayjs";
|
|
import { isPrismaObjOrUndefined } from "@calcom/lib/isPrismaObj";
|
|
import { withReporting } from "@calcom/lib/sentryWrapper";
|
|
import prisma from "@calcom/prisma";
|
|
import { Prisma } from "@calcom/prisma/client";
|
|
import type { CreationSource } from "@calcom/prisma/enums";
|
|
import { BookingStatus } from "@calcom/prisma/enums";
|
|
import type { CalendarEvent } from "@calcom/types/Calendar";
|
|
import type short from "short-uuid";
|
|
import type { TgetBookingDataSchema } from "../getBookingDataSchema";
|
|
import type { AwaitedBookingData, EventTypeId } from "./getBookingData";
|
|
import type { NewBookingEventType } from "./getEventTypesFromDB";
|
|
import type { LoadedUsers } from "./loadUsers";
|
|
import type { OriginalRescheduledBooking } from "./originalRescheduledBookingUtils";
|
|
import type { PaymentAppData, Tracking } from "./types";
|
|
|
|
type ReqBodyWithEnd = TgetBookingDataSchema & { end: string };
|
|
|
|
type CreateBookingParams = {
|
|
uid: short.SUUID;
|
|
rescheduledBy: string | undefined;
|
|
reqBody: {
|
|
user: ReqBodyWithEnd["user"];
|
|
metadata: ReqBodyWithEnd["metadata"];
|
|
recurringEventId: ReqBodyWithEnd["recurringEventId"];
|
|
};
|
|
eventType: {
|
|
eventTypeData: NewBookingEventType;
|
|
id: EventTypeId;
|
|
slug: AwaitedBookingData["eventTypeSlug"];
|
|
organizerUser: LoadedUsers[number] & {
|
|
isFixed?: boolean;
|
|
metadata?: Prisma.JsonValue;
|
|
};
|
|
isConfirmedByDefault: boolean;
|
|
paymentAppData: PaymentAppData;
|
|
};
|
|
input: {
|
|
bookerEmail: AwaitedBookingData["email"];
|
|
rescheduleReason: AwaitedBookingData["rescheduleReason"];
|
|
smsReminderNumber: AwaitedBookingData["smsReminderNumber"];
|
|
responses: ReqBodyWithEnd["responses"] | null;
|
|
};
|
|
evt: CalendarEvent;
|
|
originalRescheduledBooking: OriginalRescheduledBooking;
|
|
creationSource?: CreationSource;
|
|
tracking?: Tracking;
|
|
};
|
|
|
|
function updateEventDetails(
|
|
evt: CalendarEvent,
|
|
originalRescheduledBooking: OriginalRescheduledBooking | null
|
|
) {
|
|
if (originalRescheduledBooking) {
|
|
evt.description = originalRescheduledBooking?.description || evt.description;
|
|
evt.location = evt.location || originalRescheduledBooking?.location;
|
|
}
|
|
}
|
|
|
|
// Define the function with underscore prefix
|
|
const _createBooking = async ({
|
|
uid,
|
|
reqBody,
|
|
eventType,
|
|
input,
|
|
evt,
|
|
originalRescheduledBooking,
|
|
rescheduledBy,
|
|
creationSource,
|
|
tracking,
|
|
}: CreateBookingParams & { rescheduledBy: string | undefined }) => {
|
|
updateEventDetails(evt, originalRescheduledBooking);
|
|
|
|
const bookingAndAssociatedData = buildNewBookingData({
|
|
uid,
|
|
rescheduledBy,
|
|
reqBody,
|
|
eventType,
|
|
input,
|
|
evt,
|
|
originalRescheduledBooking,
|
|
creationSource,
|
|
tracking,
|
|
});
|
|
|
|
return await saveBooking(
|
|
bookingAndAssociatedData,
|
|
originalRescheduledBooking,
|
|
eventType.paymentAppData,
|
|
eventType.organizerUser
|
|
);
|
|
};
|
|
|
|
export const createBooking = withReporting(_createBooking, "createBooking");
|
|
|
|
async function saveBooking(
|
|
bookingAndAssociatedData: ReturnType<typeof buildNewBookingData>,
|
|
originalRescheduledBooking: OriginalRescheduledBooking,
|
|
paymentAppData: PaymentAppData,
|
|
organizerUser: CreateBookingParams["eventType"]["organizerUser"]
|
|
) {
|
|
const { newBookingData, originalBookingUpdateDataForCancellation } = bookingAndAssociatedData;
|
|
const createBookingObj = {
|
|
include: {
|
|
user: {
|
|
select: {
|
|
uuid: true,
|
|
email: true,
|
|
name: true,
|
|
timeZone: true,
|
|
username: true,
|
|
isPlatformManaged: true,
|
|
},
|
|
},
|
|
attendees: true,
|
|
payment: true,
|
|
references: true,
|
|
},
|
|
data: newBookingData,
|
|
};
|
|
|
|
if (originalRescheduledBooking?.paid && originalRescheduledBooking?.payment) {
|
|
const bookingPayment = originalRescheduledBooking.payment.find((payment) => payment.success);
|
|
if (bookingPayment) {
|
|
createBookingObj.data.payment = { connect: { id: bookingPayment.id } };
|
|
}
|
|
}
|
|
|
|
if (typeof paymentAppData.price === "number" && paymentAppData.price > 0) {
|
|
await prisma.credential.findFirstOrThrow({
|
|
where: {
|
|
appId: paymentAppData.appId,
|
|
...(paymentAppData.credentialId ? { id: paymentAppData.credentialId } : { userId: organizerUser.id }),
|
|
},
|
|
select: { id: true },
|
|
});
|
|
}
|
|
|
|
return prisma.$transaction(async (tx) => {
|
|
if (originalBookingUpdateDataForCancellation) {
|
|
await tx.booking.update(originalBookingUpdateDataForCancellation);
|
|
}
|
|
|
|
const booking = await tx.booking.create(createBookingObj);
|
|
|
|
return { ...booking, userUuid: booking.user?.uuid ?? null };
|
|
});
|
|
}
|
|
|
|
function getEventTypeRel(eventTypeId: EventTypeId) {
|
|
return eventTypeId ? { connect: { id: eventTypeId } } : {};
|
|
}
|
|
|
|
function getAttendeesData(evt: Pick<CalendarEvent, "attendees" | "team">) {
|
|
//if attendee is team member, it should fetch their locale not booker's locale
|
|
//perhaps make email fetch request to see if his locale is stored, else
|
|
const teamMembers = evt?.team?.members ?? [];
|
|
|
|
return evt.attendees.concat(teamMembers).map((attendee) => ({
|
|
name: attendee.name,
|
|
email: attendee.email,
|
|
timeZone: attendee.timeZone,
|
|
locale: attendee.language.locale,
|
|
phoneNumber: attendee.phoneNumber,
|
|
}));
|
|
}
|
|
|
|
function buildNewBookingData(params: CreateBookingParams) {
|
|
const {
|
|
uid,
|
|
evt,
|
|
reqBody,
|
|
eventType,
|
|
input,
|
|
originalRescheduledBooking,
|
|
rescheduledBy,
|
|
creationSource,
|
|
tracking,
|
|
} = params;
|
|
|
|
const attendeesData = getAttendeesData(evt);
|
|
const eventTypeRel = getEventTypeRel(eventType.id);
|
|
const newBookingData: Prisma.BookingCreateInput = {
|
|
uid,
|
|
userPrimaryEmail: evt.organizer.email,
|
|
responses: input.responses === null || evt.seatsPerTimeSlot ? Prisma.JsonNull : input.responses,
|
|
title: evt.title,
|
|
startTime: dayjs.utc(evt.startTime).toDate(),
|
|
endTime: dayjs.utc(evt.endTime).toDate(),
|
|
description: evt.seatsPerTimeSlot ? null : evt.additionalNotes,
|
|
customInputs: isPrismaObjOrUndefined(evt.customInputs),
|
|
status: eventType.isConfirmedByDefault ? BookingStatus.ACCEPTED : BookingStatus.PENDING,
|
|
oneTimePassword: evt.oneTimePassword,
|
|
location: evt.location,
|
|
eventType: eventTypeRel,
|
|
smsReminderNumber: input.smsReminderNumber,
|
|
metadata: reqBody.metadata,
|
|
attendees: {
|
|
createMany: {
|
|
data: attendeesData,
|
|
},
|
|
},
|
|
dynamicEventSlugRef: !eventType.id ? eventType.slug : null,
|
|
dynamicGroupSlugRef: !eventType.id ? (reqBody.user as string).toLowerCase() : null,
|
|
iCalUID: evt.iCalUID ?? "",
|
|
iCalSequence: originalRescheduledBooking ? evt.iCalSequence || 1 : 0,
|
|
user: {
|
|
connect: {
|
|
id: eventType.organizerUser.id,
|
|
},
|
|
},
|
|
destinationCalendar:
|
|
evt.destinationCalendar && evt.destinationCalendar.length > 0
|
|
? {
|
|
connect: { id: evt.destinationCalendar[0].id },
|
|
}
|
|
: undefined,
|
|
creationSource,
|
|
tracking: tracking ? { create: tracking } : undefined,
|
|
};
|
|
|
|
if (reqBody.recurringEventId) {
|
|
newBookingData.recurringEventId = reqBody.recurringEventId;
|
|
}
|
|
|
|
let originalBookingUpdateDataForCancellation: Prisma.BookingUpdateArgs | undefined;
|
|
|
|
if (originalRescheduledBooking) {
|
|
newBookingData.metadata = {
|
|
...(typeof originalRescheduledBooking.metadata === "object" && originalRescheduledBooking.metadata),
|
|
...reqBody.metadata,
|
|
};
|
|
newBookingData.paid = originalRescheduledBooking.paid;
|
|
newBookingData.fromReschedule = originalRescheduledBooking.uid;
|
|
if (originalRescheduledBooking.uid) {
|
|
newBookingData.cancellationReason = input.rescheduleReason;
|
|
}
|
|
// Reschedule logic with booking with seats
|
|
if (
|
|
newBookingData.attendees?.createMany?.data &&
|
|
eventType?.eventTypeData?.seatsPerTimeSlot &&
|
|
input.bookerEmail
|
|
) {
|
|
newBookingData.attendees.createMany.data = attendeesData.filter(
|
|
(attendee) => attendee.email === input.bookerEmail
|
|
);
|
|
}
|
|
|
|
if (originalRescheduledBooking.recurringEventId) {
|
|
newBookingData.recurringEventId = originalRescheduledBooking.recurringEventId;
|
|
}
|
|
|
|
if (!evt.seatsPerTimeSlot && originalRescheduledBooking?.uid) {
|
|
originalBookingUpdateDataForCancellation = {
|
|
where: {
|
|
id: originalRescheduledBooking.id,
|
|
},
|
|
data: {
|
|
rescheduled: true,
|
|
status: BookingStatus.CANCELLED,
|
|
rescheduledBy: rescheduledBy,
|
|
},
|
|
};
|
|
}
|
|
}
|
|
|
|
return {
|
|
newBookingData,
|
|
originalBookingUpdateDataForCancellation,
|
|
};
|
|
}
|
|
|
|
export type Booking = Awaited<ReturnType<typeof createBooking>>;
|