Files
Benny JooandGitHub cb7844fd22 refactor: Migrate repositories/services from /lib to /features (#25925)
* deployment

* update imports

* booking report

* update import paths

* watch list

* watch list

* api key

* api key

* selected slots

* wip

* event type translation

* work flow step

* booking reference

* fix tests

* fix

* fix

* migrate

* wip

* address

* fix
2025-12-17 14:14:50 +00:00

127 lines
4.3 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { serialize } from "cookie";
import type { NextApiRequest, NextApiResponse } from "next";
import { v4 as uuid } from "uuid";
import dayjs from "@calcom/dayjs";
import { PrismaSelectedSlotRepository } from "@calcom/features/selectedSlots/repositories/PrismaSelectedSlotRepository";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { MINUTES_TO_BOOK } from "@calcom/lib/constants";
import type { PrismaClient } from "@calcom/prisma";
import { BookingStatus } from "@calcom/prisma/enums";
import { TRPCError } from "@trpc/server";
import type { TReserveSlotInputSchema } from "./reserveSlot.schema";
interface ReserveSlotOptions {
ctx: {
prisma: PrismaClient;
req?: NextApiRequest | undefined;
res?: NextApiResponse | undefined;
};
input: TReserveSlotInputSchema;
}
export const reserveSlotHandler = async ({ ctx, input }: ReserveSlotOptions) => {
const { prisma, req, res } = ctx;
const uid = req?.cookies?.uid || uuid();
const { slotUtcStartDate, slotUtcEndDate, eventTypeId, _isDryRun } = input;
const releaseAt = dayjs.utc().add(parseInt(MINUTES_TO_BOOK), "minutes").format();
const eventType = await prisma.eventType.findUnique({
where: { id: eventTypeId },
select: { users: { select: { id: true } }, seatsPerTimeSlot: true },
});
if (!eventType) {
throw new TRPCError({
message: "Event type not found",
code: "NOT_FOUND",
});
}
let shouldReserveSlot = true;
// If this is a seated event then don't reserve a slot
if (eventType.seatsPerTimeSlot) {
// Check to see if this is the last attendee
const bookingWithAttendees = await prisma.booking.findFirst({
where: {
eventTypeId,
startTime: slotUtcStartDate,
endTime: slotUtcEndDate,
status: BookingStatus.ACCEPTED,
},
select: { attendees: true },
});
const bookingAttendeesLength = bookingWithAttendees?.attendees?.length;
if (bookingAttendeesLength) {
const seatsLeft = eventType.seatsPerTimeSlot - bookingAttendeesLength;
if (seatsLeft < 1) shouldReserveSlot = false;
} else {
// If there is no booking yet then don't reserve the slot
shouldReserveSlot = false;
}
}
// Check for existing reservations for the same slot
const slotsRepo = new PrismaSelectedSlotRepository(prisma);
const reservedBySomeoneElse = await slotsRepo.findReservedByOthers({
slot: {
utcStartIso: slotUtcStartDate,
utcEndIso: slotUtcEndDate,
},
eventTypeId,
uid,
});
if (eventType && shouldReserveSlot && !reservedBySomeoneElse && !_isDryRun) {
try {
await Promise.all(
// FIXME: In case of team event, users doesn't have assignees, those are in hosts. users just have the creator of the event which is wrong.
// Also, we must not block all the users' slots, we must use routedTeamMemberIds if set like we do in getSchedule.
// We could even improve it by identifying the next person being booked now that we have a queue of assignees.
eventType.users.map((user) =>
prisma.selectedSlots.upsert({
where: { selectedSlotUnique: { userId: user.id, slotUtcStartDate, slotUtcEndDate, uid } },
update: {
slotUtcStartDate,
slotUtcEndDate,
releaseAt,
eventTypeId,
},
create: {
userId: user.id,
eventTypeId,
slotUtcStartDate,
slotUtcEndDate,
uid,
releaseAt,
isSeat: eventType.seatsPerTimeSlot !== null,
},
})
)
);
} catch {
throw new TRPCError({
message: "Event type not found",
code: "NOT_FOUND",
});
}
}
// We need this cookie to be accessible from embeds where the booking flow is displayed within an iframe on a different origin.
// For third‑party iframe contexts (embeds on other sites), browsers require SameSite=None and Secure to make the cookie available.
// For local development on http://localhost we fall back to SameSite=Lax to avoid requiring https during development.
const useSecureCookies = WEBAPP_URL.startsWith("https://");
res?.setHeader(
"Set-Cookie",
serialize("uid", uid, {
path: "/",
sameSite: useSecureCookies ? "none" : "lax",
secure: useSecureCookies,
})
);
return {
uid: uid,
};
};