Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: alannnc <alannnc@gmail.com> Co-authored-by: zomars <zomars@me.com>pull/11431/head^2
parent
a2df323cf8
commit
d7c3132fa5
|
@ -26,11 +26,11 @@ import {
|
|||
DialogFooter,
|
||||
MeetingTimeInTimezones,
|
||||
showToast,
|
||||
Tooltip,
|
||||
TableActions,
|
||||
TextAreaField,
|
||||
Tooltip,
|
||||
} from "@calcom/ui";
|
||||
import { Check, Clock, MapPin, RefreshCcw, Send, Ban, X, CreditCard } from "@calcom/ui/components/icon";
|
||||
import { Ban, Check, Clock, CreditCard, MapPin, RefreshCcw, Send, X } from "@calcom/ui/components/icon";
|
||||
|
||||
import useMeQuery from "@lib/hooks/useMeQuery";
|
||||
|
||||
|
@ -74,8 +74,10 @@ function BookingListItem(booking: BookingItemProps) {
|
|||
}
|
||||
utils.viewer.bookings.invalidate();
|
||||
},
|
||||
onError: () => {
|
||||
showToast(t("booking_confirmation_failed"), "error");
|
||||
onError: (e) => {
|
||||
let message = t("booking_confirmation_failed");
|
||||
if ("message" in e) message = e.message;
|
||||
showToast(message, "error");
|
||||
utils.viewer.bookings.invalidate();
|
||||
},
|
||||
});
|
||||
|
|
|
@ -137,6 +137,8 @@ test.describe("pro user", () => {
|
|||
page.click('[data-testid="confirm"]'),
|
||||
page.waitForResponse((response) => response.url().includes("/api/trpc/bookings/confirm")),
|
||||
]);
|
||||
|
||||
await page.goto("/bookings/unconfirmed");
|
||||
// This is the only booking in there that needed confirmation and now it should be empty screen
|
||||
await expect(page.locator('[data-testid="empty-screen"]')).toBeVisible();
|
||||
});
|
||||
|
@ -227,6 +229,45 @@ test.describe("pro user", () => {
|
|||
const firstSlotAvailableText = await firstSlotAvailable.innerText();
|
||||
expect(firstSlotAvailableText).toContain("9:00");
|
||||
});
|
||||
|
||||
test("Cannot confirm booking for a slot, if another confirmed booking already exists for same slot.", async ({
|
||||
page,
|
||||
users,
|
||||
}) => {
|
||||
// First booking done for first available time slot in next month
|
||||
await bookOptinEvent(page);
|
||||
|
||||
const [pro] = users.get();
|
||||
await page.goto(`/${pro.username}`);
|
||||
|
||||
// Second booking done for same time slot
|
||||
await bookOptinEvent(page);
|
||||
|
||||
await pro.apiLogin();
|
||||
|
||||
await page.goto("/bookings/unconfirmed");
|
||||
|
||||
// Confirm first booking
|
||||
await Promise.all([
|
||||
page.click('[data-testid="confirm"]'),
|
||||
page.waitForResponse((response) => response.url().includes("/api/trpc/bookings/confirm")),
|
||||
]);
|
||||
|
||||
await Promise.all([
|
||||
page.goto("/bookings/unconfirmed"),
|
||||
page.waitForResponse((response) => response.url().includes("/api/trpc/bookings/get")),
|
||||
]);
|
||||
|
||||
// Confirm second booking
|
||||
await page.click('[data-testid="confirm"]');
|
||||
const response = await page.waitForResponse(
|
||||
(response) => response.url().includes("/api/trpc/bookings/confirm") && response.status() !== 200
|
||||
);
|
||||
const responseObj = await response.json();
|
||||
|
||||
expect(responseObj[0]?.error?.json?.data?.code).toEqual("BAD_REQUEST");
|
||||
expect(responseObj[0]?.error?.json?.message).toEqual("Slot already confirmed for other booking");
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("prefill", () => {
|
||||
|
|
|
@ -7,6 +7,7 @@ import { handleConfirmation } from "@calcom/features/bookings/lib/handleConfirma
|
|||
import { handleWebhookTrigger } from "@calcom/features/bookings/lib/handleWebhookTrigger";
|
||||
import type { EventTypeInfo } from "@calcom/features/webhooks/lib/sendPayload";
|
||||
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
|
||||
import { getUsernameList } from "@calcom/lib/defaultEvents";
|
||||
import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType";
|
||||
import { getTranslation } from "@calcom/lib/server";
|
||||
import { getUsersCredentials } from "@calcom/lib/server/getUsersCredentials";
|
||||
|
@ -19,6 +20,7 @@ import type { IAbstractPaymentService, PaymentApp } from "@calcom/types/PaymentS
|
|||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import type { TrpcSessionUser } from "../../../trpc";
|
||||
import { getAvailableSlots } from "../slots/util";
|
||||
import type { TConfirmInputSchema } from "./confirm.schema";
|
||||
import type { BookingsProcedureContext } from "./util";
|
||||
|
||||
|
@ -55,6 +57,7 @@ export const confirmHandler = async ({ ctx, input }: ConfirmOptions) => {
|
|||
teamId: true,
|
||||
recurringEvent: true,
|
||||
title: true,
|
||||
slug: true,
|
||||
requiresConfirmation: true,
|
||||
currency: true,
|
||||
length: true,
|
||||
|
@ -113,6 +116,27 @@ export const confirmHandler = async ({ ctx, input }: ConfirmOptions) => {
|
|||
throw new TRPCError({ code: "BAD_REQUEST", message: "Booking already confirmed" });
|
||||
}
|
||||
|
||||
// Check for user's slot availability for current booking start and end time
|
||||
if (confirmed) {
|
||||
const slotsAvailable = await getAvailableSlots({
|
||||
input: {
|
||||
startTime: booking?.startTime.toISOString(),
|
||||
endTime: booking?.endTime.toISOString(),
|
||||
eventTypeId: booking?.eventType?.id,
|
||||
eventTypeSlug: booking?.eventType?.slug,
|
||||
timeZone: user?.timeZone,
|
||||
usernameList: getUsernameList(user?.username ?? ""),
|
||||
isTeamEvent: !!booking?.eventType?.teamId || false,
|
||||
},
|
||||
});
|
||||
|
||||
// If no free slot available with current booking request's start time and
|
||||
// end time, then the slot is booked for other booking ,throw error
|
||||
if (slotsAvailable?.slots && Object.keys(slotsAvailable.slots).length === 0) {
|
||||
throw new TRPCError({ code: "BAD_REQUEST", message: "Slot already confirmed for other booking" });
|
||||
}
|
||||
}
|
||||
|
||||
// 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({
|
||||
|
|
Loading…
Reference in New Issue