Compare commits

...

2 Commits

Author SHA1 Message Date
Peer Richelsen 56c2c81def
Merge branch 'main' into revert-11594-revert-10882-fix#10789 2023-10-12 00:16:12 +01:00
Omar López 048912799e Revert "Revert "fix: #10789 Prevent Double Booking (for requests that require confirmation)" (#11594)"
This reverts commit 9c448e3b87.
2023-09-28 12:31:47 -07:00
3 changed files with 69 additions and 2 deletions

View File

@ -73,8 +73,10 @@ function BookingListItem(booking: BookingItemProps) {
} }
utils.viewer.bookings.invalidate(); utils.viewer.bookings.invalidate();
}, },
onError: () => { onError: (e) => {
showToast(t("booking_confirmation_failed"), "error"); let message = t("booking_confirmation_failed");
if ("message" in e) message = e.message;
showToast(message, "error");
utils.viewer.bookings.invalidate(); utils.viewer.bookings.invalidate();
}, },
}); });

View File

@ -137,6 +137,8 @@ test.describe("pro user", () => {
page.click('[data-testid="confirm"]'), page.click('[data-testid="confirm"]'),
page.waitForResponse((response) => response.url().includes("/api/trpc/bookings/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 // 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(); await expect(page.locator('[data-testid="empty-screen"]')).toBeVisible();
}); });
@ -227,6 +229,45 @@ test.describe("pro user", () => {
const firstSlotAvailableText = await firstSlotAvailable.innerText(); const firstSlotAvailableText = await firstSlotAvailable.innerText();
expect(firstSlotAvailableText).toContain("9:00"); 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", () => { test.describe("prefill", () => {

View File

@ -7,6 +7,7 @@ import { handleConfirmation } from "@calcom/features/bookings/lib/handleConfirma
import { handleWebhookTrigger } from "@calcom/features/bookings/lib/handleWebhookTrigger"; import { handleWebhookTrigger } from "@calcom/features/bookings/lib/handleWebhookTrigger";
import type { EventTypeInfo } from "@calcom/features/webhooks/lib/sendPayload"; import type { EventTypeInfo } from "@calcom/features/webhooks/lib/sendPayload";
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib"; import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
import { getUsernameList } from "@calcom/lib/defaultEvents";
import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType"; import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType";
import { getTranslation } from "@calcom/lib/server"; import { getTranslation } from "@calcom/lib/server";
import { getUsersCredentials } from "@calcom/lib/server/getUsersCredentials"; 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 { TRPCError } from "@trpc/server";
import type { TrpcSessionUser } from "../../../trpc"; import type { TrpcSessionUser } from "../../../trpc";
import { getAvailableSlots } from "../slots/util";
import type { TConfirmInputSchema } from "./confirm.schema"; import type { TConfirmInputSchema } from "./confirm.schema";
import type { BookingsProcedureContext } from "./util"; import type { BookingsProcedureContext } from "./util";
@ -55,6 +57,7 @@ export const confirmHandler = async ({ ctx, input }: ConfirmOptions) => {
teamId: true, teamId: true,
recurringEvent: true, recurringEvent: true,
title: true, title: true,
slug: true,
requiresConfirmation: true, requiresConfirmation: true,
currency: true, currency: true,
length: true, length: true,
@ -113,6 +116,27 @@ export const confirmHandler = async ({ ctx, input }: ConfirmOptions) => {
throw new TRPCError({ code: "BAD_REQUEST", message: "Booking already confirmed" }); 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 booking requires payment and is not paid, we don't allow confirmation
if (confirmed && booking.payment.length > 0 && !booking.paid) { if (confirmed && booking.payment.length > 0 && !booking.paid) {
await prisma.booking.update({ await prisma.booking.update({