diff --git a/apps/api/test/lib/bookings/_post.test.ts b/apps/api/test/lib/bookings/_post.test.ts index a6a308c6f8..64abddcfe3 100644 --- a/apps/api/test/lib/bookings/_post.test.ts +++ b/apps/api/test/lib/bookings/_post.test.ts @@ -8,6 +8,7 @@ import { describe, expect, test, vi } from "vitest"; import dayjs from "@calcom/dayjs"; import sendPayload from "@calcom/features/webhooks/lib/sendPayload"; +import { ErrorCode } from "@calcom/lib/errorCodes"; import { buildBooking, buildEventType, buildWebhook } from "@calcom/lib/test/builder"; import prisma from "@calcom/prisma"; @@ -148,7 +149,7 @@ describe.skipIf(true)("POST /api/bookings", () => { expect(res._getStatusCode()).toBe(500); expect(JSON.parse(res._getData())).toEqual( expect.objectContaining({ - message: "No available users found.", + message: ErrorCode.NoAvailableUsersFound, }) ); }); diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 9c7bd29d8f..94506cb885 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -56,6 +56,15 @@ "a_refund_failed": "A refund failed", "awaiting_payment_subject": "Awaiting Payment: {{title}} on {{date}}", "meeting_awaiting_payment": "Your meeting is awaiting payment", + "payment_not_created_error": "Payment could not be created", + "couldnt_charge_card_error": "Could not charge card for Payment", + "no_available_users_found_error": "No available users found. Could you try another time slot?", + "request_body_end_time_internal_error": "Internal Error. Request body does not contain end time", + "create_calendar_event_error": "Unable to create Calendar event in Organizer's calendar", + "update_calendar_event_error": "Unable to update Calendar event.", + "delete_calendar_event_error": "Unable to delete Calendar event.", + "already_signed_up_for_this_booking_error": "You are already signed up for this booking.", + "hosts_unavailable_for_booking": "Some of the hosts are unavailable for booking.", "help": "Help", "price": "Price", "paid": "Paid", @@ -1363,6 +1372,7 @@ "event_name_info": "The event type name", "event_date_info": "The event date", "event_time_info": "The event start time", + "event_type_not_found": "EventType not Found", "location_info": "The location of the event", "additional_notes_info": "The additional notes of booking", "attendee_name_info": "The person booking's name", diff --git a/packages/app-store/alby/lib/PaymentService.ts b/packages/app-store/alby/lib/PaymentService.ts index f008063bd6..9974f1aa25 100644 --- a/packages/app-store/alby/lib/PaymentService.ts +++ b/packages/app-store/alby/lib/PaymentService.ts @@ -3,12 +3,16 @@ import type { Booking, Payment, PaymentOption, Prisma } from "@prisma/client"; import { v4 as uuidv4 } from "uuid"; import type z from "zod"; +import { ErrorCode } from "@calcom/lib/errorCodes"; +import logger from "@calcom/lib/logger"; import prisma from "@calcom/prisma"; import type { CalendarEvent } from "@calcom/types/Calendar"; import type { IAbstractPaymentService } from "@calcom/types/PaymentService"; import { albyCredentialKeysSchema } from "./albyCredentialKeysSchema"; +const log = logger.getSubLogger({ prefix: ["payment-service:alby"] }); + export class PaymentService implements IAbstractPaymentService { private credentials: z.infer | null; @@ -36,7 +40,7 @@ export class PaymentService implements IAbstractPaymentService { }, }); if (!booking || !this.credentials?.account_lightning_address) { - throw new Error(); + throw new Error("Alby: Booking or Lightning address not found"); } const uid = uuidv4(); @@ -80,8 +84,8 @@ export class PaymentService implements IAbstractPaymentService { } return paymentData; } catch (error) { - console.error(error); - throw new Error("Payment could not be created"); + log.error("Alby: Payment could not be created", bookingId); + throw new Error(ErrorCode.PaymentCreationFailure); } } async update(): Promise { diff --git a/packages/app-store/paypal/lib/PaymentService.ts b/packages/app-store/paypal/lib/PaymentService.ts index 4566431acb..9b6d479d09 100644 --- a/packages/app-store/paypal/lib/PaymentService.ts +++ b/packages/app-store/paypal/lib/PaymentService.ts @@ -4,12 +4,16 @@ import z from "zod"; import Paypal from "@calcom/app-store/paypal/lib/Paypal"; import { WEBAPP_URL } from "@calcom/lib/constants"; +import { ErrorCode } from "@calcom/lib/errorCodes"; +import logger from "@calcom/lib/logger"; import prisma from "@calcom/prisma"; import type { CalendarEvent } from "@calcom/types/Calendar"; import type { IAbstractPaymentService } from "@calcom/types/PaymentService"; import { paymentOptionEnum } from "../zod"; +const log = logger.getSubLogger({ prefix: ["payment-service:paypal"] }); + export const paypalCredentialKeysSchema = z.object({ client_id: z.string(), secret_key: z.string(), @@ -87,8 +91,8 @@ export class PaymentService implements IAbstractPaymentService { } return paymentData; } catch (error) { - console.error(error); - throw new Error("Payment could not be created"); + log.error("Paypal: Payment could not be created for bookingId", bookingId); + throw new Error(ErrorCode.PaymentCreationFailure); } } async update(): Promise { @@ -166,8 +170,8 @@ export class PaymentService implements IAbstractPaymentService { } return paymentData; } catch (error) { - console.error(error); - throw new Error("Payment could not be created"); + log.error("Paypal: Payment method could not be collected for bookingId", bookingId); + throw new Error("Paypal: Payment method could not be collected"); } } chargeCard( diff --git a/packages/app-store/stripepayment/lib/PaymentService.ts b/packages/app-store/stripepayment/lib/PaymentService.ts index 559b8a2908..ed8d1bb3e9 100644 --- a/packages/app-store/stripepayment/lib/PaymentService.ts +++ b/packages/app-store/stripepayment/lib/PaymentService.ts @@ -4,7 +4,9 @@ import { v4 as uuidv4 } from "uuid"; import z from "zod"; import { sendAwaitingPaymentEmail } from "@calcom/emails"; +import { ErrorCode } from "@calcom/lib/errorCodes"; import { getErrorFromUnknown } from "@calcom/lib/errors"; +import logger from "@calcom/lib/logger"; import prisma from "@calcom/prisma"; import type { CalendarEvent } from "@calcom/types/Calendar"; import type { IAbstractPaymentService } from "@calcom/types/PaymentService"; @@ -14,6 +16,8 @@ import { createPaymentLink } from "./client"; import { retrieveOrCreateStripeCustomerByEmail } from "./customer"; import type { StripePaymentData, StripeSetupIntentData } from "./server"; +const log = logger.getSubLogger({ prefix: ["payment-service:stripe"] }); + export const stripeCredentialKeysSchema = z.object({ stripe_user_id: z.string(), default_currency: z.string(), @@ -129,7 +133,8 @@ export class PaymentService implements IAbstractPaymentService { return paymentData; } catch (error) { console.error(`Payment could not be created for bookingId ${bookingId}`, error); - throw new Error("Payment could not be created"); + log.error("Stripe: Payment could not be created", bookingId, JSON.stringify(error)); + throw new Error("payment_not_created_error"); } } @@ -199,8 +204,12 @@ export class PaymentService implements IAbstractPaymentService { return paymentData; } catch (error) { - console.error(`Payment method could not be collected for bookingId ${bookingId}`, error); - throw new Error("Payment could not be created"); + log.error( + "Stripe: Payment method could not be collected for bookingId", + bookingId, + JSON.stringify(error) + ); + throw new Error("Stripe: Payment method could not be collected"); } } @@ -277,8 +286,8 @@ export class PaymentService implements IAbstractPaymentService { return paymentData; } catch (error) { - console.error(`Could not charge card for payment ${payment.id}`, error); - throw new Error("Payment could not be created"); + log.error("Stripe: Could not charge card for payment", _bookingId, JSON.stringify(error)); + throw new Error(ErrorCode.ChargeCardFailure); } } @@ -369,7 +378,7 @@ export class PaymentService implements IAbstractPaymentService { await this.stripe.paymentIntents.cancel(payment.externalId, { stripeAccount }); return true; } catch (e) { - console.error(e); + log.error("Stripe: Unable to delete Payment in stripe of paymentId", paymentId, JSON.stringify(e)); return false; } } diff --git a/packages/core/CalendarManager.ts b/packages/core/CalendarManager.ts index 5d9f0252ee..7648d3c015 100644 --- a/packages/core/CalendarManager.ts +++ b/packages/core/CalendarManager.ts @@ -206,7 +206,7 @@ export const getBusyCalendarTimes = async ( selectedCalendars: SelectedCalendar[] ) => { let results: EventBusyDate[][] = []; - const months = getMonths(dateFrom, dateTo); + // const months = getMonths(dateFrom, dateTo); try { // Subtract 11 hours from the start date to avoid problems in UTC- time zones. const startDate = dayjs(dateFrom).subtract(11, "hours").format(); @@ -348,6 +348,19 @@ export const updateEvent = async ( }) : undefined; + if (!updatedResult) { + logger.error( + "updateEvent failed", + safeStringify({ + success, + bookingRefUid, + credential: getPiiFreeCredential(credential), + originalEvent: getPiiFreeCalendarEvent(calEvent), + calError, + }) + ); + } + if (Array.isArray(updatedResult)) { calWarnings = updatedResult.flatMap((res) => res.additionalInfo?.calWarnings ?? []); } else { @@ -388,10 +401,11 @@ export const deleteEvent = async ({ if (calendar) { return calendar.deleteEvent(bookingRefUid, event, externalCalendarId); } else { - log.warn( + log.error( "Could not do deleteEvent - No calendar adapter found", safeStringify({ credential: getPiiFreeCredential(credential), + event, }) ); } diff --git a/packages/core/getUserAvailability.ts b/packages/core/getUserAvailability.ts index 5843005e20..3c4f77497b 100644 --- a/packages/core/getUserAvailability.ts +++ b/packages/core/getUserAvailability.ts @@ -164,7 +164,7 @@ export const getUserAvailability = async function getUsersWorkingHoursLifeTheUni const user = initialData?.user || (await getUser(where)); - if (!user) throw new HttpError({ statusCode: 404, message: "No user found" }); + if (!user) throw new HttpError({ statusCode: 404, message: "No user found in getUserAvailability" }); log.debug( "getUserAvailability for user", safeStringify({ user: { id: user.id }, slot: { dateFrom, dateTo } }) diff --git a/packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx b/packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx index afec8ed37c..ebbbdeeb45 100644 --- a/packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx +++ b/packages/features/bookings/Booker/components/BookEventForm/BookEventForm.tsx @@ -28,7 +28,6 @@ import { useBookingSuccessRedirect } from "@calcom/lib/bookingSuccessRedirect"; import { MINUTES_TO_BOOK } from "@calcom/lib/constants"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery"; -import { HttpError } from "@calcom/lib/http-error"; import { trpc } from "@calcom/trpc"; import { Alert, Button, EmptyScreen, Form, showToast } from "@calcom/ui"; import { Calendar } from "@calcom/ui/components/icon"; @@ -153,6 +152,7 @@ export const BookEventFormChild = ({ const verifiedEmail = useBookerStore((state) => state.verifiedEmail); const setVerifiedEmail = useBookerStore((state) => state.setVerifiedEmail); const bookingSuccessRedirect = useBookingSuccessRedirect(); + const [responseVercelIdHeader, setResponseVercelIdHeader] = useState(null); const router = useRouter(); const { t, i18n } = useLocale(); @@ -220,7 +220,12 @@ export const BookEventFormChild = ({ booking: responseData, }); }, - onError: () => { + onError: (err, _, ctx) => { + // TODO: + // const vercelId = ctx?.meta?.headers?.get("x-vercel-id"); + // if (vercelId) { + // setResponseVercelIdHeader(vercelId); + // } errorRef && errorRef.current?.scrollIntoView({ behavior: "smooth" }); }, }); @@ -390,7 +395,8 @@ export const BookEventFormChild = ({ bookingForm.formState.errors["globalError"], createBookingMutation, createRecurringBookingMutation, - t + t, + responseVercelIdHeader )} /> @@ -438,16 +444,19 @@ const getError = ( bookingMutation: UseMutationResult, // eslint-disable-next-line @typescript-eslint/no-explicit-any recurringBookingMutation: UseMutationResult, - t: TFunction + t: TFunction, + responseVercelIdHeader: string | null ) => { if (globalError) return globalError.message; const error = bookingMutation.error || recurringBookingMutation.error; - return error instanceof HttpError || error instanceof Error ? ( - <>{t("can_you_try_again")} + return error.message ? ( + <> + {responseVercelIdHeader ?? ""} {t(error.message)} + ) : ( - "Unknown error" + <>{t("can_you_try_again")} ); }; diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index a8d9c5dbe2..8328fd26ef 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -6,6 +6,7 @@ import { isValidPhoneNumber } from "libphonenumber-js"; import { cloneDeep } from "lodash"; import type { NextApiRequest } from "next"; import short, { uuid } from "short-uuid"; +import type { Logger } from "tslog"; import { v5 as uuidv5 } from "uuid"; import z from "zod"; @@ -52,6 +53,7 @@ import { cancelScheduledJobs, scheduleTrigger } from "@calcom/features/webhooks/ import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib"; import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser"; import { getDefaultEvent, getUsernameList } from "@calcom/lib/defaultEvents"; +import { ErrorCode } from "@calcom/lib/errorCodes"; import { getErrorFromUnknown } from "@calcom/lib/errors"; import getPaymentAppData from "@calcom/lib/getPaymentAppData"; import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType"; @@ -362,7 +364,8 @@ async function ensureAvailableUsers( eventType: Awaited> & { users: IsFixedAwareUser[]; }, - input: { dateFrom: string; dateTo: string; timeZone: string; originalRescheduledBooking?: BookingType } + input: { dateFrom: string; dateTo: string; timeZone: string; originalRescheduledBooking?: BookingType }, + loggerWithEventDetails: Logger ) { const availableUsers: IsFixedAwareUser[] = []; const duration = dayjs(input.dateTo).diff(input.dateFrom, "minute"); @@ -433,7 +436,8 @@ async function ensureAvailableUsers( } } if (!availableUsers.length) { - throw new Error("No available users found."); + loggerWithEventDetails.error(`No available users found.`); + throw new Error(ErrorCode.NoAvailableUsersFound); } return availableUsers; } @@ -556,7 +560,7 @@ async function getBookingData({ return true; }; if (!reqBodyWithEnd(reqBody)) { - throw new Error("Internal Error."); + throw new Error(ErrorCode.RequestBodyWithouEnd); } // reqBody.end is no longer an optional property. if ("customInputs" in reqBody) { @@ -691,10 +695,11 @@ async function handler( const fullName = getFullName(bookerName); + // Why are we only using "en" locale const tGuests = await getTranslation("en", "common"); const dynamicUserList = Array.isArray(reqBody.user) ? reqBody.user : getUsernameList(reqBody.user); - if (!eventType) throw new HttpError({ statusCode: 404, message: "eventType.notFound" }); + if (!eventType) throw new HttpError({ statusCode: 404, message: "event_type_not_found" }); const isTeamEventType = !!eventType.schedulingType && ["COLLECTIVE", "ROUND_ROBIN"].includes(eventType.schedulingType); @@ -935,7 +940,8 @@ async function handler( dateTo: dayjs(reqBody.end).tz(reqBody.timeZone).format(), timeZone: reqBody.timeZone, originalRescheduledBooking, - } + }, + loggerWithEventDetails ); const luckyUsers: typeof users = []; @@ -965,7 +971,7 @@ async function handler( if ( availableUsers.filter((user) => user.isFixed).length !== users.filter((user) => user.isFixed).length ) { - throw new Error("Some of the hosts are unavailable for booking."); + throw new Error(ErrorCode.HostsUnavailableForBooking); } // Pushing fixed user before the luckyUser guarantees the (first) fixed user as the organizer. users = [...availableUsers.filter((user) => user.isFixed), ...luckyUsers]; @@ -1283,7 +1289,7 @@ async function handler( booking.attendees.find((attendee) => attendee.email === invitee[0].email) && dayjs.utc(booking.startTime).format() === evt.startTime ) { - throw new HttpError({ statusCode: 409, message: "Already signed up for this booking." }); + throw new HttpError({ statusCode: 409, message: ErrorCode.AlreadySignedUpForBooking }); } // There are two paths here, reschedule a booking with seats and booking seats without reschedule @@ -2694,7 +2700,7 @@ const findBookingQuery = async (bookingId: number) => { // This should never happen but it's just typescript safe if (!foundBooking) { - throw new Error("Internal Error."); + throw new Error("Internal Error. Couldn't find booking"); } // Don't leak any sensitive data diff --git a/packages/features/bookings/lib/handleNewBooking/test/fresh-booking.test.ts b/packages/features/bookings/lib/handleNewBooking/test/fresh-booking.test.ts index fbc0291e3a..c893929b9a 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/fresh-booking.test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/fresh-booking.test.ts @@ -13,6 +13,7 @@ import { describe, expect } from "vitest"; import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData"; import { WEBAPP_URL } from "@calcom/lib/constants"; +import { ErrorCode } from "@calcom/lib/errorCodes"; import { resetTestEmails } from "@calcom/lib/testEmails"; import { BookingStatus } from "@calcom/prisma/enums"; import { test } from "@calcom/web/test/fixtures/fixtures"; @@ -1024,7 +1025,7 @@ describe("handleNewBooking", () => { }); await expect(async () => await handleNewBooking(req)).rejects.toThrowError( - "No available users found" + ErrorCode.NoAvailableUsersFound ); }, timeout @@ -1111,7 +1112,7 @@ describe("handleNewBooking", () => { }); await expect(async () => await handleNewBooking(req)).rejects.toThrowError( - "No available users found" + ErrorCode.NoAvailableUsersFound ); }, timeout @@ -1239,7 +1240,7 @@ describe("handleNewBooking", () => { * NOTE: We might want to think about making the bookings get ACCEPTED automatically if the booker is the organizer of the event-type. This is a design decision it seems for now. */ test( - `should make a fresh booking in PENDING state even when the booker is the organizer of the event-type + `should make a fresh booking in PENDING state even when the booker is the organizer of the event-type 1. Should create a booking in the database with status PENDING 2. Should send emails to the booker as well as organizer for booking request and awaiting approval 3. Should trigger BOOKING_REQUESTED webhook diff --git a/packages/features/bookings/lib/handleNewBooking/test/recurring-event.test.ts b/packages/features/bookings/lib/handleNewBooking/test/recurring-event.test.ts index 68c1ba52a0..04f7e72266 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/recurring-event.test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/recurring-event.test.ts @@ -2,6 +2,7 @@ import { v4 as uuidv4 } from "uuid"; import { describe, expect } from "vitest"; import { WEBAPP_URL } from "@calcom/lib/constants"; +import { ErrorCode } from "@calcom/lib/errorCodes"; import logger from "@calcom/lib/logger"; import { BookingStatus } from "@calcom/prisma/enums"; import { test } from "@calcom/web/test/fixtures/fixtures"; @@ -368,7 +369,7 @@ describe("handleNewBooking", () => { }), }); - expect(() => handleRecurringEventBooking(req, res)).rejects.toThrow("No available users found"); + expect(() => handleRecurringEventBooking(req, res)).rejects.toThrow(ErrorCode.NoAvailableUsersFound); // Actually the first booking goes through in this case but the status is still a failure. We should do a dry run to check if booking is possible for the 2 slots and if yes, then only go for the actual booking otherwise fail the recurring bookign }, timeout diff --git a/packages/features/bookings/lib/handleNewBooking/test/team-bookings/collective-scheduling.test.ts b/packages/features/bookings/lib/handleNewBooking/test/team-bookings/collective-scheduling.test.ts index 386c910e5f..eb59eed52e 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/team-bookings/collective-scheduling.test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/team-bookings/collective-scheduling.test.ts @@ -4,6 +4,7 @@ import { describe, expect } from "vitest"; import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData"; import { WEBAPP_URL } from "@calcom/lib/constants"; +import { ErrorCode } from "@calcom/lib/errorCodes"; import { SchedulingType } from "@calcom/prisma/enums"; import { BookingStatus } from "@calcom/prisma/enums"; import { test } from "@calcom/web/test/fixtures/fixtures"; @@ -353,7 +354,7 @@ describe("handleNewBooking", () => { await expect(async () => { await handleNewBooking(req); - }).rejects.toThrowError("Some of the hosts are unavailable for booking"); + }).rejects.toThrowError(ErrorCode.HostsUnavailableForBooking); }, timeout ); @@ -666,7 +667,7 @@ describe("handleNewBooking", () => { await expect(async () => { await handleNewBooking(req); - }).rejects.toThrowError("No available users found."); + }).rejects.toThrowError(ErrorCode.NoAvailableUsersFound); }, timeout ); diff --git a/packages/lib/errorCodes.ts b/packages/lib/errorCodes.ts new file mode 100644 index 0000000000..07c51ae693 --- /dev/null +++ b/packages/lib/errorCodes.ts @@ -0,0 +1,9 @@ +export enum ErrorCode { + PaymentCreationFailure = "payment_not_created_error", + NoAvailableUsersFound = "no_available_users_found_error", + ChargeCardFailure = "couldnt_charge_card_error", + RequestBodyWithouEnd = "request_body_end_time_internal_error", + AlreadySignedUpForBooking = "already_signed_up_for_this_booking_error", + HostsUnavailableForBooking = "hosts_unavailable_for_booking", + EventTypeNotFound = "event_type_not_found_error", +}