From 20898e150572102ed692c4d30b8451d2ea6bd57a Mon Sep 17 00:00:00 2001 From: Hariom Balhara Date: Sat, 30 Sep 2023 10:22:32 +0530 Subject: [PATCH] fix: Handle payment flow webhooks in case of event requiring confirmation (#11458) Co-authored-by: alannnc --- apps/web/public/static/locales/en/common.json | 1 + apps/web/test/lib/checkBookingLimits.test.ts | 2 +- apps/web/test/lib/checkDurationLimits.test.ts | 2 +- apps/web/test/lib/getSchedule.test.ts | 50 +- .../test/lib/handleChildrenEventTypes.test.ts | 2 +- apps/web/test/lib/team-event-types.test.ts | 2 +- .../bookingScenario/MockPaymentService.ts | 88 + .../{ => bookingScenario}/bookingScenario.ts | 640 ++++--- .../web/test/utils/bookingScenario/expects.ts | 481 +++++ package.json | 1 + .../lib/CalendarService.test.ts | 2 +- packages/core/CalendarManager.ts | 3 + packages/core/EventManager.ts | 17 +- packages/core/videoClient.ts | 29 +- .../lib/doesBookingRequireConfirmation.ts | 36 + .../lib/getWebhookPayloadForBooking.ts | 37 + .../bookings/lib/handleBookingRequested.ts | 69 + .../bookings/lib/handleNewBooking.test.ts | 1294 ++++++++++--- .../features/bookings/lib/handleNewBooking.ts | 65 +- packages/features/ee/payments/api/webhook.ts | 158 +- .../webhooks/components/WebhookForm.tsx | 1 + packages/features/webhooks/lib/constants.ts | 1 + packages/lib/payment/getBooking.ts | 118 ++ packages/lib/payment/handlePaymentSuccess.ts | 158 +- .../migration.sql | 2 + packages/prisma/schema.prisma | 1 + tests/libs/__mocks__/prisma.ts | 84 +- tests/libs/__mocks__/prismaMock.ts | 18 + yarn.lock | 1673 +++++++++-------- 29 files changed, 3379 insertions(+), 1656 deletions(-) create mode 100644 apps/web/test/utils/bookingScenario/MockPaymentService.ts rename apps/web/test/utils/{ => bookingScenario}/bookingScenario.ts (52%) create mode 100644 apps/web/test/utils/bookingScenario/expects.ts create mode 100644 packages/features/bookings/lib/doesBookingRequireConfirmation.ts create mode 100644 packages/features/bookings/lib/getWebhookPayloadForBooking.ts create mode 100644 packages/features/bookings/lib/handleBookingRequested.ts create mode 100644 packages/lib/payment/getBooking.ts create mode 100644 packages/prisma/migrations/20230904102526_add_booking_payment_initiated/migration.sql create mode 100644 tests/libs/__mocks__/prismaMock.ts diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index dbf6790069..74ac083f23 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -423,6 +423,7 @@ "booking_created": "Booking Created", "booking_rejected": "Booking Rejected", "booking_requested": "Booking Requested", + "booking_payment_initiated": "Booking Payment Initiated", "meeting_ended": "Meeting Ended", "form_submitted": "Form Submitted", "booking_paid": "Booking Paid", diff --git a/apps/web/test/lib/checkBookingLimits.test.ts b/apps/web/test/lib/checkBookingLimits.test.ts index b80e9ef695..1dec09fc79 100644 --- a/apps/web/test/lib/checkBookingLimits.test.ts +++ b/apps/web/test/lib/checkBookingLimits.test.ts @@ -1,4 +1,4 @@ -import prismaMock from "../../../../tests/libs/__mocks__/prisma"; +import prismaMock from "../../../../tests/libs/__mocks__/prismaMock"; import { describe, expect, it } from "vitest"; diff --git a/apps/web/test/lib/checkDurationLimits.test.ts b/apps/web/test/lib/checkDurationLimits.test.ts index d8c7daf827..14e79e869a 100644 --- a/apps/web/test/lib/checkDurationLimits.test.ts +++ b/apps/web/test/lib/checkDurationLimits.test.ts @@ -1,4 +1,4 @@ -import prismaMock from "../../../../tests/libs/__mocks__/prisma"; +import prismaMock from "../../../../tests/libs/__mocks__/prismaMock"; import { describe, expect, it } from "vitest"; diff --git a/apps/web/test/lib/getSchedule.test.ts b/apps/web/test/lib/getSchedule.test.ts index b3d6d158c8..77fd0bcb32 100644 --- a/apps/web/test/lib/getSchedule.test.ts +++ b/apps/web/test/lib/getSchedule.test.ts @@ -1,20 +1,18 @@ import CalendarManagerMock from "../../../../tests/libs/__mocks__/CalendarManager"; -import prismaMock from "../../../../tests/libs/__mocks__/prisma"; +import prismock from "../../../../tests/libs/__mocks__/prisma"; import { diff } from "jest-diff"; import { describe, expect, vi, beforeEach, afterEach, test } from "vitest"; -import prisma from "@calcom/prisma"; import type { BookingStatus } from "@calcom/prisma/enums"; import type { Slot } from "@calcom/trpc/server/routers/viewer/slots/types"; import { getAvailableSlots as getSchedule } from "@calcom/trpc/server/routers/viewer/slots/util"; -import { getDate, getGoogleCalendarCredential, createBookingScenario } from "../utils/bookingScenario"; - -// TODO: Mock properly -prismaMock.eventType.findUnique.mockResolvedValue(null); -// @ts-expect-error Prisma v5 typings are not yet available -prismaMock.user.findMany.mockResolvedValue([]); +import { + getDate, + getGoogleCalendarCredential, + createBookingScenario, +} from "../utils/bookingScenario/bookingScenario"; vi.mock("@calcom/lib/constants", () => ({ IS_PRODUCTION: true, @@ -146,13 +144,13 @@ const TestData = { }; const cleanup = async () => { - await prisma.eventType.deleteMany(); - await prisma.user.deleteMany(); - await prisma.schedule.deleteMany(); - await prisma.selectedCalendar.deleteMany(); - await prisma.credential.deleteMany(); - await prisma.booking.deleteMany(); - await prisma.app.deleteMany(); + await prismock.eventType.deleteMany(); + await prismock.user.deleteMany(); + await prismock.schedule.deleteMany(); + await prismock.selectedCalendar.deleteMany(); + await prismock.credential.deleteMany(); + await prismock.booking.deleteMany(); + await prismock.app.deleteMany(); }; beforeEach(async () => { @@ -201,7 +199,7 @@ describe("getSchedule", () => { apps: [TestData.apps.googleCalendar], }; // An event with one accepted booking - createBookingScenario(scenarioData); + await createBookingScenario(scenarioData); const scheduleForDayWithAGoogleCalendarBooking = await getSchedule({ input: { @@ -228,7 +226,7 @@ describe("getSchedule", () => { const { dateString: plus3DateString } = getDate({ dateIncrement: 3 }); // An event with one accepted booking - createBookingScenario({ + await createBookingScenario({ // An event with length 30 minutes, slotInterval 45 minutes, and minimumBookingNotice 1440 minutes (24 hours) eventTypes: [ { @@ -354,7 +352,7 @@ describe("getSchedule", () => { }); test("slots are available as per `length`, `slotInterval` of the event", async () => { - createBookingScenario({ + await createBookingScenario({ eventTypes: [ { id: 1, @@ -453,7 +451,7 @@ describe("getSchedule", () => { })() ); - createBookingScenario({ + await createBookingScenario({ eventTypes: [ { id: 1, @@ -569,7 +567,7 @@ describe("getSchedule", () => { apps: [TestData.apps.googleCalendar], }; - createBookingScenario(scenarioData); + await createBookingScenario(scenarioData); const scheduleForEventOnADayWithNonCalBooking = await getSchedule({ input: { @@ -643,7 +641,7 @@ describe("getSchedule", () => { apps: [TestData.apps.googleCalendar], }; - createBookingScenario(scenarioData); + await createBookingScenario(scenarioData); const scheduleForEventOnADayWithCalBooking = await getSchedule({ input: { @@ -701,7 +699,7 @@ describe("getSchedule", () => { apps: [TestData.apps.googleCalendar], }; - createBookingScenario(scenarioData); + await createBookingScenario(scenarioData); const schedule = await getSchedule({ input: { @@ -765,7 +763,7 @@ describe("getSchedule", () => { ], }; - createBookingScenario(scenarioData); + await createBookingScenario(scenarioData); const scheduleForEventOnADayWithDateOverride = await getSchedule({ input: { @@ -790,7 +788,7 @@ describe("getSchedule", () => { const { dateString: plus1DateString } = getDate({ dateIncrement: 1 }); const { dateString: plus2DateString } = getDate({ dateIncrement: 2 }); - createBookingScenario({ + await createBookingScenario({ eventTypes: [ // A Collective Event Type hosted by this user { @@ -885,7 +883,7 @@ describe("getSchedule", () => { const { dateString: plus1DateString } = getDate({ dateIncrement: 1 }); const { dateString: plus2DateString } = getDate({ dateIncrement: 2 }); - createBookingScenario({ + await createBookingScenario({ eventTypes: [ // An event having two users with one accepted booking { @@ -1010,7 +1008,7 @@ describe("getSchedule", () => { const { dateString: plus2DateString } = getDate({ dateIncrement: 2 }); const { dateString: plus3DateString } = getDate({ dateIncrement: 3 }); - createBookingScenario({ + await createBookingScenario({ eventTypes: [ // An event having two users with one accepted booking { diff --git a/apps/web/test/lib/handleChildrenEventTypes.test.ts b/apps/web/test/lib/handleChildrenEventTypes.test.ts index d90cb2fb17..42000292f0 100644 --- a/apps/web/test/lib/handleChildrenEventTypes.test.ts +++ b/apps/web/test/lib/handleChildrenEventTypes.test.ts @@ -1,4 +1,4 @@ -import prismaMock from "../../../../tests/libs/__mocks__/prisma"; +import prismaMock from "../../../../tests/libs/__mocks__/prismaMock"; import type { EventType } from "@prisma/client"; import { describe, expect, it, vi } from "vitest"; diff --git a/apps/web/test/lib/team-event-types.test.ts b/apps/web/test/lib/team-event-types.test.ts index 998eb4b7aa..dc0ed54b0d 100644 --- a/apps/web/test/lib/team-event-types.test.ts +++ b/apps/web/test/lib/team-event-types.test.ts @@ -1,4 +1,4 @@ -import prismaMock from "../../../../tests/libs/__mocks__/prisma"; +import prismaMock from "../../../../tests/libs/__mocks__/prismaMock"; import { expect, it } from "vitest"; diff --git a/apps/web/test/utils/bookingScenario/MockPaymentService.ts b/apps/web/test/utils/bookingScenario/MockPaymentService.ts new file mode 100644 index 0000000000..2eef10444a --- /dev/null +++ b/apps/web/test/utils/bookingScenario/MockPaymentService.ts @@ -0,0 +1,88 @@ +import prismaMock from "../../../../../tests/libs/__mocks__/prisma"; + +import type { Payment, Prisma, PaymentOption, Booking } from "@prisma/client"; +import { v4 as uuidv4 } from "uuid"; +import "vitest-fetch-mock"; + +import { sendAwaitingPaymentEmail } from "@calcom/emails"; +import logger from "@calcom/lib/logger"; +import type { CalendarEvent } from "@calcom/types/Calendar"; +import type { IAbstractPaymentService } from "@calcom/types/PaymentService"; + +export function getMockPaymentService() { + function createPaymentLink(/*{ paymentUid, name, email, date }*/) { + return "http://mock-payment.example.com/"; + } + const paymentUid = uuidv4(); + const externalId = uuidv4(); + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + class MockPaymentService implements IAbstractPaymentService { + // TODO: We shouldn't need to implement adding a row to Payment table but that's a requirement right now. + // We should actually delegate table creation to the core app. Here, only the payment app specific logic should come + async create( + payment: Pick, + bookingId: Booking["id"], + userId: Booking["userId"], + username: string | null, + bookerName: string | null, + bookerEmail: string, + paymentOption: PaymentOption + ) { + const paymentCreateData = { + id: 1, + uid: paymentUid, + appId: null, + bookingId, + // booking Booking? @relation(fields: [bookingId], references: [id], onDelete: Cascade) + fee: 10, + success: true, + refunded: false, + data: {}, + externalId, + paymentOption, + amount: payment.amount, + currency: payment.currency, + }; + + const paymentData = prismaMock.payment.create({ + data: paymentCreateData, + }); + logger.silly("Created mock payment", JSON.stringify({ paymentData })); + + return paymentData; + } + async afterPayment( + event: CalendarEvent, + booking: { + user: { email: string | null; name: string | null; timeZone: string } | null; + id: number; + startTime: { toISOString: () => string }; + uid: string; + }, + paymentData: Payment + ): Promise { + // TODO: App implementing PaymentService is supposed to send email by itself at the moment. + await sendAwaitingPaymentEmail({ + ...event, + paymentInfo: { + link: createPaymentLink(/*{ + paymentUid: paymentData.uid, + name: booking.user?.name, + email: booking.user?.email, + date: booking.startTime.toISOString(), + }*/), + paymentOption: paymentData.paymentOption || "ON_BOOKING", + amount: paymentData.amount, + currency: paymentData.currency, + }, + }); + } + } + return { + paymentUid, + externalId, + MockPaymentService, + }; +} diff --git a/apps/web/test/utils/bookingScenario.ts b/apps/web/test/utils/bookingScenario/bookingScenario.ts similarity index 52% rename from apps/web/test/utils/bookingScenario.ts rename to apps/web/test/utils/bookingScenario/bookingScenario.ts index dca048fac4..ce11e1b89b 100644 --- a/apps/web/test/utils/bookingScenario.ts +++ b/apps/web/test/utils/bookingScenario/bookingScenario.ts @@ -1,25 +1,23 @@ -import appStoreMock from "../../../../tests/libs/__mocks__/app-store"; -import i18nMock from "../../../../tests/libs/__mocks__/libServerI18n"; -import prismaMock from "../../../../tests/libs/__mocks__/prisma"; +import appStoreMock from "../../../../../tests/libs/__mocks__/app-store"; +import i18nMock from "../../../../../tests/libs/__mocks__/libServerI18n"; +import prismock from "../../../../../tests/libs/__mocks__/prisma"; -import type { - EventType as PrismaEventType, - User as PrismaUser, - Booking as PrismaBooking, - App as PrismaApp, -} from "@prisma/client"; import type { Prisma } from "@prisma/client"; import type { WebhookTriggerEvents } from "@prisma/client"; +import type Stripe from "stripe"; import { v4 as uuidv4 } from "uuid"; -import { expect } from "vitest"; import "vitest-fetch-mock"; import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData"; +import { handleStripePaymentSuccess } from "@calcom/features/ee/payments/api/webhook"; +import { HttpError } from "@calcom/lib/http-error"; import logger from "@calcom/lib/logger"; import type { SchedulingType } from "@calcom/prisma/enums"; import type { BookingStatus } from "@calcom/prisma/enums"; +import type { NewCalendarEventType } from "@calcom/types/Calendar"; import type { EventBusyDate } from "@calcom/types/Calendar"; -import type { Fixtures } from "@calcom/web/test/fixtures/fixtures"; + +import { getMockPaymentService } from "./MockPaymentService"; type App = { slug: string; @@ -78,7 +76,7 @@ type InputUser = typeof TestData.users.example & { id: number } & { }[]; }; -type InputEventType = { +export type InputEventType = { id: number; title?: string; length?: number; @@ -94,9 +92,11 @@ type InputEventType = { beforeEventBuffer?: number; afterEventBuffer?: number; requiresConfirmation?: boolean; -}; +} & Partial>; type InputBooking = { + id?: number; + uid?: string; userId?: number; eventTypeId: number; startTime: string; @@ -104,14 +104,40 @@ type InputBooking = { title?: string; status: BookingStatus; attendees?: { email: string }[]; + references?: { + type: string; + uid: string; + meetingId?: string; + meetingPassword?: string; + meetingUrl?: string; + bookingId?: number; + externalCalendarId?: string; + deleted?: boolean; + credentialId?: number; + }[]; }; const Timezones = { "+5:30": "Asia/Kolkata", "+6:00": "Asia/Dhaka", }; +logger.setSettings({ minLevel: "silly" }); -function addEventTypes(eventTypes: InputEventType[], usersStore: InputUser[]) { +async function addEventTypesToDb( + eventTypes: (Prisma.EventTypeCreateInput & { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + users?: any[]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + workflows?: any[]; + })[] +) { + logger.silly("TestData: Add EventTypes to DB", JSON.stringify(eventTypes)); + await prismock.eventType.createMany({ + data: eventTypes, + }); +} + +async function addEventTypes(eventTypes: InputEventType[], usersStore: InputUser[]) { const baseEventType = { title: "Base EventType Title", slug: "base-event-type-slug", @@ -119,7 +145,7 @@ function addEventTypes(eventTypes: InputEventType[], usersStore: InputUser[]) { beforeEventBuffer: 0, afterEventBuffer: 0, schedulingType: null, - + length: 15, //TODO: What is the purpose of periodStartDate and periodEndDate? Test these? periodStartDate: new Date("2022-01-21T09:03:48.000Z"), periodEndDate: new Date("2022-01-21T09:03:48.000Z"), @@ -150,170 +176,162 @@ function addEventTypes(eventTypes: InputEventType[], usersStore: InputUser[]) { users, }; }); - - logger.silly("TestData: Creating EventType", eventTypes); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const eventTypeMock = ({ where }) => { - return new Promise((resolve) => { - const eventType = eventTypesWithUsers.find((e) => e.id === where.id) as unknown as PrismaEventType & { - users: PrismaUser[]; - }; - resolve(eventType); - }); - }; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - prismaMock.eventType.findUnique.mockImplementation(eventTypeMock); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - prismaMock.eventType.findUniqueOrThrow.mockImplementation(eventTypeMock); + logger.silly("TestData: Creating EventType", JSON.stringify(eventTypesWithUsers)); + await addEventTypesToDb(eventTypesWithUsers); } -async function addBookings(bookings: InputBooking[], eventTypes: InputEventType[]) { - logger.silly("TestData: Creating Bookings", bookings); - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - prismaMock.booking.findMany.mockImplementation((findManyArg) => { - // @ts-expect-error Prisma v5 breaks this - const where = findManyArg?.where || {}; - return new Promise((resolve) => { - resolve( - // @ts-expect-error Prisma v5 breaks this - bookings - // We can improve this filter to support the entire where clause but that isn't necessary yet. So, handle what we know we pass to `findMany` and is needed - .filter((booking) => { - /** - * A user is considered busy within a given time period if there - * is a booking they own OR host. This function mocks some of the logic - * for each condition. For details see the following ticket: - * https://github.com/calcom/cal.com/issues/6374 - */ - - // ~~ FIRST CONDITION ensures that this booking is owned by this user - // and that the status is what we want - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const statusIn = where.OR[0].status?.in || []; - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const userIdIn = where.OR[0].userId?.in || []; - const firstConditionMatches = - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - statusIn.includes(booking.status) && - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - (booking.userId === where.OR[0].userId || userIdIn.includes(booking.userId)); - - // We return this booking if either condition is met - return firstConditionMatches; - }) - .map((booking) => ({ - uid: uuidv4(), - title: "Test Booking Title", - ...booking, - eventType: eventTypes.find((eventType) => eventType.id === booking.eventTypeId), - })) as unknown as PrismaBooking[] - ); - }); +function addBookingReferencesToDB(bookingReferences: Prisma.BookingReferenceCreateManyInput[]) { + prismock.bookingReference.createMany({ + data: bookingReferences, }); } -async function addWebhooks(webhooks: InputWebhook[]) { - prismaMock.webhook.findMany.mockResolvedValue( - // @ts-expect-error Prisma v5 breaks this - webhooks.map((webhook) => { - return { - ...webhook, - payloadTemplate: null, - secret: null, - id: uuidv4(), - createdAt: new Date(), - userId: webhook.userId || null, - eventTypeId: webhook.eventTypeId || null, - teamId: webhook.teamId || null, - }; +async function addBookingsToDb( + bookings: (Prisma.BookingCreateInput & { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + references: any[]; + })[] +) { + await prismock.booking.createMany({ + data: bookings, + }); + logger.silly( + "TestData: Booking as in DB", + JSON.stringify({ + bookings: await prismock.booking.findMany({ + include: { + references: true, + }, + }), }) ); } -function addUsers(users: InputUser[]) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - prismaMock.user.findUniqueOrThrow.mockImplementation((findUniqueArgs) => { - return new Promise((resolve) => { - // @ts-expect-error Prisma v5 breaks this - resolve({ - // @ts-expect-error Prisma v5 breaks this - email: `IntegrationTestUser${findUniqueArgs?.where.id}@example.com`, - } as unknown as PrismaUser); - }); +async function addBookings(bookings: InputBooking[]) { + logger.silly("TestData: Creating Bookings", JSON.stringify(bookings)); + const allBookings = [...bookings].map((booking) => { + if (booking.references) { + addBookingReferencesToDB( + booking.references.map((reference) => { + return { + ...reference, + bookingId: booking.id, + }; + }) + ); + } + return { + uid: uuidv4(), + workflowReminders: [], + references: [], + title: "Test Booking Title", + ...booking, + }; }); - prismaMock.user.findMany.mockResolvedValue( - // @ts-expect-error Prisma v5 breaks this - users.map((user) => { - return { - ...user, - username: `IntegrationTestUser${user.id}`, - email: `IntegrationTestUser${user.id}@example.com`, - }; - }) as unknown as PrismaUser[] + await addBookingsToDb( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + allBookings.map((booking) => { + const bookingCreate = booking; + if (booking.references) { + bookingCreate.references = { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + createMany: { + data: booking.references, + }, + }; + } + return bookingCreate; + }) ); } +// eslint-disable-next-line @typescript-eslint/no-explicit-any +async function addWebhooksToDb(webhooks: any[]) { + await prismock.webhook.createMany({ + data: webhooks, + }); +} + +async function addWebhooks(webhooks: InputWebhook[]) { + logger.silly("TestData: Creating Webhooks", webhooks); + + await addWebhooksToDb(webhooks); +} + +async function addUsersToDb(users: (Prisma.UserCreateInput & { schedules: Prisma.ScheduleCreateInput[] })[]) { + logger.silly("TestData: Creating Users", JSON.stringify(users)); + await prismock.user.createMany({ + data: users, + }); +} + +async function addUsers(users: InputUser[]) { + const prismaUsersCreate = users.map((user) => { + const newUser = user; + if (user.schedules) { + newUser.schedules = { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + createMany: { + data: user.schedules.map((schedule) => { + return { + ...schedule, + availability: { + createMany: { + data: schedule.availability, + }, + }, + }; + }), + }, + }; + } + if (user.credentials) { + newUser.credentials = { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + createMany: { + data: user.credentials, + }, + }; + } + return newUser; + }); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + await addUsersToDb(prismaUsersCreate); +} + export async function createBookingScenario(data: ScenarioData) { - logger.silly("TestData: Creating Scenario", data); - addUsers(data.users); + logger.silly("TestData: Creating Scenario", JSON.stringify({ data })); + await addUsers(data.users); - const eventType = addEventTypes(data.eventTypes, data.users); + const eventType = await addEventTypes(data.eventTypes, data.users); if (data.apps) { - // @ts-expect-error Prisma v5 breaks this - prismaMock.app.findMany.mockResolvedValue(data.apps as PrismaApp[]); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - //@ts-ignore - const appMock = ({ where: { slug: whereSlug } }) => { - return new Promise((resolve) => { - if (!data.apps) { - resolve(null); - return; - } - - const foundApp = data.apps.find(({ slug }) => slug == whereSlug); - //TODO: Pass just the app name in data.apps and maintain apps in a separate object or load them dyamically - resolve( - ({ - ...foundApp, - ...(foundApp?.slug ? TestData.apps[foundApp.slug as keyof typeof TestData.apps] || {} : {}), - enabled: true, - createdAt: new Date(), - updatedAt: new Date(), - categories: [], - } as PrismaApp) || null - ); - }); - }; - // FIXME: How do we know which app to return? - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - prismaMock.app.findUnique.mockImplementation(appMock); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - prismaMock.app.findFirst.mockImplementation(appMock); + prismock.app.createMany({ + data: data.apps, + }); } data.bookings = data.bookings || []; - allowSuccessfulBookingCreation(); - addBookings(data.bookings, data.eventTypes); + // allowSuccessfulBookingCreation(); + await addBookings(data.bookings); // mockBusyCalendarTimes([]); - addWebhooks(data.webhooks || []); + await addWebhooks(data.webhooks || []); + // addPaymentMock(); return { eventType, }; } +// async function addPaymentsToDb(payments: Prisma.PaymentCreateInput[]) { +// await prismaMock.payment.createMany({ +// data: payments, +// }); +// } + /** * This fn indents to /ally compute day, month, year for the purpose of testing. * We are not using DayJS because that's actually being tested by this code. @@ -372,9 +390,11 @@ export function getMockedCredential({ scope: string; }; }) { + const app = appStoreMetadata[metadataLookupKey as keyof typeof appStoreMetadata]; return { - type: appStoreMetadata[metadataLookupKey as keyof typeof appStoreMetadata].type, - appId: appStoreMetadata[metadataLookupKey as keyof typeof appStoreMetadata].slug, + type: app.type, + appId: app.slug, + app: app, key: { expiry_date: Date.now() + 1000000, token_type: "Bearer", @@ -399,7 +419,16 @@ export function getZoomAppCredential() { return getMockedCredential({ metadataLookupKey: "zoomvideo", key: { - scope: "meeting:writed", + scope: "meeting:write", + }, + }); +} + +export function getStripeAppCredential() { + return getMockedCredential({ + metadataLookupKey: "stripepayment", + key: { + scope: "read_write", }, }); } @@ -466,6 +495,7 @@ export const TestData = { apps: { "google-calendar": { slug: "google-calendar", + enabled: true, dirName: "whatever", // eslint-disable-next-line @typescript-eslint/ban-ts-comment //@ts-ignore @@ -479,6 +509,38 @@ export const TestData = { "daily-video": { slug: "daily-video", dirName: "whatever", + enabled: true, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + keys: { + expiry_date: Infinity, + api_key: "", + scale_plan: "false", + client_id: "client_id", + client_secret: "client_secret", + redirect_uris: ["http://localhost:3000/auth/callback"], + }, + }, + zoomvideo: { + slug: "zoom", + enabled: true, + dirName: "whatever", + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + keys: { + expiry_date: Infinity, + api_key: "", + scale_plan: "false", + client_id: "client_id", + client_secret: "client_secret", + redirect_uris: ["http://localhost:3000/auth/callback"], + }, + }, + "stripe-payment": { + //TODO: Read from appStoreMeta + slug: "stripe", + enabled: true, + dirName: "stripepayment", // eslint-disable-next-line @typescript-eslint/ban-ts-comment //@ts-ignore keys: { @@ -493,14 +555,6 @@ export const TestData = { }, }; -function allowSuccessfulBookingCreation() { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - //@ts-ignore - prismaMock.booking.create.mockImplementation(function (booking) { - return booking.data; - }); -} - export class MockError extends Error { constructor(message: string) { super(message); @@ -540,6 +594,7 @@ export function getScenarioData({ usersApartFromOrganizer = [], apps = [], webhooks, + bookings, }: // hosts = [], { organizer: ReturnType; @@ -547,6 +602,7 @@ export function getScenarioData({ apps: ScenarioData["apps"]; usersApartFromOrganizer?: ScenarioData["users"]; webhooks?: ScenarioData["webhooks"]; + bookings?: ScenarioData["bookings"]; // hosts?: ScenarioData["hosts"]; }) { const users = [organizer, ...usersApartFromOrganizer]; @@ -561,22 +617,28 @@ export function getScenarioData({ }); return { // hosts: [...hosts], - eventTypes: [...eventTypes], + eventTypes: eventTypes.map((eventType, index) => { + return { + ...eventType, + title: `Test Event Type - ${index + 1}`, + description: `It's a test event type - ${index + 1}`, + }; + }), users, apps: [...apps], webhooks, + bookings: bookings || [], }; } -export function mockEnableEmailFeature() { - // @ts-expect-error Prisma v5 breaks this - prismaMock.feature.findMany.mockResolvedValue([ - { +export function enableEmailFeature() { + prismock.feature.create({ + data: { slug: "emails", - // It's a kill switch enabled: false, + type: "KILL_SWITCH", }, - ]); + }); } export function mockNoTranslations() { @@ -589,20 +651,74 @@ export function mockNoTranslations() { }); } -export function mockCalendarToHaveNoBusySlots(metadataLookupKey: keyof typeof appStoreMetadata) { +export function mockCalendarToHaveNoBusySlots( + metadataLookupKey: keyof typeof appStoreMetadata, + calendarData?: { + create: { + uid: string; + }; + update?: { + uid: string; + }; + } +) { const appStoreLookupKey = metadataLookupKey; + const normalizedCalendarData = calendarData || { + create: { + uid: "MOCK_ID", + }, + update: { + uid: "UPDATED_MOCK_ID", + }, + }; + logger.silly(`Mocking ${appStoreLookupKey} on appStoreMock`); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const createEventCalls: any[] = []; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const updateEventCalls: any[] = []; + const app = appStoreMetadata[metadataLookupKey as keyof typeof appStoreMetadata]; appStoreMock.default[appStoreLookupKey as keyof typeof appStoreMock.default].mockResolvedValue({ lib: { // eslint-disable-next-line @typescript-eslint/ban-ts-comment //@ts-ignore CalendarService: function MockCalendarService() { return { - createEvent: () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + createEvent: async function (...rest: any[]): Promise { + const [calEvent, credentialId] = rest; + logger.silly( + "mockCalendarToHaveNoBusySlots.createEvent", + JSON.stringify({ calEvent, credentialId }) + ); + createEventCalls.push(rest); return Promise.resolve({ - type: "daily_video", - id: "dailyEventName", - password: "dailyvideopass", - url: "http://dailyvideo.example.com", + type: app.type, + additionalInfo: {}, + uid: "PROBABLY_UNUSED_UID", + id: normalizedCalendarData.create.uid, + // Password and URL seems useless for CalendarService, plan to remove them if that's the case + password: "MOCK_PASSWORD", + url: "https://UNUSED_URL", + }); + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + updateEvent: async function (...rest: any[]): Promise { + const [uid, event, externalCalendarId] = rest; + logger.silly( + "mockCalendarToHaveNoBusySlots.updateEvent", + JSON.stringify({ uid, event, externalCalendarId }) + ); + // eslint-disable-next-line prefer-rest-params + updateEventCalls.push(rest); + return Promise.resolve({ + type: app.type, + additionalInfo: {}, + uid: "PROBABLY_UNUSED_UID", + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + id: normalizedCalendarData.update!.uid!, + // Password and URL seems useless for CalendarService, plan to remove them if that's the case + password: "MOCK_PASSWORD", + url: "https://UNUSED_URL", }); }, getAvailability: (): Promise => { @@ -614,16 +730,37 @@ export function mockCalendarToHaveNoBusySlots(metadataLookupKey: keyof typeof ap }, }, }); + return { + createEventCalls, + updateEventCalls, + }; } export function mockSuccessfulVideoMeetingCreation({ metadataLookupKey, appStoreLookupKey, + videoMeetingData, }: { metadataLookupKey: string; appStoreLookupKey?: string; + videoMeetingData?: { + password: string; + id: string; + url: string; + }; }) { appStoreLookupKey = appStoreLookupKey || metadataLookupKey; + videoMeetingData = videoMeetingData || { + id: "MOCK_ID", + password: "MOCK_PASS", + url: `http://mock-${metadataLookupKey}.example.com`, + }; + logger.silly( + "mockSuccessfulVideoMeetingCreation", + JSON.stringify({ metadataLookupKey, appStoreLookupKey }) + ); + const createMeetingCalls: any[] = []; + const updateMeetingCalls: any[] = []; // eslint-disable-next-line @typescript-eslint/ban-ts-comment //@ts-ignore appStoreMock.default[appStoreLookupKey as keyof typeof appStoreMock.default].mockImplementation(() => { @@ -633,12 +770,31 @@ export function mockSuccessfulVideoMeetingCreation({ // eslint-disable-next-line @typescript-eslint/ban-ts-comment //@ts-ignore VideoApiAdapter: () => ({ - createMeeting: () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + createMeeting: (...rest: any[]) => { + createMeetingCalls.push(rest); return Promise.resolve({ type: appStoreMetadata[metadataLookupKey as keyof typeof appStoreMetadata].type, - id: "MOCK_ID", - password: "MOCK_PASS", - url: `http://mock-${metadataLookupKey}.example.com`, + ...videoMeetingData, + }); + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + updateMeeting: async (...rest: any[]) => { + const [bookingRef, calEvent] = rest; + updateMeetingCalls.push(rest); + if (!bookingRef.type) { + throw new Error("bookingRef.type is not defined"); + } + if (!calEvent.organizer) { + throw new Error("calEvent.organizer is not defined"); + } + logger.silly( + "mockSuccessfulVideoMeetingCreation.updateMeeting", + JSON.stringify({ bookingRef, calEvent }) + ); + return Promise.resolve({ + type: appStoreMetadata[metadataLookupKey as keyof typeof appStoreMetadata].type, + ...videoMeetingData, }); }, }), @@ -646,6 +802,37 @@ export function mockSuccessfulVideoMeetingCreation({ }); }); }); + return { + createMeetingCalls, + updateMeetingCalls, + }; +} + +export function mockPaymentApp({ + metadataLookupKey, + appStoreLookupKey, +}: { + metadataLookupKey: string; + appStoreLookupKey?: string; +}) { + appStoreLookupKey = appStoreLookupKey || metadataLookupKey; + const { paymentUid, externalId, MockPaymentService } = getMockPaymentService(); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + appStoreMock.default[appStoreLookupKey as keyof typeof appStoreMock.default].mockImplementation(() => { + return new Promise((resolve) => { + resolve({ + lib: { + PaymentService: MockPaymentService, + }, + }); + }); + }); + + return { + paymentUid, + externalId, + }; } export function mockErrorOnVideoMeetingCreation({ @@ -675,39 +862,6 @@ export function mockErrorOnVideoMeetingCreation({ }); } -export function expectWebhookToHaveBeenCalledWith( - subscriberUrl: string, - data: { - triggerEvent: WebhookTriggerEvents; - payload: { metadata: Record; responses: Record }; - } -) { - const fetchCalls = fetchMock.mock.calls; - const webhookFetchCall = fetchCalls.find((call) => call[0] === subscriberUrl); - if (!webhookFetchCall) { - throw new Error(`Webhook not called with ${subscriberUrl}`); - } - expect(webhookFetchCall[0]).toBe(subscriberUrl); - const body = webhookFetchCall[1]?.body; - const parsedBody = JSON.parse((body as string) || "{}"); - console.log({ payload: parsedBody.payload }); - expect(parsedBody.triggerEvent).toBe(data.triggerEvent); - parsedBody.payload.metadata.videoCallUrl = parsedBody.payload.metadata.videoCallUrl - ? parsedBody.payload.metadata.videoCallUrl.replace(/\/video\/[a-zA-Z0-9]{22}/, "/video/DYNAMIC_UID") - : parsedBody.payload.metadata.videoCallUrl; - expect(parsedBody.payload.metadata).toContain(data.payload.metadata); - expect(parsedBody.payload.responses).toEqual(data.payload.responses); -} - -export function expectWorkflowToBeTriggered() { - // TODO: Implement this. -} - -export function expectBookingToBeInDatabase(booking: Partial) { - const createBookingCalledWithArgs = prismaMock.booking.create.mock.calls[0]; - expect(createBookingCalledWithArgs[0].data).toEqual(expect.objectContaining(booking)); -} - export function getBooker({ name, email }: { name: string; email: string }) { return { name, @@ -715,40 +869,28 @@ export function getBooker({ name, email }: { name: string; email: string }) { }; } -declare global { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace jest { - interface Matchers { - toHaveEmail(expectedEmail: { htmlToContain?: string; to: string }): R; - } - } +export function getMockedStripePaymentEvent({ paymentIntentId }: { paymentIntentId: string }) { + return { + id: null, + data: { + object: { + id: paymentIntentId, + }, + }, + } as unknown as Stripe.Event; } -expect.extend({ - toHaveEmail( - testEmail: ReturnType[number], - expectedEmail: { - //TODO: Support email HTML parsing to target specific elements - htmlToContain?: string; - to: string; +export async function mockPaymentSuccessWebhookFromStripe({ externalId }: { externalId: string }) { + let webhookResponse = null; + try { + await handleStripePaymentSuccess(getMockedStripePaymentEvent({ paymentIntentId: externalId })); + } catch (e) { + if (!(e instanceof HttpError)) { + logger.silly("mockPaymentSuccessWebhookFromStripe:catch", JSON.stringify(e)); + } else { + logger.error("mockPaymentSuccessWebhookFromStripe:catch", JSON.stringify(e)); } - ) { - let isHtmlContained = true; - let isToAddressExpected = true; - if (expectedEmail.htmlToContain) { - isHtmlContained = testEmail.html.includes(expectedEmail.htmlToContain); - } - isToAddressExpected = expectedEmail.to === testEmail.to; - - return { - pass: isHtmlContained && isToAddressExpected, - message: () => { - if (!isHtmlContained) { - return `Email HTML is not as expected. Expected:"${expectedEmail.htmlToContain}" isn't contained in "${testEmail.html}"`; - } - - return `Email To address is not as expected. Expected:${expectedEmail.to} isn't contained in ${testEmail.to}`; - }, - }; - }, -}); + webhookResponse = e as HttpError; + } + return { webhookResponse }; +} diff --git a/apps/web/test/utils/bookingScenario/expects.ts b/apps/web/test/utils/bookingScenario/expects.ts new file mode 100644 index 0000000000..356c9752a3 --- /dev/null +++ b/apps/web/test/utils/bookingScenario/expects.ts @@ -0,0 +1,481 @@ +import prismaMock from "../../../../../tests/libs/__mocks__/prisma"; + +import type { Booking, BookingReference } from "@prisma/client"; +import type { WebhookTriggerEvents } from "@prisma/client"; +import { expect } from "vitest"; +import "vitest-fetch-mock"; + +import logger from "@calcom/lib/logger"; +import type { Fixtures } from "@calcom/web/test/fixtures/fixtures"; + +import type { InputEventType } from "./bookingScenario"; + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace jest { + interface Matchers { + toHaveEmail(expectedEmail: { htmlToContain?: string; to: string }, to: string): R; + } + } +} + +expect.extend({ + toHaveEmail( + emails: Fixtures["emails"], + expectedEmail: { + //TODO: Support email HTML parsing to target specific elements + htmlToContain?: string; + to: string; + }, + to: string + ) { + const testEmail = emails.get().find((email) => email.to.includes(to)); + if (!testEmail) { + return { + pass: false, + message: () => `No email sent to ${to}`, + }; + } + let isHtmlContained = true; + let isToAddressExpected = true; + if (expectedEmail.htmlToContain) { + isHtmlContained = testEmail.html.includes(expectedEmail.htmlToContain); + } + isToAddressExpected = expectedEmail.to === testEmail.to; + + return { + pass: isHtmlContained && isToAddressExpected, + message: () => { + if (!isHtmlContained) { + return `Email HTML is not as expected. Expected:"${expectedEmail.htmlToContain}" isn't contained in "${testEmail.html}"`; + } + return `Email To address is not as expected. Expected:${expectedEmail.to} isn't equal to ${testEmail.to}`; + }, + }; + }, +}); + +export function expectWebhookToHaveBeenCalledWith( + subscriberUrl: string, + data: { + triggerEvent: WebhookTriggerEvents; + payload: Record | null; + } +) { + const fetchCalls = fetchMock.mock.calls; + const webhooksToSubscriberUrl = fetchCalls.filter((call) => { + return call[0] === subscriberUrl; + }); + logger.silly("Scanning fetchCalls for webhook", fetchCalls); + const webhookFetchCall = webhooksToSubscriberUrl.find((call) => { + const body = call[1]?.body; + const parsedBody = JSON.parse((body as string) || "{}"); + return parsedBody.triggerEvent === data.triggerEvent; + }); + + if (!webhookFetchCall) { + throw new Error( + `Webhook not sent to ${subscriberUrl} for ${data.triggerEvent}. All webhooks: ${JSON.stringify( + webhooksToSubscriberUrl + )}` + ); + } + expect(webhookFetchCall[0]).toBe(subscriberUrl); + const body = webhookFetchCall[1]?.body; + const parsedBody = JSON.parse((body as string) || "{}"); + + expect(parsedBody.triggerEvent).toBe(data.triggerEvent); + if (parsedBody.payload.metadata?.videoCallUrl) { + parsedBody.payload.metadata.videoCallUrl = parsedBody.payload.metadata.videoCallUrl + ? parsedBody.payload.metadata.videoCallUrl.replace(/\/video\/[a-zA-Z0-9]{22}/, "/video/DYNAMIC_UID") + : parsedBody.payload.metadata.videoCallUrl; + } + if (data.payload) { + if (data.payload.metadata !== undefined) { + expect(parsedBody.payload.metadata).toEqual(expect.objectContaining(data.payload.metadata)); + } + if (data.payload.responses !== undefined) + expect(parsedBody.payload.responses).toEqual(expect.objectContaining(data.payload.responses)); + const { responses: _1, metadata: _2, ...remainingPayload } = data.payload; + expect(parsedBody.payload).toEqual(expect.objectContaining(remainingPayload)); + } +} + +export function expectWorkflowToBeTriggered() { + // TODO: Implement this. +} + +export async function expectBookingToBeInDatabase( + booking: Partial & Pick & { references?: Partial[] } +) { + const actualBooking = await prismaMock.booking.findUnique({ + where: { + uid: booking.uid, + }, + include: { + references: true, + }, + }); + + const { references, ...remainingBooking } = booking; + expect(actualBooking).toEqual(expect.objectContaining(remainingBooking)); + expect(actualBooking?.references).toEqual( + expect.arrayContaining((references || []).map((reference) => expect.objectContaining(reference))) + ); +} + +export function expectSuccessfulBookingCreationEmails({ + emails, + organizer, + booker, +}: { + emails: Fixtures["emails"]; + organizer: { email: string; name: string }; + booker: { email: string; name: string }; +}) { + expect(emails).toHaveEmail( + { + htmlToContain: "confirmed_event_type_subject", + to: `${organizer.email}`, + }, + `${organizer.email}` + ); + + expect(emails).toHaveEmail( + { + htmlToContain: "confirmed_event_type_subject", + to: `${booker.name} <${booker.email}>`, + }, + `${booker.name} <${booker.email}>` + ); +} + +export function expectSuccessfulBookingRescheduledEmails({ + emails, + organizer, + booker, +}: { + emails: Fixtures["emails"]; + organizer: { email: string; name: string }; + booker: { email: string; name: string }; +}) { + expect(emails).toHaveEmail( + { + htmlToContain: "event_type_has_been_rescheduled_on_time_date", + to: `${organizer.email}`, + }, + `${organizer.email}` + ); + + expect(emails).toHaveEmail( + { + htmlToContain: "event_type_has_been_rescheduled_on_time_date", + to: `${booker.name} <${booker.email}>`, + }, + `${booker.name} <${booker.email}>` + ); +} +export function expectAwaitingPaymentEmails({ + emails, + booker, +}: { + emails: Fixtures["emails"]; + organizer: { email: string; name: string }; + booker: { email: string; name: string }; +}) { + expect(emails).toHaveEmail( + { + htmlToContain: "awaiting_payment_subject", + to: `${booker.name} <${booker.email}>`, + }, + `${booker.email}` + ); +} + +export function expectBookingRequestedEmails({ + emails, + organizer, + booker, +}: { + emails: Fixtures["emails"]; + organizer: { email: string; name: string }; + booker: { email: string; name: string }; +}) { + expect(emails).toHaveEmail( + { + htmlToContain: "event_awaiting_approval_subject", + to: `${organizer.email}`, + }, + `${organizer.email}` + ); + + expect(emails).toHaveEmail( + { + htmlToContain: "booking_submitted_subject", + to: `${booker.email}`, + }, + `${booker.email}` + ); +} + +export function expectBookingRequestedWebhookToHaveBeenFired({ + booker, + location, + subscriberUrl, + paidEvent, + eventType, +}: { + organizer: { email: string; name: string }; + booker: { email: string; name: string }; + subscriberUrl: string; + location: string; + paidEvent?: boolean; + eventType: InputEventType; +}) { + // There is an inconsistency in the way we send the data to the webhook for paid events and unpaid events. Fix that and then remove this if statement. + if (!paidEvent) { + expectWebhookToHaveBeenCalledWith(subscriberUrl, { + triggerEvent: "BOOKING_REQUESTED", + payload: { + eventTitle: eventType.title, + eventDescription: eventType.description, + metadata: { + // In a Pending Booking Request, we don't send the video call url + }, + responses: { + name: { label: "your_name", value: booker.name }, + email: { label: "email_address", value: booker.email }, + location: { + label: "location", + value: { optionValue: "", value: location }, + }, + }, + }, + }); + } else { + expectWebhookToHaveBeenCalledWith(subscriberUrl, { + triggerEvent: "BOOKING_REQUESTED", + payload: { + eventTitle: eventType.title, + eventDescription: eventType.description, + metadata: { + // In a Pending Booking Request, we don't send the video call url + }, + responses: { + name: { label: "name", value: booker.name }, + email: { label: "email", value: booker.email }, + location: { + label: "location", + value: { optionValue: "", value: location }, + }, + }, + }, + }); + } +} + +export function expectBookingCreatedWebhookToHaveBeenFired({ + booker, + location, + subscriberUrl, + paidEvent, + videoCallUrl, +}: { + organizer: { email: string; name: string }; + booker: { email: string; name: string }; + subscriberUrl: string; + location: string; + paidEvent?: boolean; + videoCallUrl?: string; +}) { + if (!paidEvent) { + expectWebhookToHaveBeenCalledWith(subscriberUrl, { + triggerEvent: "BOOKING_CREATED", + payload: { + metadata: { + ...(videoCallUrl ? { videoCallUrl } : null), + }, + responses: { + name: { label: "your_name", value: booker.name }, + email: { label: "email_address", value: booker.email }, + location: { + label: "location", + value: { optionValue: "", value: location }, + }, + }, + }, + }); + } else { + expectWebhookToHaveBeenCalledWith(subscriberUrl, { + triggerEvent: "BOOKING_CREATED", + payload: { + // FIXME: File this bug and link ticket here. This is a bug in the code. metadata must be sent here like other BOOKING_CREATED webhook + metadata: null, + responses: { + name: { label: "name", value: booker.name }, + email: { label: "email", value: booker.email }, + location: { + label: "location", + value: { optionValue: "", value: location }, + }, + }, + }, + }); + } +} + +export function expectBookingRescheduledWebhookToHaveBeenFired({ + booker, + location, + subscriberUrl, + videoCallUrl, +}: { + organizer: { email: string; name: string }; + booker: { email: string; name: string }; + subscriberUrl: string; + location: string; + paidEvent?: boolean; + videoCallUrl?: string; +}) { + expectWebhookToHaveBeenCalledWith(subscriberUrl, { + triggerEvent: "BOOKING_RESCHEDULED", + payload: { + metadata: { + ...(videoCallUrl ? { videoCallUrl } : null), + }, + responses: { + name: { label: "your_name", value: booker.name }, + email: { label: "email_address", value: booker.email }, + location: { + label: "location", + value: { optionValue: "", value: location }, + }, + }, + }, + }); +} + +export function expectBookingPaymentIntiatedWebhookToHaveBeenFired({ + booker, + location, + subscriberUrl, + paymentId, +}: { + organizer: { email: string; name: string }; + booker: { email: string; name: string }; + subscriberUrl: string; + location: string; + paymentId: number; +}) { + expectWebhookToHaveBeenCalledWith(subscriberUrl, { + triggerEvent: "BOOKING_PAYMENT_INITIATED", + payload: { + paymentId: paymentId, + metadata: { + // In a Pending Booking Request, we don't send the video call url + }, + responses: { + name: { label: "your_name", value: booker.name }, + email: { label: "email_address", value: booker.email }, + location: { + label: "location", + value: { optionValue: "", value: location }, + }, + }, + }, + }); +} + +export function expectSuccessfulCalendarEventCreationInCalendar( + calendarMock: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + createEventCalls: any[]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + updateEventCalls: any[]; + }, + expected: { + externalCalendarId: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + calEvent: any; + uid: string; + } +) { + expect(calendarMock.createEventCalls.length).toBe(1); + const call = calendarMock.createEventCalls[0]; + const uid = call[0]; + const calendarEvent = call[1]; + const externalId = call[2]; + expect(uid).toBe(expected.uid); + expect(calendarEvent).toEqual(expect.objectContaining(expected.calEvent)); + expect(externalId).toBe(expected.externalCalendarId); +} + +export function expectSuccessfulCalendarEventUpdationInCalendar( + calendarMock: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + createEventCalls: any[]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + updateEventCalls: any[]; + }, + expected: { + externalCalendarId: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + calEvent: any; + uid: string; + } +) { + expect(calendarMock.updateEventCalls.length).toBe(1); + const call = calendarMock.updateEventCalls[0]; + const uid = call[0]; + const calendarEvent = call[1]; + const externalId = call[2]; + expect(uid).toBe(expected.uid); + expect(calendarEvent).toEqual(expect.objectContaining(expected.calEvent)); + expect(externalId).toBe(expected.externalCalendarId); +} + +export function expectSuccessfulVideoMeetingCreationInCalendar( + videoMock: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + createMeetingCalls: any[]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + updateMeetingCalls: any[]; + }, + expected: { + externalCalendarId: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + calEvent: any; + uid: string; + } +) { + expect(videoMock.createMeetingCalls.length).toBe(1); + const call = videoMock.createMeetingCalls[0]; + const uid = call[0]; + const calendarEvent = call[1]; + const externalId = call[2]; + expect(uid).toBe(expected.uid); + expect(calendarEvent).toEqual(expect.objectContaining(expected.calEvent)); + expect(externalId).toBe(expected.externalCalendarId); +} + +export function expectSuccessfulVideoMeetingUpdationInCalendar( + videoMock: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + createMeetingCalls: any[]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + updateMeetingCalls: any[]; + }, + expected: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + bookingRef: any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + calEvent: any; + } +) { + expect(videoMock.updateMeetingCalls.length).toBe(1); + const call = videoMock.updateMeetingCalls[0]; + const bookingRef = call[0]; + const calendarEvent = call[1]; + expect(bookingRef).toEqual(expect.objectContaining(expected.bookingRef)); + expect(calendarEvent).toEqual(expect.objectContaining(expected.calEvent)); +} diff --git a/package.json b/package.json index 6511ddb568..74de710939 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,7 @@ "lint-staged": "^12.5.0", "mailhog": "^4.16.0", "prettier": "^2.8.6", + "prismock": "^1.21.1", "tsc-absolute": "^1.0.0", "typescript": "^4.9.4", "vitest": "^0.34.3", diff --git a/packages/app-store/googlecalendar/lib/CalendarService.test.ts b/packages/app-store/googlecalendar/lib/CalendarService.test.ts index adf15d6120..7464e91a1b 100644 --- a/packages/app-store/googlecalendar/lib/CalendarService.test.ts +++ b/packages/app-store/googlecalendar/lib/CalendarService.test.ts @@ -1,4 +1,4 @@ -import prismaMock from "../../../../tests/libs/__mocks__/prisma"; +import prismaMock from "../../../../tests/libs/__mocks__/prismaMock"; import { afterEach, expect, test, vi } from "vitest"; diff --git a/packages/core/CalendarManager.ts b/packages/core/CalendarManager.ts index cac2f56696..0e91f10ea8 100644 --- a/packages/core/CalendarManager.ts +++ b/packages/core/CalendarManager.ts @@ -260,6 +260,9 @@ export const createEvent = async ( return undefined; }) : undefined; + if (!creationResult) { + logger.silly("createEvent failed", { success, uid, creationResult, originalEvent: calEvent, calError }); + } return { appName: credential.appId || "", diff --git a/packages/core/EventManager.ts b/packages/core/EventManager.ts index f7bf1b65b5..6993a50dad 100644 --- a/packages/core/EventManager.ts +++ b/packages/core/EventManager.ts @@ -80,6 +80,7 @@ export default class EventManager { * @param user */ constructor(user: EventManagerUser) { + logger.silly("Initializing EventManager", JSON.stringify({ user })); const appCredentials = getApps(user.credentials, true).flatMap((app) => app.credentials.map((creds) => ({ ...creds, appName: app.name })) ); @@ -312,7 +313,6 @@ export default class EventManager { }, }); } - return { results, referencesToCreate: [...booking.references], @@ -361,6 +361,7 @@ export default class EventManager { [] as DestinationCalendar[] ); for (const destination of destinationCalendars) { + logger.silly("Creating Calendar event", JSON.stringify({ destination })); if (destination.credentialId) { let credential = this.calendarCredentials.find((c) => c.id === destination.credentialId); if (!credential) { @@ -400,13 +401,21 @@ export default class EventManager { } } } else { + logger.silly( + "No destination Calendar found, falling back to first connected calendar", + JSON.stringify({ + calendarCredentials: this.calendarCredentials, + }) + ); + /** * Not ideal but, if we don't find a destination calendar, - * fallback to the first connected calendar + * fallback to the first connected calendar - Shouldn't be a CRM calendar */ - const [credential] = this.calendarCredentials.filter((cred) => cred.type === "calendar"); + const [credential] = this.calendarCredentials.filter((cred) => !cred.type.endsWith("other_calendar")); if (credential) { const createdEvent = await createEvent(credential, event); + logger.silly("Created Calendar event", { createdEvent }); if (createdEvent) { createdEvents.push(createdEvent); } @@ -503,6 +512,7 @@ export default class EventManager { ): Promise>> { let calendarReference: PartialReference[] | undefined = undefined, credential; + logger.silly("updateAllCalendarEvents", JSON.stringify({ event, booking, newBookingId })); try { // If a newBookingId is given, update that calendar event let newBooking; @@ -564,6 +574,7 @@ export default class EventManager { (credential) => credential.type === reference?.type ); for (const credential of credentials) { + logger.silly("updateAllCalendarEvents-credential", JSON.stringify({ credentials })); result.push(updateEvent(credential, event, bookingRefUid, calenderExternalId)); } } diff --git a/packages/core/videoClient.ts b/packages/core/videoClient.ts index 1f5a4d996b..8d3e44f4d4 100644 --- a/packages/core/videoClient.ts +++ b/packages/core/videoClient.ts @@ -24,6 +24,7 @@ const getVideoAdapters = async (withCredentials: CredentialPayload[]): Promise const createMeeting = async (credential: CredentialPayload, calEvent: CalendarEvent) => { const uid: string = getUid(calEvent); - + log.silly("videoClient:createMeeting", JSON.stringify({ credential, uid, calEvent })); if (!credential || !credential.appId) { throw new Error( "Credentials must be set! Video platforms are optional, so this method shouldn't even be called when no video credentials are set." @@ -116,21 +119,23 @@ const updateMeeting = async ( bookingRef: PartialReference | null ): Promise> => { const uid = translator.fromUUID(uuidv5(JSON.stringify(calEvent), uuidv5.URL)); - let success = true; - const [firstVideoAdapter] = await getVideoAdapters([credential]); - const updatedMeeting = - credential && bookingRef - ? await firstVideoAdapter?.updateMeeting(bookingRef, calEvent).catch(async (e) => { - await sendBrokenIntegrationEmail(calEvent, "video"); - log.error("updateMeeting failed", e, calEvent); - success = false; - return undefined; - }) - : undefined; + const canCallUpdateMeeting = !!(credential && bookingRef); + const updatedMeeting = canCallUpdateMeeting + ? await firstVideoAdapter?.updateMeeting(bookingRef, calEvent).catch(async (e) => { + await sendBrokenIntegrationEmail(calEvent, "video"); + log.error("updateMeeting failed", e, calEvent); + success = false; + return undefined; + }) + : undefined; if (!updatedMeeting) { + log.error( + "updateMeeting failed", + JSON.stringify({ bookingRef, canCallUpdateMeeting, calEvent, credential }) + ); return { appName: credential.appId || "", type: credential.type, diff --git a/packages/features/bookings/lib/doesBookingRequireConfirmation.ts b/packages/features/bookings/lib/doesBookingRequireConfirmation.ts new file mode 100644 index 0000000000..e29dac4635 --- /dev/null +++ b/packages/features/bookings/lib/doesBookingRequireConfirmation.ts @@ -0,0 +1,36 @@ +import type { z } from "zod"; + +import dayjs from "@calcom/dayjs"; +import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; + +/** + * Determines if a booking actually requires confirmation(considering requiresConfirmationThreshold) + */ +export const doesBookingRequireConfirmation = ({ + booking: { startTime, eventType }, +}: { + booking: { + startTime: Date; + eventType: { + requiresConfirmation?: boolean; + metadata: z.infer; + } | null; + }; +}) => { + let requiresConfirmation = eventType?.requiresConfirmation; + const rcThreshold = eventType?.metadata?.requiresConfirmationThreshold; + if (rcThreshold) { + // Convert startTime to UTC and create Day.js instances + const startTimeUTC = dayjs(startTime).utc(); + const currentTime = dayjs(); + + // Calculate the time difference in the specified unit + const timeDifference = startTimeUTC.diff(currentTime, rcThreshold.unit); + + // Check if the time difference exceeds the threshold + if (timeDifference > rcThreshold.time) { + requiresConfirmation = false; + } + } + return requiresConfirmation; +}; diff --git a/packages/features/bookings/lib/getWebhookPayloadForBooking.ts b/packages/features/bookings/lib/getWebhookPayloadForBooking.ts new file mode 100644 index 0000000000..a003fbdc35 --- /dev/null +++ b/packages/features/bookings/lib/getWebhookPayloadForBooking.ts @@ -0,0 +1,37 @@ +import type { EventTypeInfo } from "@calcom/features/webhooks/lib/sendPayload"; +import type { CalendarEvent } from "@calcom/types/Calendar"; + +export const getWebhookPayloadForBooking = ({ + booking, + evt, +}: { + booking: { + eventType: { + title: string; + description: string | null; + requiresConfirmation: boolean; + price: number; + currency: string; + length: number; + id: number; + } | null; + id: number; + eventTypeId: number | null; + userId: number | null; + }; + evt: CalendarEvent; +}) => { + const eventTypeInfo: EventTypeInfo = { + eventTitle: booking.eventType?.title, + eventDescription: booking.eventType?.description, + requiresConfirmation: booking.eventType?.requiresConfirmation || null, + price: booking.eventType?.price, + currency: booking.eventType?.currency, + length: booking.eventType?.length, + }; + return { + ...evt, + ...eventTypeInfo, + bookingId: booking.id, + }; +}; diff --git a/packages/features/bookings/lib/handleBookingRequested.ts b/packages/features/bookings/lib/handleBookingRequested.ts new file mode 100644 index 0000000000..e38f50d634 --- /dev/null +++ b/packages/features/bookings/lib/handleBookingRequested.ts @@ -0,0 +1,69 @@ +import { sendAttendeeRequestEmail, sendOrganizerRequestEmail } from "@calcom/emails"; +import { getWebhookPayloadForBooking } from "@calcom/features/bookings/lib/getWebhookPayloadForBooking"; +import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks"; +import sendPayload from "@calcom/features/webhooks/lib/sendPayload"; +import logger from "@calcom/lib/logger"; +import { WebhookTriggerEvents } from "@calcom/prisma/enums"; +import type { CalendarEvent } from "@calcom/types/Calendar"; + +const log = logger.getChildLogger({ prefix: ["[handleConfirmation] book:user"] }); + +/** + * Supposed to do whatever is needed when a booking is requested. + */ +export async function handleBookingRequested(args: { + evt: CalendarEvent; + booking: { + eventType: { + currency: string; + description: string | null; + id: number; + length: number; + price: number; + requiresConfirmation: boolean; + title: string; + teamId?: number | null; + } | null; + eventTypeId: number | null; + userId: number | null; + id: number; + }; +}) { + const { evt, booking } = args; + + await sendOrganizerRequestEmail({ ...evt }); + await sendAttendeeRequestEmail({ ...evt }, evt.attendees[0]); + + try { + const subscribersBookingRequested = await getWebhooks({ + userId: booking.userId, + eventTypeId: booking.eventTypeId, + triggerEvent: WebhookTriggerEvents.BOOKING_REQUESTED, + teamId: booking.eventType?.teamId, + }); + + const webhookPayload = getWebhookPayloadForBooking({ + booking, + evt, + }); + + const promises = subscribersBookingRequested.map((sub) => + sendPayload( + sub.secret, + WebhookTriggerEvents.BOOKING_REQUESTED, + new Date().toISOString(), + sub, + webhookPayload + ).catch((e) => { + console.error( + `Error executing webhook for event: ${WebhookTriggerEvents.BOOKING_REQUESTED}, URL: ${sub.subscriberUrl}`, + e + ); + }) + ); + await Promise.all(promises); + } catch (error) { + // Silently fail + log.error(error); + } +} diff --git a/packages/features/bookings/lib/handleNewBooking.test.ts b/packages/features/bookings/lib/handleNewBooking.test.ts index 3832ea172c..7ec4d42591 100644 --- a/packages/features/bookings/lib/handleNewBooking.test.ts +++ b/packages/features/bookings/lib/handleNewBooking.test.ts @@ -1,55 +1,74 @@ /** * How to ensure that unmocked prisma queries aren't called? */ +import prismaMock from "../../../../tests/libs/__mocks__/prisma"; + import type { Request, Response } from "express"; import type { NextApiRequest, NextApiResponse } from "next"; import { createMocks } from "node-mocks-http"; import { describe, expect, beforeEach } from "vitest"; import { WEBAPP_URL } from "@calcom/lib/constants"; +import logger from "@calcom/lib/logger"; import { BookingStatus } from "@calcom/prisma/enums"; import { test } from "@calcom/web/test/fixtures/fixtures"; import { createBookingScenario, getDate, - expectWorkflowToBeTriggered, getGoogleCalendarCredential, TestData, getOrganizer, getBooker, getScenarioData, - expectBookingToBeInDatabase, getZoomAppCredential, - mockEnableEmailFeature, + enableEmailFeature, mockNoTranslations, mockErrorOnVideoMeetingCreation, mockSuccessfulVideoMeetingCreation, mockCalendarToHaveNoBusySlots, - expectWebhookToHaveBeenCalledWith, + getStripeAppCredential, MockError, -} from "@calcom/web/test/utils/bookingScenario"; + mockPaymentApp, + mockPaymentSuccessWebhookFromStripe, +} from "@calcom/web/test/utils/bookingScenario/bookingScenario"; +import { + expectWorkflowToBeTriggered, + expectSuccessfulBookingCreationEmails, + expectBookingToBeInDatabase, + expectAwaitingPaymentEmails, + expectBookingRequestedEmails, + expectBookingRequestedWebhookToHaveBeenFired, + expectBookingCreatedWebhookToHaveBeenFired, + expectBookingPaymentIntiatedWebhookToHaveBeenFired, + expectBookingRescheduledWebhookToHaveBeenFired, + expectSuccessfulBookingRescheduledEmails, + expectSuccessfulCalendarEventUpdationInCalendar, + expectSuccessfulVideoMeetingUpdationInCalendar, +} from "@calcom/web/test/utils/bookingScenario/expects"; type CustomNextApiRequest = NextApiRequest & Request; + type CustomNextApiResponse = NextApiResponse & Response; // Local test runs sometime gets too slow const timeout = process.env.CI ? 5000 : 20000; - -describe.sequential("handleNewBooking", () => { +describe("handleNewBooking", () => { beforeEach(() => { // Required to able to generate token in email in some cases - process.env.CALENDSO_ENCRYPTION_KEY="abcdefghjnmkljhjklmnhjklkmnbhjui" + process.env.CALENDSO_ENCRYPTION_KEY = "abcdefghjnmkljhjklmnhjklkmnbhjui"; + process.env.STRIPE_WEBHOOK_SECRET = "MOCK_STRIPE_WEBHOOK_SECRET"; mockNoTranslations(); - mockEnableEmailFeature(); + // mockEnableEmailFeature(); + enableEmailFeature(); globalThis.testEmails = []; fetchMock.resetMocks(); }); - describe.sequential("Frontend:", () => { + describe("Fresh Booking:", () => { test( `should create a successful booking with Cal Video(Daily Video) if no explicit location is provided - 1. Should create a booking in the database - 2. Should send emails to the booker as well as organizer - 3. Should trigger BOOKING_CREATED webhook + 1. Should create a booking in the database + 2. Should send emails to the booker as well as organizer + 3. Should trigger BOOKING_CREATED webhook `, async ({ emails }) => { const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default; @@ -66,6 +85,49 @@ describe.sequential("handleNewBooking", () => { credentials: [getGoogleCalendarCredential()], selectedCalendars: [TestData.selectedCalendars.google], }); + await createBookingScenario( + getScenarioData({ + webhooks: [ + { + userId: organizer.id, + eventTriggers: ["BOOKING_CREATED"], + subscriberUrl: "http://my-webhook.example.com", + active: true, + eventTypeId: 1, + appId: null, + }, + ], + eventTypes: [ + { + id: 1, + slotInterval: 45, + length: 45, + users: [ + { + id: 101, + }, + ], + }, + ], + organizer, + apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]], + }) + ); + + mockSuccessfulVideoMeetingCreation({ + metadataLookupKey: "dailyvideo", + videoMeetingData: { + id: "MOCK_ID", + password: "MOCK_PASS", + url: `http://mock-dailyvideo.example.com`, + }, + }); + + mockCalendarToHaveNoBusySlots("googlecalendar", { + create: { + uid: "MOCK_ID", + }, + }); const mockBookingData = getMockRequestDataForBooking({ data: { @@ -83,40 +145,6 @@ describe.sequential("handleNewBooking", () => { body: mockBookingData, }); - const scenarioData = getScenarioData({ - webhooks: [ - { - userId: organizer.id, - eventTriggers: ["BOOKING_CREATED"], - subscriberUrl: "http://my-webhook.example.com", - active: true, - eventTypeId: 1, - appId: null, - }, - ], - eventTypes: [ - { - id: 1, - slotInterval: 45, - length: 45, - users: [ - { - id: 101, - }, - ], - }, - ], - organizer, - apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]], - }); - - mockSuccessfulVideoMeetingCreation({ - metadataLookupKey: "dailyvideo", - }); - - mockCalendarToHaveNoBusySlots("googlecalendar"); - createBookingScenario(scenarioData); - const createdBooking = await handleNewBooking(req); expect(createdBooking.responses).toContain({ email: booker.email, @@ -127,183 +155,384 @@ describe.sequential("handleNewBooking", () => { location: "integrations:daily", }); - expectBookingToBeInDatabase({ + await expectBookingToBeInDatabase({ description: "", - eventType: { - connect: { - id: mockBookingData.eventTypeId, - }, - }, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + uid: createdBooking.uid!, + eventTypeId: mockBookingData.eventTypeId, status: BookingStatus.ACCEPTED, + references: [ + { + type: "daily_video", + uid: "MOCK_ID", + meetingId: "MOCK_ID", + meetingPassword: "MOCK_PASS", + meetingUrl: "http://mock-dailyvideo.example.com", + }, + { + type: "google_calendar", + uid: "MOCK_ID", + meetingId: "MOCK_ID", + meetingPassword: "MOCK_PASSWORD", + meetingUrl: "https://UNUSED_URL", + }, + ], }); expectWorkflowToBeTriggered(); - const testEmails = emails.get(); - expect(testEmails[0]).toHaveEmail({ - htmlToContain: "confirmed_event_type_subject", - to: `${organizer.email}`, - }); - expect(testEmails[1]).toHaveEmail({ - htmlToContain: "confirmed_event_type_subject", - to: `${booker.name} <${booker.email}>`, - }); - expect(testEmails[1].html).toContain("confirmed_event_type_subject"); - expectWebhookToHaveBeenCalledWith("http://my-webhook.example.com", { - triggerEvent: "BOOKING_CREATED", - payload: { - metadata: { - videoCallUrl: `${WEBAPP_URL}/video/DYNAMIC_UID`, - }, - responses: { - name: { label: "your_name", value: "Booker" }, - email: { label: "email_address", value: "booker@example.com" }, - location: { - label: "location", - value: { optionValue: "", value: "integrations:daily" }, - }, - title: { label: "what_is_this_meeting_about" }, - notes: { label: "additional_notes" }, - guests: { label: "additional_guests" }, - rescheduleReason: { label: "reason_for_reschedule" }, - }, - }, + expectSuccessfulBookingCreationEmails({ booker, organizer, emails }); + expectBookingCreatedWebhookToHaveBeenFired({ + booker, + organizer, + location: "integrations:daily", + subscriberUrl: "http://my-webhook.example.com", + videoCallUrl: `${WEBAPP_URL}/video/DYNAMIC_UID`, }); }, timeout ); - test( - `should submit a booking request for event requiring confirmation - 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 + describe("Event Type that requires confirmation", () => { + test( + `should create a booking request for event that requires confirmation + 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 `, - async ({ emails }) => { - const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default; - const booker = getBooker({ - email: "booker@example.com", - name: "Booker", - }); + async ({ emails }) => { + const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default; + const subscriberUrl = "http://my-webhook.example.com"; + const booker = getBooker({ + email: "booker@example.com", + name: "Booker", + }); - const organizer = getOrganizer({ - name: "Organizer", - email: "organizer@example.com", - id: 101, - schedules: [TestData.schedules.IstWorkHours], - credentials: [getGoogleCalendarCredential()], - selectedCalendars: [TestData.selectedCalendars.google], - }); + const organizer = getOrganizer({ + name: "Organizer", + email: "organizer@example.com", + id: 101, + schedules: [TestData.schedules.IstWorkHours], + credentials: [getGoogleCalendarCredential()], + selectedCalendars: [TestData.selectedCalendars.google], + }); + const scenarioData = getScenarioData({ + webhooks: [ + { + userId: organizer.id, + eventTriggers: ["BOOKING_CREATED"], + subscriberUrl, + active: true, + eventTypeId: 1, + appId: null, + }, + ], + eventTypes: [ + { + id: 1, + slotInterval: 45, + requiresConfirmation: true, + length: 45, + users: [ + { + id: 101, + }, + ], + }, + ], + organizer, + apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]], + }); + await createBookingScenario(scenarioData); - const mockBookingData = getMockRequestDataForBooking({ - data: { - eventTypeId: 1, - responses: { - email: booker.email, - name: booker.name, - location: { optionValue: "", value: "integrations:daily" }, - }, - }, - }); + mockSuccessfulVideoMeetingCreation({ + metadataLookupKey: "dailyvideo", + }); - const { req } = createMockNextJsRequest({ - method: "POST", - body: mockBookingData, - }); + mockCalendarToHaveNoBusySlots("googlecalendar"); - const scenarioData = getScenarioData({ - webhooks: [ - { - userId: organizer.id, - eventTriggers: ["BOOKING_CREATED"], - subscriberUrl: "http://my-webhook.example.com", - active: true, + const mockBookingData = getMockRequestDataForBooking({ + data: { eventTypeId: 1, - appId: null, + responses: { + email: booker.email, + name: booker.name, + location: { optionValue: "", value: "integrations:daily" }, + }, }, - ], - eventTypes: [ - { - id: 1, - slotInterval: 45, - requiresConfirmation: true, - length: 45, - users: [ + }); + + const { req } = createMockNextJsRequest({ + method: "POST", + body: mockBookingData, + }); + + const createdBooking = await handleNewBooking(req); + expect(createdBooking.responses).toContain({ + email: booker.email, + name: booker.name, + }); + + expect(createdBooking).toContain({ + location: "integrations:daily", + }); + + await expectBookingToBeInDatabase({ + description: "", + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + uid: createdBooking.uid!, + eventTypeId: mockBookingData.eventTypeId, + status: BookingStatus.PENDING, + }); + + expectWorkflowToBeTriggered(); + + expectBookingRequestedEmails({ + booker, + organizer, + emails, + }); + + expectBookingRequestedWebhookToHaveBeenFired({ + booker, + organizer, + location: "integrations:daily", + subscriberUrl, + eventType: scenarioData.eventTypes[0], + }); + }, + timeout + ); + + test( + `should create a booking for event that requires confirmation based on a booking notice duration threshold, if threshold is not met + 1. Should create a booking in the database with status ACCEPTED + 2. Should send emails to the booker as well as organizer + 3. Should trigger BOOKING_CREATED webhook + `, + async ({ emails }) => { + const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default; + const booker = getBooker({ + email: "booker@example.com", + name: "Booker", + }); + const subscriberUrl = "http://my-webhook.example.com"; + + const organizer = getOrganizer({ + name: "Organizer", + email: "organizer@example.com", + id: 101, + schedules: [TestData.schedules.IstWorkHours], + credentials: [getGoogleCalendarCredential()], + selectedCalendars: [TestData.selectedCalendars.google], + }); + + await createBookingScenario( + getScenarioData({ + webhooks: [ { - id: 101, + userId: organizer.id, + eventTriggers: ["BOOKING_CREATED"], + subscriberUrl, + active: true, + eventTypeId: 1, + appId: null, }, ], - }, - ], - organizer, - apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]], - }); + eventTypes: [ + { + id: 1, + slotInterval: 45, + requiresConfirmation: true, + metadata: { + requiresConfirmationThreshold: { + time: 30, + unit: "minutes", + }, + }, + length: 45, + users: [ + { + id: 101, + }, + ], + }, + ], + organizer, + apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]], + }) + ); - mockSuccessfulVideoMeetingCreation({ - metadataLookupKey: "dailyvideo", - }); + mockSuccessfulVideoMeetingCreation({ + metadataLookupKey: "dailyvideo", + }); - mockCalendarToHaveNoBusySlots("googlecalendar"); - createBookingScenario(scenarioData); + mockCalendarToHaveNoBusySlots("googlecalendar"); - const createdBooking = await handleNewBooking(req); - expect(createdBooking.responses).toContain({ - email: booker.email, - name: booker.name, - }); - - expect(createdBooking).toContain({ - location: "integrations:daily", - }); - - expectBookingToBeInDatabase({ - description: "", - eventType: { - connect: { - id: mockBookingData.eventTypeId, - }, - }, - status: BookingStatus.PENDING, - }); - - expectWorkflowToBeTriggered(); - - const testEmails = emails.get(); - expect(testEmails[0]).toHaveEmail({ - htmlToContain: "event_awaiting_approval_subject", - to: `${organizer.email}`, - }); - - expect(testEmails[1]).toHaveEmail({ - htmlToContain: "booking_submitted_subject", - to: `${booker.email}`, - }); - - expectWebhookToHaveBeenCalledWith("http://my-webhook.example.com", { - triggerEvent: "BOOKING_REQUESTED", - payload: { - metadata: { - // In a Pending Booking Request, we don't send the video call url - videoCallUrl: undefined, - }, - responses: { - name: { label: "your_name", value: "Booker" }, - email: { label: "email_address", value: "booker@example.com" }, - location: { - label: "location", - value: { optionValue: "", value: "integrations:daily" }, + const mockBookingData = getMockRequestDataForBooking({ + data: { + eventTypeId: 1, + responses: { + email: booker.email, + name: booker.name, + location: { optionValue: "", value: "integrations:daily" }, }, - title: { label: "what_is_this_meeting_about" }, - notes: { label: "additional_notes" }, - guests: { label: "additional_guests" }, - rescheduleReason: { label: "reason_for_reschedule" }, }, - }, - }); - }, - timeout - ); + }); + const { req } = createMockNextJsRequest({ + method: "POST", + body: mockBookingData, + }); + + const createdBooking = await handleNewBooking(req); + expect(createdBooking.responses).toContain({ + email: booker.email, + name: booker.name, + }); + + expect(createdBooking).toContain({ + location: "integrations:daily", + }); + + await expectBookingToBeInDatabase({ + description: "", + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + uid: createdBooking.uid!, + eventTypeId: mockBookingData.eventTypeId, + status: BookingStatus.ACCEPTED, + }); + + expectWorkflowToBeTriggered(); + + expectSuccessfulBookingCreationEmails({ booker, organizer, emails }); + + expectBookingCreatedWebhookToHaveBeenFired({ + booker, + organizer, + location: "integrations:daily", + subscriberUrl, + videoCallUrl: `${WEBAPP_URL}/video/DYNAMIC_UID`, + }); + }, + timeout + ); + + test( + `should create a booking for event that requires confirmation based on a booking notice duration threshold, if threshold IS MET + 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 + `, + async ({ emails }) => { + const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default; + const subscriberUrl = "http://my-webhook.example.com"; + const booker = getBooker({ + email: "booker@example.com", + name: "Booker", + }); + + const organizer = getOrganizer({ + name: "Organizer", + email: "organizer@example.com", + id: 101, + schedules: [TestData.schedules.IstWorkHours], + credentials: [getGoogleCalendarCredential()], + selectedCalendars: [TestData.selectedCalendars.google], + }); + const scenarioData = getScenarioData({ + webhooks: [ + { + userId: organizer.id, + eventTriggers: ["BOOKING_CREATED"], + subscriberUrl, + active: true, + eventTypeId: 1, + appId: null, + }, + ], + eventTypes: [ + { + id: 1, + slotInterval: 45, + requiresConfirmation: true, + metadata: { + requiresConfirmationThreshold: { + time: 120, + unit: "hours", + }, + }, + length: 45, + users: [ + { + id: 101, + }, + ], + }, + ], + organizer, + apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]], + }); + + await createBookingScenario(scenarioData); + + mockSuccessfulVideoMeetingCreation({ + metadataLookupKey: "dailyvideo", + }); + + mockCalendarToHaveNoBusySlots("googlecalendar"); + + const mockBookingData = getMockRequestDataForBooking({ + data: { + eventTypeId: 1, + responses: { + email: booker.email, + name: booker.name, + location: { optionValue: "", value: "integrations:daily" }, + }, + }, + }); + + const { req } = createMockNextJsRequest({ + method: "POST", + body: mockBookingData, + }); + + const createdBooking = await handleNewBooking(req); + expect(createdBooking.responses).toContain({ + email: booker.email, + name: booker.name, + }); + + expect(createdBooking).toContain({ + location: "integrations:daily", + }); + + await expectBookingToBeInDatabase({ + description: "", + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + uid: createdBooking.uid!, + eventTypeId: mockBookingData.eventTypeId, + status: BookingStatus.PENDING, + }); + + expectWorkflowToBeTriggered(); + + expectBookingRequestedEmails({ booker, organizer, emails }); + + expectBookingRequestedWebhookToHaveBeenFired({ + booker, + organizer, + location: "integrations:daily", + subscriberUrl, + eventType: scenarioData.eventTypes[0], + }); + }, + timeout + ); + }); + + // FIXME: We shouldn't throw error here, the behaviour should be fixed. test( `if booking with Cal Video(Daily Video) fails, booking creation fails with uncaught error`, async ({}) => { @@ -313,22 +542,8 @@ describe.sequential("handleNewBooking", () => { name: "Booker", }); const organizer = TestData.users.example; - const { req } = createMockNextJsRequest({ - method: "POST", - body: getMockRequestDataForBooking({ - data: { - eventTypeId: 1, - responses: { - email: booker.email, - name: booker.name, - location: { optionValue: "", value: "integrations:daily" }, - }, - }, - }), - }); - const scenarioData = { - hosts: [], + await createBookingScenario({ eventTypes: [ { id: 1, @@ -351,14 +566,27 @@ describe.sequential("handleNewBooking", () => { }, ], apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]], - }; + }); mockErrorOnVideoMeetingCreation({ metadataLookupKey: "dailyvideo", }); + mockCalendarToHaveNoBusySlots("googlecalendar"); - createBookingScenario(scenarioData); + const { req } = createMockNextJsRequest({ + method: "POST", + body: getMockRequestDataForBooking({ + data: { + eventTypeId: 1, + responses: { + email: booker.email, + name: booker.name, + location: { optionValue: "", value: "integrations:daily" }, + }, + }, + }), + }); try { await handleNewBooking(req); @@ -374,6 +602,7 @@ describe.sequential("handleNewBooking", () => { `should create a successful booking with Zoom if used`, async ({ emails }) => { const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default; + const subscriberUrl = "http://my-webhook.example.com"; const booker = getBooker({ email: "booker@example.com", name: "Booker", @@ -387,6 +616,37 @@ describe.sequential("handleNewBooking", () => { credentials: [getZoomAppCredential()], selectedCalendars: [TestData.selectedCalendars.google], }); + await createBookingScenario( + getScenarioData({ + organizer, + eventTypes: [ + { + id: 1, + slotInterval: 45, + length: 45, + users: [ + { + id: 101, + }, + ], + }, + ], + apps: [TestData.apps["zoomvideo"]], + webhooks: [ + { + userId: organizer.id, + eventTriggers: ["BOOKING_CREATED"], + subscriberUrl, + active: true, + eventTypeId: 1, + appId: null, + }, + ], + }) + ); + mockSuccessfulVideoMeetingCreation({ + metadataLookupKey: "zoomvideo", + }); const { req } = createMockNextJsRequest({ method: "POST", @@ -401,71 +661,16 @@ describe.sequential("handleNewBooking", () => { }, }), }); - - const bookingScenario = getScenarioData({ - organizer, - eventTypes: [ - { - id: 1, - slotInterval: 45, - length: 45, - users: [ - { - id: 101, - }, - ], - }, - ], - apps: [TestData.apps["daily-video"]], - webhooks: [ - { - userId: organizer.id, - eventTriggers: ["BOOKING_CREATED"], - subscriberUrl: "http://my-webhook.example.com", - active: true, - eventTypeId: 1, - appId: null, - }, - ], - }); - - createBookingScenario(bookingScenario); - mockSuccessfulVideoMeetingCreation({ - metadataLookupKey: "zoomvideo", - }); await handleNewBooking(req); - const testEmails = emails.get(); + expectSuccessfulBookingCreationEmails({ booker, organizer, emails }); - expect(testEmails[0]).toHaveEmail({ - htmlToContain: "confirmed_event_type_subject", - to: `${organizer.email}`, - }); - - expect(testEmails[1]).toHaveEmail({ - htmlToContain: "confirmed_event_type_subject", - to: `${booker.name} <${booker.email}>`, - }); - - expectWebhookToHaveBeenCalledWith("http://my-webhook.example.com", { - triggerEvent: "BOOKING_CREATED", - payload: { - metadata: { - videoCallUrl: "http://mock-zoomvideo.example.com", - }, - responses: { - name: { label: "your_name", value: "Booker" }, - email: { label: "email_address", value: "booker@example.com" }, - location: { - label: "location", - value: { optionValue: "", value: "integrations:zoom" }, - }, - title: { label: "what_is_this_meeting_about" }, - notes: { label: "additional_notes" }, - guests: { label: "additional_guests" }, - rescheduleReason: { label: "reason_for_reschedule" }, - }, - }, + expectBookingCreatedWebhookToHaveBeenFired({ + booker, + organizer, + location: "integrations:zoom", + subscriberUrl, + videoCallUrl: "http://mock-zoomvideo.example.com", }); }, timeout @@ -536,7 +741,7 @@ describe.sequential("handleNewBooking", () => { }); mockCalendarToHaveNoBusySlots("googlecalendar"); - createBookingScenario(scenarioData); + await createBookingScenario(scenarioData); const createdBooking = await handleNewBooking(req); expect(createdBooking.responses).toContain({ @@ -548,47 +753,519 @@ describe.sequential("handleNewBooking", () => { location: "New York", }); - expectBookingToBeInDatabase({ + await expectBookingToBeInDatabase({ description: "", - eventType: { - connect: { - id: mockBookingData.eventTypeId, - }, - }, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + uid: createdBooking.uid!, + eventTypeId: mockBookingData.eventTypeId, status: BookingStatus.ACCEPTED, }); expectWorkflowToBeTriggered(); - const testEmails = emails.get(); - expect(testEmails[0]).toHaveEmail({ - htmlToContain: "confirmed_event_type_subject", - to: `${organizer.email}`, + expectSuccessfulBookingCreationEmails({ booker, organizer, emails }); + expectBookingCreatedWebhookToHaveBeenFired({ + booker, + organizer, + location: "New York", + subscriberUrl: "http://my-webhook.example.com", }); - expect(testEmails[1]).toHaveEmail({ - htmlToContain: "confirmed_event_type_subject", - to: `${booker.name} <${booker.email}>`, - }); - expect(testEmails[1].html).toContain("confirmed_event_type_subject"); - expectWebhookToHaveBeenCalledWith("http://my-webhook.example.com", { - triggerEvent: "BOOKING_CREATED", - payload: { - metadata: { - }, - responses: { - name: { label: "your_name", value: "Booker" }, - email: { label: "email_address", value: "booker@example.com" }, - location: { - label: "location", - value: { optionValue: "", value: "New York" }, + }, + timeout + ); + + describe("Paid Events", () => { + test( + `Event Type that doesn't require confirmation + 1. Should create a booking in the database with status PENDING + 2. Should send email to the booker for Payment request + 3. Should trigger BOOKING_PAYMENT_INITIATED webhook + 4. Once payment is successful, should trigger BOOKING_CREATED webhook + `, + async ({ emails }) => { + const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default; + const booker = getBooker({ + email: "booker@example.com", + name: "Booker", + }); + + const organizer = getOrganizer({ + name: "Organizer", + email: "organizer@example.com", + id: 101, + schedules: [TestData.schedules.IstWorkHours], + credentials: [getGoogleCalendarCredential(), getStripeAppCredential()], + selectedCalendars: [TestData.selectedCalendars.google], + }); + const scenarioData = getScenarioData({ + webhooks: [ + { + userId: organizer.id, + eventTriggers: ["BOOKING_CREATED"], + subscriberUrl: "http://my-webhook.example.com", + active: true, + eventTypeId: 1, + appId: null, }, - title: { label: "what_is_this_meeting_about" }, - notes: { label: "additional_notes" }, - guests: { label: "additional_guests" }, - rescheduleReason: { label: "reason_for_reschedule" }, + ], + eventTypes: [ + { + id: 1, + title: "Paid Event", + description: "It's a test Paid Event", + slotInterval: 45, + requiresConfirmation: false, + metadata: { + apps: { + // EventType is connected to stripe. + stripe: { + price: 100, + enabled: true, + currency: "inr" /*, credentialId: 57*/, + }, + }, + }, + length: 45, + users: [ + { + id: 101, + }, + ], + }, + ], + organizer, + apps: [ + TestData.apps["google-calendar"], + TestData.apps["daily-video"], + TestData.apps["stripe-payment"], + ], + }); + await createBookingScenario(scenarioData); + mockSuccessfulVideoMeetingCreation({ + metadataLookupKey: "dailyvideo", + }); + const { paymentUid, externalId } = mockPaymentApp({ + metadataLookupKey: "stripe", + appStoreLookupKey: "stripepayment", + }); + mockCalendarToHaveNoBusySlots("googlecalendar"); + const mockBookingData = getMockRequestDataForBooking({ + data: { + eventTypeId: 1, + responses: { + email: booker.email, + name: booker.name, + location: { optionValue: "", value: "integrations:daily" }, + }, + }, + }); + + const { req } = createMockNextJsRequest({ + method: "POST", + body: mockBookingData, + }); + const createdBooking = await handleNewBooking(req); + + expect(createdBooking.responses).toContain({ + email: booker.email, + name: booker.name, + }); + expect(createdBooking).toContain({ + location: "integrations:daily", + paymentUid: paymentUid, + }); + await expectBookingToBeInDatabase({ + description: "", + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + uid: createdBooking.uid!, + eventTypeId: mockBookingData.eventTypeId, + status: BookingStatus.PENDING, + }); + expectWorkflowToBeTriggered(); + expectAwaitingPaymentEmails({ organizer, booker, emails }); + + expectBookingPaymentIntiatedWebhookToHaveBeenFired({ + booker, + organizer, + location: "integrations:daily", + subscriberUrl: "http://my-webhook.example.com", + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + paymentId: createdBooking.paymentId!, + }); + + const { webhookResponse } = await mockPaymentSuccessWebhookFromStripe({ externalId }); + + logger.info("webhookResponse", webhookResponse); + expect(webhookResponse?.statusCode).toBe(200); + await expectBookingToBeInDatabase({ + description: "", + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + uid: createdBooking.uid!, + eventTypeId: mockBookingData.eventTypeId, + status: BookingStatus.ACCEPTED, + }); + + expectBookingCreatedWebhookToHaveBeenFired({ + booker, + organizer, + location: "integrations:daily", + subscriberUrl: "http://my-webhook.example.com", + videoCallUrl: `${WEBAPP_URL}/video/DYNAMIC_UID`, + paidEvent: true, + }); + }, + timeout + ); + // TODO: We should introduce a new state BOOKING.PAYMENT_PENDING that can clearly differentiate b/w pending confirmation(stuck on Organizer) and pending payment(stuck on booker) + test( + `Event Type that requires confirmation + 1. Should create a booking in the database with status PENDING + 2. Should send email to the booker for Payment request + 3. Should trigger BOOKING_PAYMENT_INITIATED webhook + 4. Once payment is successful, should trigger BOOKING_REQUESTED webhook + 5. Booking should still stay in pending state + `, + async ({ emails }) => { + const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default; + const subscriberUrl = "http://my-webhook.example.com"; + const booker = getBooker({ + email: "booker@example.com", + name: "Booker", + }); + + const organizer = getOrganizer({ + name: "Organizer", + email: "organizer@example.com", + id: 101, + schedules: [TestData.schedules.IstWorkHours], + credentials: [getGoogleCalendarCredential(), getStripeAppCredential()], + selectedCalendars: [TestData.selectedCalendars.google], + }); + + const scenarioData = getScenarioData({ + webhooks: [ + { + userId: organizer.id, + eventTriggers: ["BOOKING_CREATED"], + subscriberUrl, + active: true, + eventTypeId: 1, + appId: null, + }, + ], + eventTypes: [ + { + id: 1, + slotInterval: 45, + requiresConfirmation: true, + metadata: { + apps: { + stripe: { + price: 100, + enabled: true, + currency: "inr" /*, credentialId: 57*/, + }, + }, + }, + length: 45, + users: [ + { + id: 101, + }, + ], + }, + ], + organizer, + apps: [ + TestData.apps["google-calendar"], + TestData.apps["daily-video"], + TestData.apps["stripe-payment"], + ], + }); + await createBookingScenario(scenarioData); + mockSuccessfulVideoMeetingCreation({ + metadataLookupKey: "dailyvideo", + }); + const { paymentUid, externalId } = mockPaymentApp({ + metadataLookupKey: "stripe", + appStoreLookupKey: "stripepayment", + }); + mockCalendarToHaveNoBusySlots("googlecalendar"); + + const mockBookingData = getMockRequestDataForBooking({ + data: { + eventTypeId: 1, + responses: { + email: booker.email, + name: booker.name, + location: { optionValue: "", value: "integrations:daily" }, + }, + }, + }); + const { req } = createMockNextJsRequest({ + method: "POST", + body: mockBookingData, + }); + const createdBooking = await handleNewBooking(req); + + expect(createdBooking.responses).toContain({ + email: booker.email, + name: booker.name, + }); + expect(createdBooking).toContain({ + location: "integrations:daily", + paymentUid: paymentUid, + }); + await expectBookingToBeInDatabase({ + description: "", + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + uid: createdBooking.uid!, + eventTypeId: mockBookingData.eventTypeId, + status: BookingStatus.PENDING, + }); + expectWorkflowToBeTriggered(); + expectAwaitingPaymentEmails({ organizer, booker, emails }); + expectBookingPaymentIntiatedWebhookToHaveBeenFired({ + booker, + organizer, + location: "integrations:daily", + subscriberUrl: "http://my-webhook.example.com", + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + paymentId: createdBooking.paymentId!, + }); + + const { webhookResponse } = await mockPaymentSuccessWebhookFromStripe({ externalId }); + + expect(webhookResponse?.statusCode).toBe(200); + await expectBookingToBeInDatabase({ + description: "", + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + uid: createdBooking.uid!, + eventTypeId: mockBookingData.eventTypeId, + status: BookingStatus.PENDING, + }); + expectBookingRequestedWebhookToHaveBeenFired({ + booker, + organizer, + location: "integrations:daily", + subscriberUrl, + paidEvent: true, + eventType: scenarioData.eventTypes[0], + }); + }, + timeout + ); + }); + }); + + describe("Reschedule", () => { + test( + `should rechedule a booking successfully with Cal Video(Daily Video) if no explicit location is provided + 1. Should cancel the booking + 2. Should create a new booking in the database + 3. Should send emails to the booker as well as organizer + 4. Should trigger BOOKING_RESCHEDULED webhook + `, + async ({ emails }) => { + const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default; + const booker = getBooker({ + email: "booker@example.com", + name: "Booker", + }); + + const organizer = getOrganizer({ + name: "Organizer", + email: "organizer@example.com", + id: 101, + schedules: [TestData.schedules.IstWorkHours], + credentials: [getGoogleCalendarCredential()], + selectedCalendars: [TestData.selectedCalendars.google], + }); + + const { dateString: plus1DateString } = getDate({ dateIncrement: 1 }); + const uidOfBookingToBeRescheduled = "n5Wv3eHgconAED2j4gcVhP"; + await createBookingScenario( + getScenarioData({ + webhooks: [ + { + userId: organizer.id, + eventTriggers: ["BOOKING_CREATED"], + subscriberUrl: "http://my-webhook.example.com", + active: true, + eventTypeId: 1, + appId: null, + }, + ], + eventTypes: [ + { + id: 1, + slotInterval: 45, + length: 45, + users: [ + { + id: 101, + }, + ], + }, + ], + bookings: [ + { + uid: uidOfBookingToBeRescheduled, + eventTypeId: 1, + status: BookingStatus.ACCEPTED, + startTime: `${plus1DateString}T05:00:00.000Z`, + endTime: `${plus1DateString}T05:15:00.000Z`, + references: [ + { + type: "daily_video", + uid: "MOCK_ID", + meetingId: "MOCK_ID", + meetingPassword: "MOCK_PASS", + meetingUrl: "http://mock-dailyvideo.example.com", + }, + { + type: "google_calendar", + uid: "MOCK_ID", + meetingId: "MOCK_ID", + meetingPassword: "MOCK_PASSWORD", + meetingUrl: "https://UNUSED_URL", + externalCalendarId: "MOCK_EXTERNAL_CALENDAR_ID", + credentialId: undefined, + }, + ], + }, + ], + organizer, + apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]], + }) + ); + + const videoMock = mockSuccessfulVideoMeetingCreation({ + metadataLookupKey: "dailyvideo", + }); + + const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", { + create: { + uid: "MOCK_ID", + }, + update: { + uid: "UPDATED_MOCK_ID", + }, + }); + + const mockBookingData = getMockRequestDataForBooking({ + data: { + eventTypeId: 1, + rescheduleUid: uidOfBookingToBeRescheduled, + start: `${plus1DateString}T04:00:00.000Z`, + end: `${plus1DateString}T04:15:00.000Z`, + responses: { + email: booker.email, + name: booker.name, + location: { optionValue: "", value: "integrations:daily" }, }, }, }); + + const { req } = createMockNextJsRequest({ + method: "POST", + body: mockBookingData, + }); + + const createdBooking = await handleNewBooking(req); + + const previousBooking = await prismaMock.booking.findUnique({ + where: { + uid: uidOfBookingToBeRescheduled, + }, + }); + + logger.silly({ + previousBooking, + allBookings: await prismaMock.booking.findMany(), + }); + + // Expect previous booking to be cancelled + await expectBookingToBeInDatabase({ + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + uid: uidOfBookingToBeRescheduled, + status: BookingStatus.CANCELLED, + }); + + expect(previousBooking?.status).toBe(BookingStatus.CANCELLED); + /** + * Booking Time should be new time + */ + expect(createdBooking.startTime?.toISOString()).toBe(`${plus1DateString}T04:00:00.000Z`); + expect(createdBooking.endTime?.toISOString()).toBe(`${plus1DateString}T04:15:00.000Z`); + + expect(createdBooking.responses).toContain({ + email: booker.email, + name: booker.name, + }); + + expect(createdBooking).toContain({ + location: "integrations:daily", + }); + + // Expect new booking to be there. + await expectBookingToBeInDatabase({ + description: "", + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + uid: createdBooking.uid!, + eventTypeId: mockBookingData.eventTypeId, + status: BookingStatus.ACCEPTED, + references: [ + { + type: "daily_video", + uid: "MOCK_ID", + meetingId: "MOCK_ID", + meetingPassword: "MOCK_PASS", + meetingUrl: "http://mock-dailyvideo.example.com", + }, + { + type: "google_calendar", + // IssueToBeFiled-Hariom: It isn' UPDATED_MOCK_ID as references are due to some reason intentionally kept the same after reschedule. See https://github.com/calcom/cal.com/blob/57b48b0a90e13b9eefc1a93abc0044633561b515/packages/core/EventManager.ts#L317 + uid: "MOCK_ID", + meetingId: "MOCK_ID", + meetingPassword: "MOCK_PASSWORD", + meetingUrl: "https://UNUSED_URL", + externalCalendarId: "MOCK_EXTERNAL_CALENDAR_ID", + }, + ], + }); + + expectWorkflowToBeTriggered(); + + expectSuccessfulVideoMeetingUpdationInCalendar(videoMock, { + calEvent: { + location: "http://mock-dailyvideo.example.com", + }, + bookingRef: { + type: "daily_video", + uid: "MOCK_ID", + meetingId: "MOCK_ID", + meetingPassword: "MOCK_PASS", + meetingUrl: "http://mock-dailyvideo.example.com", + }, + }); + + expectSuccessfulCalendarEventUpdationInCalendar(calendarMock, { + externalCalendarId: "MOCK_EXTERNAL_CALENDAR_ID", + calEvent: { + location: "http://mock-dailyvideo.example.com", + }, + uid: "MOCK_ID", + }); + + expectSuccessfulBookingRescheduledEmails({ booker, organizer, emails }); + expectBookingRescheduledWebhookToHaveBeenFired({ + booker, + organizer, + location: "integrations:daily", + subscriberUrl: "http://my-webhook.example.com", + videoCallUrl: `${WEBAPP_URL}/video/DYNAMIC_UID`, + }); }, timeout ); @@ -606,7 +1283,6 @@ function getBasicMockRequestDataForBooking() { eventTypeSlug: "no-confirmation", timeZone: "Asia/Calcutta", language: "en", - bookingUid: "bvCmP5rSquAazGSA7hz7ZP", user: "teampro", metadata: {}, hasHashedBookingLink: false, @@ -619,6 +1295,8 @@ function getMockRequestDataForBooking({ }: { data: Partial> & { eventTypeId: number; + rescheduleUid?: string; + bookingUid?: string; responses: { email: string; name: string; diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index 0a134c7586..7e97c121ba 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -1210,8 +1210,6 @@ async function handler( teamId, }; - const subscribersMeetingEnded = await getWebhooks(subscriberOptionsMeetingEnded); - const handleSeats = async () => { let resultBooking: | (Partial & { @@ -1219,6 +1217,7 @@ async function handler( seatReferenceUid?: string; paymentUid?: string; message?: string; + paymentId?: number; }) | null = null; @@ -1762,6 +1761,7 @@ async function handler( resultBooking = { ...foundBooking }; resultBooking["message"] = "Payment required"; resultBooking["paymentUid"] = payment?.uid; + resultBooking["id"] = payment?.id; } else { resultBooking = { ...foundBooking }; } @@ -2082,7 +2082,9 @@ async function handler( } let videoCallUrl; + if (originalRescheduledBooking?.uid) { + log.silly("Rescheduling booking", originalRescheduledBooking.uid); try { // cancel workflow reminders from previous rescheduled booking await cancelWorkflowReminders(originalRescheduledBooking.workflowReminders); @@ -2288,6 +2290,27 @@ async function handler( await sendOrganizerRequestEmail({ ...evt, additionalNotes }); await sendAttendeeRequestEmail({ ...evt, additionalNotes }, attendeesList[0]); } + const metadata = videoCallUrl + ? { + videoCallUrl: getVideoCallUrlFromCalEvent(evt), + } + : undefined; + const webhookData = { + ...evt, + ...eventTypeInfo, + bookingId: booking?.id, + rescheduleUid, + rescheduleStartTime: originalRescheduledBooking?.startTime + ? dayjs(originalRescheduledBooking?.startTime).utc().format() + : undefined, + rescheduleEndTime: originalRescheduledBooking?.endTime + ? dayjs(originalRescheduledBooking?.endTime).utc().format() + : undefined, + metadata: { ...metadata, ...reqBody.metadata }, + eventTypeId, + status: "ACCEPTED", + smsReminderNumber: booking?.smsReminderNumber || undefined, + }; if (bookingRequiresPayment) { // Load credentials.app.categories @@ -2329,9 +2352,23 @@ async function handler( fullName, bookerEmail ); + const subscriberOptionsPaymentInitiated: GetSubscriberOptions = { + userId: triggerForUser ? organizerUser.id : null, + eventTypeId, + triggerEvent: WebhookTriggerEvents.BOOKING_PAYMENT_INITIATED, + teamId, + }; + await handleWebhookTrigger({ + subscriberOptions: subscriberOptionsPaymentInitiated, + eventTrigger: WebhookTriggerEvents.BOOKING_PAYMENT_INITIATED, + webhookData: { + ...webhookData, + paymentId: payment?.id, + }, + }); req.statusCode = 201; - return { ...booking, message: "Payment required", paymentUid: payment?.uid }; + return { ...booking, message: "Payment required", paymentUid: payment?.uid, paymentId: payment?.id }; } loggerWithEventDetails.debug(`Booking ${organizerUser.username} completed`); @@ -2340,28 +2377,6 @@ async function handler( videoCallUrl = booking.location; } - const metadata = videoCallUrl - ? { - videoCallUrl: getVideoCallUrlFromCalEvent(evt), - } - : undefined; - - const webhookData = { - ...evt, - ...eventTypeInfo, - bookingId: booking?.id, - rescheduleUid, - rescheduleStartTime: originalRescheduledBooking?.startTime - ? dayjs(originalRescheduledBooking?.startTime).utc().format() - : undefined, - rescheduleEndTime: originalRescheduledBooking?.endTime - ? dayjs(originalRescheduledBooking?.endTime).utc().format() - : undefined, - metadata: { ...metadata, ...reqBody.metadata }, - eventTypeId, - status: "ACCEPTED", - smsReminderNumber: booking?.smsReminderNumber || undefined, - }; if (isConfirmedByDefault) { try { const subscribersMeetingEnded = await getWebhooks(subscriberOptionsMeetingEnded); diff --git a/packages/features/ee/payments/api/webhook.ts b/packages/features/ee/payments/api/webhook.ts index 0f9a435d30..627349eac6 100644 --- a/packages/features/ee/payments/api/webhook.ts +++ b/packages/features/ee/payments/api/webhook.ts @@ -5,21 +5,19 @@ import type Stripe from "stripe"; import stripe from "@calcom/app-store/stripepayment/lib/server"; import EventManager from "@calcom/core/EventManager"; -import dayjs from "@calcom/dayjs"; -import { sendOrganizerRequestEmail, sendAttendeeRequestEmail } from "@calcom/emails"; +import { sendAttendeeRequestEmail, sendOrganizerRequestEmail } from "@calcom/emails"; +import { doesBookingRequireConfirmation } from "@calcom/features/bookings/lib/doesBookingRequireConfirmation"; import { handleConfirmation } from "@calcom/features/bookings/lib/handleConfirmation"; -import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib"; import { IS_PRODUCTION } from "@calcom/lib/constants"; import { getErrorFromUnknown } from "@calcom/lib/errors"; import { HttpError as HttpCode } from "@calcom/lib/http-error"; +import logger from "@calcom/lib/logger"; +import { getBooking } from "@calcom/lib/payment/getBooking"; import { handlePaymentSuccess } from "@calcom/lib/payment/handlePaymentSuccess"; -import { getTranslation } from "@calcom/lib/server/i18n"; -import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat"; -import { bookingMinimalSelect, prisma } from "@calcom/prisma"; +import { prisma } from "@calcom/prisma"; import { BookingStatus } from "@calcom/prisma/enums"; -import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential"; -import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; -import type { CalendarEvent } from "@calcom/types/Calendar"; + +const log = logger.getChildLogger({ prefix: ["[paymentWebhook]"] }); export const config = { api: { @@ -27,109 +25,7 @@ export const config = { }, }; -async function getEventType(id: number) { - return prisma.eventType.findUnique({ - where: { - id, - }, - select: { - recurringEvent: true, - requiresConfirmation: true, - metadata: true, - }, - }); -} - -async function getBooking(bookingId: number) { - const booking = await prisma.booking.findUnique({ - where: { - id: bookingId, - }, - select: { - ...bookingMinimalSelect, - eventType: true, - smsReminderNumber: true, - location: true, - eventTypeId: true, - userId: true, - uid: true, - paid: true, - destinationCalendar: true, - status: true, - user: { - select: { - id: true, - username: true, - timeZone: true, - timeFormat: true, - email: true, - name: true, - locale: true, - destinationCalendar: true, - }, - }, - }, - }); - - if (!booking) throw new HttpCode({ statusCode: 204, message: "No booking found" }); - - type EventTypeRaw = Awaited>; - let eventTypeRaw: EventTypeRaw | null = null; - if (booking.eventTypeId) { - eventTypeRaw = await getEventType(booking.eventTypeId); - } - - const eventType = { ...eventTypeRaw, metadata: EventTypeMetaDataSchema.parse(eventTypeRaw?.metadata) }; - - const { user } = booking; - - if (!user) throw new HttpCode({ statusCode: 204, message: "No user found" }); - - const t = await getTranslation(user.locale ?? "en", "common"); - const attendeesListPromises = booking.attendees.map(async (attendee) => { - return { - name: attendee.name, - email: attendee.email, - timeZone: attendee.timeZone, - language: { - translate: await getTranslation(attendee.locale ?? "en", "common"), - locale: attendee.locale ?? "en", - }, - }; - }); - - const attendeesList = await Promise.all(attendeesListPromises); - const selectedDestinationCalendar = booking.destinationCalendar || user.destinationCalendar; - const evt: CalendarEvent = { - type: booking.title, - title: booking.title, - description: booking.description || undefined, - startTime: booking.startTime.toISOString(), - endTime: booking.endTime.toISOString(), - customInputs: isPrismaObjOrUndefined(booking.customInputs), - organizer: { - email: user.email, - name: user.name!, - timeZone: user.timeZone, - timeFormat: getTimeFormatStringFromUserTimeFormat(user.timeFormat), - language: { translate: t, locale: user.locale ?? "en" }, - id: user.id, - }, - attendees: attendeesList, - uid: booking.uid, - destinationCalendar: selectedDestinationCalendar ? [selectedDestinationCalendar] : [], - recurringEvent: parseRecurringEvent(eventType?.recurringEvent), - }; - - return { - booking, - user, - evt, - eventType, - }; -} - -async function handleStripePaymentSuccess(event: Stripe.Event) { +export async function handleStripePaymentSuccess(event: Stripe.Event) { const paymentIntent = event.data.object as Stripe.PaymentIntent; const payment = await prisma.payment.findFirst({ where: { @@ -140,8 +36,10 @@ async function handleStripePaymentSuccess(event: Stripe.Event) { bookingId: true, }, }); + if (!payment?.bookingId) { - console.log(JSON.stringify(paymentIntent), JSON.stringify(payment)); + log.error(JSON.stringify(paymentIntent), JSON.stringify(payment)); + throw new HttpCode({ statusCode: 204, message: "Payment not found" }); } if (!payment?.bookingId) throw new HttpCode({ statusCode: 204, message: "Payment not found" }); @@ -164,34 +62,16 @@ const handleSetupSuccess = async (event: Stripe.Event) => { paid: true, }; - const userWithCredentials = await prisma.user.findUnique({ - where: { - id: user.id, - }, - select: { - id: true, - username: true, - timeZone: true, - email: true, - name: true, - locale: true, - destinationCalendar: true, - credentials: { select: credentialForCalendarServiceSelect }, + if (!user) throw new HttpCode({ statusCode: 204, message: "No user found" }); + + const requiresConfirmation = doesBookingRequireConfirmation({ + booking: { + ...booking, + eventType, }, }); - - if (!userWithCredentials) throw new HttpCode({ statusCode: 204, message: "No user found" }); - - let requiresConfirmation = eventType?.requiresConfirmation; - const rcThreshold = eventType?.metadata?.requiresConfirmationThreshold; - if (rcThreshold) { - if (dayjs(dayjs(booking.startTime).utc().format()).diff(dayjs(), rcThreshold.unit) > rcThreshold.time) { - requiresConfirmation = false; - } - } - if (!requiresConfirmation) { - const eventManager = new EventManager(userWithCredentials); + const eventManager = new EventManager(user); const scheduleResult = await eventManager.create(evt); bookingData.references = { create: scheduleResult.referencesToCreate }; bookingData.status = BookingStatus.ACCEPTED; @@ -218,7 +98,7 @@ const handleSetupSuccess = async (event: Stripe.Event) => { if (!requiresConfirmation) { await handleConfirmation({ - user: userWithCredentials, + user, evt, prisma, bookingId: booking.id, diff --git a/packages/features/webhooks/components/WebhookForm.tsx b/packages/features/webhooks/components/WebhookForm.tsx index ca3e924fb0..d599f77394 100644 --- a/packages/features/webhooks/components/WebhookForm.tsx +++ b/packages/features/webhooks/components/WebhookForm.tsx @@ -35,6 +35,7 @@ const WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP_V2: Record>; + let eventTypeRaw: EventTypeRaw | null = null; + if (booking.eventTypeId) { + eventTypeRaw = await getEventType(booking.eventTypeId); + } + + const eventType = { ...eventTypeRaw, metadata: EventTypeMetaDataSchema.parse(eventTypeRaw?.metadata) }; + + const { user } = booking; + + if (!user) throw new HttpCode({ statusCode: 204, message: "No user found" }); + + const t = await getTranslation(user.locale ?? "en", "common"); + const attendeesListPromises = booking.attendees.map(async (attendee) => { + return { + name: attendee.name, + email: attendee.email, + timeZone: attendee.timeZone, + language: { + translate: await getTranslation(attendee.locale ?? "en", "common"), + locale: attendee.locale ?? "en", + }, + }; + }); + + const attendeesList = await Promise.all(attendeesListPromises); + const selectedDestinationCalendar = booking.destinationCalendar || user.destinationCalendar; + const evt: CalendarEvent = { + type: booking.title, + title: booking.title, + description: booking.description || undefined, + startTime: booking.startTime.toISOString(), + endTime: booking.endTime.toISOString(), + customInputs: isPrismaObjOrUndefined(booking.customInputs), + ...getCalEventResponses({ + booking: booking, + bookingFields: booking.eventType?.bookingFields || null, + }), + organizer: { + email: user.email, + name: user.name!, + timeZone: user.timeZone, + timeFormat: getTimeFormatStringFromUserTimeFormat(user.timeFormat), + language: { translate: t, locale: user.locale ?? "en" }, + id: user.id, + }, + attendees: attendeesList, + location: booking.location, + uid: booking.uid, + destinationCalendar: selectedDestinationCalendar ? [selectedDestinationCalendar] : [], + recurringEvent: parseRecurringEvent(eventType?.recurringEvent), + }; + + return { + booking, + user, + evt, + eventType, + }; +} diff --git a/packages/lib/payment/handlePaymentSuccess.ts b/packages/lib/payment/handlePaymentSuccess.ts index 4dfbe2be32..4e0a88867a 100644 --- a/packages/lib/payment/handlePaymentSuccess.ts +++ b/packages/lib/payment/handlePaymentSuccess.ts @@ -2,114 +2,19 @@ import type { Prisma } from "@prisma/client"; import EventManager from "@calcom/core/EventManager"; import { sendScheduledEmails } from "@calcom/emails"; -import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses"; +import { doesBookingRequireConfirmation } from "@calcom/features/bookings/lib/doesBookingRequireConfirmation"; +import { handleBookingRequested } from "@calcom/features/bookings/lib/handleBookingRequested"; import { handleConfirmation } from "@calcom/features/bookings/lib/handleConfirmation"; -import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib"; import { HttpError as HttpCode } from "@calcom/lib/http-error"; -import { getTranslation } from "@calcom/lib/server/i18n"; -import prisma, { bookingMinimalSelect } from "@calcom/prisma"; +import { getBooking } from "@calcom/lib/payment/getBooking"; +import prisma from "@calcom/prisma"; import { BookingStatus } from "@calcom/prisma/enums"; -import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential"; -import type { CalendarEvent } from "@calcom/types/Calendar"; -import { getTimeFormatStringFromUserTimeFormat } from "../timeFormat"; +import logger from "../logger"; +const log = logger.getChildLogger({ prefix: ["[handlePaymentSuccess]"] }); export async function handlePaymentSuccess(paymentId: number, bookingId: number) { - const booking = await prisma.booking.findUnique({ - where: { - id: bookingId, - }, - select: { - ...bookingMinimalSelect, - eventType: true, - smsReminderNumber: true, - location: true, - eventTypeId: true, - userId: true, - uid: true, - paid: true, - destinationCalendar: true, - status: true, - responses: true, - user: { - select: { - id: true, - username: true, - credentials: { select: credentialForCalendarServiceSelect }, - timeZone: true, - timeFormat: true, - email: true, - name: true, - locale: true, - destinationCalendar: true, - }, - }, - payment: { - select: { - amount: true, - currency: true, - paymentOption: true, - }, - }, - }, - }); - - if (!booking) throw new HttpCode({ statusCode: 204, message: "No booking found" }); - - type EventTypeRaw = Awaited>; - let eventTypeRaw: EventTypeRaw | null = null; - if (booking.eventTypeId) { - eventTypeRaw = await getEventType(booking.eventTypeId); - } - - const { user: userWithCredentials } = booking; - if (!userWithCredentials) throw new HttpCode({ statusCode: 204, message: "No user found" }); - const { credentials, ...user } = userWithCredentials; - - const t = await getTranslation(user.locale ?? "en", "common"); - const attendeesListPromises = booking.attendees.map(async (attendee) => { - return { - name: attendee.name, - email: attendee.email, - timeZone: attendee.timeZone, - language: { - translate: await getTranslation(attendee.locale ?? "en", "common"), - locale: attendee.locale ?? "en", - }, - }; - }); - - const attendeesList = await Promise.all(attendeesListPromises); - const selectedDestinationCalendar = booking.destinationCalendar || user.destinationCalendar; - const evt: CalendarEvent = { - type: booking.title, - title: booking.title, - description: booking.description || undefined, - startTime: booking.startTime.toISOString(), - endTime: booking.endTime.toISOString(), - customInputs: isPrismaObjOrUndefined(booking.customInputs), - ...getCalEventResponses({ - booking: booking, - bookingFields: booking.eventType?.bookingFields || null, - }), - organizer: { - email: user.email, - name: user.name!, - timeZone: user.timeZone, - timeFormat: getTimeFormatStringFromUserTimeFormat(user.timeFormat), - language: { translate: t, locale: user.locale ?? "en" }, - }, - attendees: attendeesList, - location: booking.location, - uid: booking.uid, - destinationCalendar: selectedDestinationCalendar ? [selectedDestinationCalendar] : [], - recurringEvent: parseRecurringEvent(eventTypeRaw?.recurringEvent), - paymentInfo: booking.payment?.[0] && { - amount: booking.payment[0].amount, - currency: booking.payment[0].currency, - paymentOption: booking.payment[0].paymentOption, - }, - }; + const { booking, user: userWithCredentials, evt, eventType } = await getBooking(bookingId); if (booking.location) evt.location = booking.location; @@ -125,10 +30,16 @@ export async function handlePaymentSuccess(paymentId: number, bookingId: number) bookingData.references = { create: scheduleResult.referencesToCreate }; } - if (eventTypeRaw?.requiresConfirmation) { + const requiresConfirmation = doesBookingRequireConfirmation({ + booking: { + ...booking, + eventType, + }, + }); + + if (requiresConfirmation) { delete bookingData.status; } - const paymentUpdate = prisma.payment.update({ where: { id: paymentId, @@ -146,16 +57,23 @@ export async function handlePaymentSuccess(paymentId: number, bookingId: number) }); await prisma.$transaction([paymentUpdate, bookingUpdate]); - - if (!isConfirmed && !eventTypeRaw?.requiresConfirmation) { - await handleConfirmation({ - user: userWithCredentials, - evt, - prisma, - bookingId: booking.id, - booking, - paid: true, - }); + if (!isConfirmed) { + if (!requiresConfirmation) { + await handleConfirmation({ + user: userWithCredentials, + evt, + prisma, + bookingId: booking.id, + booking, + paid: true, + }); + } else { + await handleBookingRequested({ + evt, + booking, + }); + log.debug(`handling booking request for eventId ${eventType.id}`); + } } else { await sendScheduledEmails({ ...evt }); } @@ -165,15 +83,3 @@ export async function handlePaymentSuccess(paymentId: number, bookingId: number) message: `Booking with id '${booking.id}' was paid and confirmed.`, }); } - -async function getEventType(id: number) { - return prisma.eventType.findUnique({ - where: { - id, - }, - select: { - recurringEvent: true, - requiresConfirmation: true, - }, - }); -} diff --git a/packages/prisma/migrations/20230904102526_add_booking_payment_initiated/migration.sql b/packages/prisma/migrations/20230904102526_add_booking_payment_initiated/migration.sql new file mode 100644 index 0000000000..25f5032284 --- /dev/null +++ b/packages/prisma/migrations/20230904102526_add_booking_payment_initiated/migration.sql @@ -0,0 +1,2 @@ +-- AlterEnum +ALTER TYPE "WebhookTriggerEvents" ADD VALUE 'BOOKING_PAYMENT_INITIATED'; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 93806d7e15..412689ee63 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -541,6 +541,7 @@ enum PaymentOption { enum WebhookTriggerEvents { BOOKING_CREATED + BOOKING_PAYMENT_INITIATED BOOKING_PAID BOOKING_RESCHEDULED BOOKING_REQUESTED diff --git a/tests/libs/__mocks__/prisma.ts b/tests/libs/__mocks__/prisma.ts index 2b3153db07..351fc230f1 100644 --- a/tests/libs/__mocks__/prisma.ts +++ b/tests/libs/__mocks__/prisma.ts @@ -1,18 +1,90 @@ +import { PrismockClient } from "prismock"; import { beforeEach, vi } from "vitest"; -import { mockDeep, mockReset } from "vitest-mock-extended"; -import type { PrismaClient } from "@calcom/prisma"; +import logger from "@calcom/lib/logger"; +import * as selects from "@calcom/prisma/selects"; vi.mock("@calcom/prisma", () => ({ default: prisma, prisma, - availabilityUserSelect: vi.fn(), - userSelect: vi.fn(), + ...selects, })); +const handlePrismockBugs = () => { + const __updateBooking = prismock.booking.update; + const __findManyWebhook = prismock.webhook.findMany; + const __findManyBooking = prismock.booking.findMany; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + prismock.booking.update = (...rest: any[]) => { + // There is a bug in prismock where it considers `createMany` and `create` itself to have the data directly + // In booking flows, we encounter such scenario, so let's fix that here directly till it's fixed in prismock + if (rest[0].data.references?.createMany) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + rest[0].data.references.createMany = rest[0].data.references?.createMany.data; + logger.silly("Fixed Prismock bug"); + } + if (rest[0].data.references?.create) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + rest[0].data.references.create = rest[0].data.references?.create.data; + logger.silly("Fixed Prismock bug-1"); + } + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + return __updateBooking(...rest); + }; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + prismock.webhook.findMany = (...rest: any[]) => { + // There is some bug in prismock where it can't handle complex where clauses + if (rest[0].where?.OR && rest[0].where.AND) { + rest[0].where = undefined; + logger.silly("Fixed Prismock bug-2"); + } + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + return __findManyWebhook(...rest); + }; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + prismock.booking.findMany = (...rest: any[]) => { + // There is a bug in prismock where it considers `createMany` and `create` itself to have the data directly + // In booking flows, we encounter such scenario, so let's fix that here directly till it's fixed in prismock + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const where = rest[0]?.where; + if (where?.OR) { + logger.silly("Fixed Prismock bug-3"); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + where.OR.forEach((or: any) => { + if (or.startTime?.gte) { + or.startTime.gte = or.startTime.gte.toISOString ? or.startTime.gte.toISOString() : or.startTime.gte; + } + if (or.startTime?.lte) { + or.startTime.lte = or.startTime.lte.toISOString ? or.startTime.lte.toISOString() : or.startTime.lte; + } + if (or.endTime?.gte) { + or.endTime.lte = or.endTime.gte.toISOString ? or.endTime.gte.toISOString() : or.endTime.gte; + } + if (or.endTime?.lte) { + or.endTime.lte = or.endTime.lte.toISOString ? or.endTime.lte.toISOString() : or.endTime.lte; + } + }); + } + return __findManyBooking(...rest); + }; +}; + beforeEach(() => { - mockReset(prisma); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + prismock.reset(); + handlePrismockBugs(); }); -const prisma = mockDeep(); +const prismock = new PrismockClient(); + +const prisma = prismock; export default prisma; diff --git a/tests/libs/__mocks__/prismaMock.ts b/tests/libs/__mocks__/prismaMock.ts new file mode 100644 index 0000000000..2b3153db07 --- /dev/null +++ b/tests/libs/__mocks__/prismaMock.ts @@ -0,0 +1,18 @@ +import { beforeEach, vi } from "vitest"; +import { mockDeep, mockReset } from "vitest-mock-extended"; + +import type { PrismaClient } from "@calcom/prisma"; + +vi.mock("@calcom/prisma", () => ({ + default: prisma, + prisma, + availabilityUserSelect: vi.fn(), + userSelect: vi.fn(), +})); + +beforeEach(() => { + mockReset(prisma); +}); + +const prisma = mockDeep(); +export default prisma; diff --git a/yarn.lock b/yarn.lock index 81feb7176d..b8c007aa76 100644 --- a/yarn.lock +++ b/yarn.lock @@ -45,13 +45,6 @@ __metadata: languageName: node linkType: hard -"@aashutoshrathi/word-wrap@npm:^1.2.3": - version: 1.2.6 - resolution: "@aashutoshrathi/word-wrap@npm:1.2.6" - checksum: ada901b9e7c680d190f1d012c84217ce0063d8f5c5a7725bb91ec3c5ed99bb7572680eb2d2938a531ccbaec39a95422fcd8a6b4a13110c7d98dd75402f66a0cd - languageName: node - linkType: hard - "@achrinza/event-pubsub@npm:5.0.8": version: 5.0.8 resolution: "@achrinza/event-pubsub@npm:5.0.8" @@ -98,13 +91,6 @@ __metadata: languageName: node linkType: hard -"@alloc/quick-lru@npm:^5.2.0": - version: 5.2.0 - resolution: "@alloc/quick-lru@npm:5.2.0" - checksum: bdc35758b552bcf045733ac047fb7f9a07c4678b944c641adfbd41f798b4b91fffd0fdc0df2578d9b0afc7b4d636aa6e110ead5d6281a2adc1ab90efd7f057f8 - languageName: node - linkType: hard - "@ampproject/remapping@npm:^2.2.0": version: 2.2.1 resolution: "@ampproject/remapping@npm:2.2.1" @@ -115,6 +101,21 @@ __metadata: languageName: node linkType: hard +"@antfu/ni@npm:0.21.5": + version: 0.21.5 + resolution: "@antfu/ni@npm:0.21.5" + bin: + na: bin/na.mjs + nci: bin/nci.mjs + ni: bin/ni.mjs + nlx: bin/nlx.mjs + nr: bin/nr.mjs + nu: bin/nu.mjs + nun: bin/nun.mjs + checksum: 9d5e04faa5096c94141870cc5b7f46d89e60d615e81765f788651235767ac6b86c6dd89bb709ac7a86fe9ded471fa549f04dd4dcdb121f2c492c9fd85200d0bc + languageName: node + linkType: hard + "@anthropic-ai/sdk@npm:^0.5.7": version: 0.5.10 resolution: "@anthropic-ai/sdk@npm:0.5.10" @@ -211,6 +212,25 @@ __metadata: languageName: node linkType: hard +"@auth/core@npm:^0.1.4": + version: 0.1.4 + resolution: "@auth/core@npm:0.1.4" + dependencies: + "@panva/hkdf": 1.0.2 + cookie: 0.5.0 + jose: 4.11.1 + oauth4webapi: 2.0.5 + preact: 10.11.3 + preact-render-to-string: 5.2.3 + peerDependencies: + nodemailer: 6.8.0 + peerDependenciesMeta: + nodemailer: + optional: true + checksum: 64854404ea1883e0deb5535b34bed95cd43fc85094aeaf4f15a79e14045020eb944f844defe857edfc8528a0a024be89cbb2a3069dedef0e9217a74ca6c3eb79 + languageName: node + linkType: hard + "@aws-crypto/ie11-detection@npm:^3.0.0": version: 3.0.0 resolution: "@aws-crypto/ie11-detection@npm:3.0.0" @@ -3163,7 +3183,7 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.14.5, @babel/runtime@npm:^7.17.2, @babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.21.0": +"@babel/runtime@npm:^7.21.0": version: 7.23.1 resolution: "@babel/runtime@npm:7.23.1" dependencies: @@ -3493,6 +3513,41 @@ __metadata: languageName: unknown linkType: soft +"@calcom/auth@workspace:apps/auth": + version: 0.0.0-use.local + resolution: "@calcom/auth@workspace:apps/auth" + dependencies: + "@auth/core": ^0.1.4 + "@calcom/app-store": "*" + "@calcom/app-store-cli": "*" + "@calcom/config": "*" + "@calcom/core": "*" + "@calcom/dayjs": "*" + "@calcom/embed-core": "workspace:*" + "@calcom/embed-react": "workspace:*" + "@calcom/embed-snippet": "workspace:*" + "@calcom/features": "*" + "@calcom/lib": "*" + "@calcom/prisma": "*" + "@calcom/trpc": "*" + "@calcom/tsconfig": "*" + "@calcom/types": "*" + "@calcom/ui": "*" + "@types/node": 16.9.1 + "@types/react": 18.0.26 + "@types/react-dom": ^18.0.9 + eslint: ^8.34.0 + eslint-config-next: ^13.2.1 + next: ^13.4.6 + next-auth: ^4.22.1 + postcss: ^8.4.18 + react: ^18.2.0 + react-dom: ^18.2.0 + tailwindcss: ^3.3.1 + typescript: ^4.9.4 + languageName: unknown + linkType: soft + "@calcom/basecamp3@workspace:packages/app-store/basecamp3": version: 0.0.0-use.local resolution: "@calcom/basecamp3@workspace:packages/app-store/basecamp3" @@ -3582,30 +3637,29 @@ __metadata: "@calcom/ui": "*" "@headlessui/react": ^1.5.0 "@heroicons/react": ^1.0.6 - "@prisma/client": ^4.8.1 + "@prisma/client": ^5.0.0 "@tailwindcss/forms": ^0.5.2 "@types/node": 16.9.1 - "@types/react": ^18.0.17 + "@types/react": 18.0.26 autoprefixer: ^10.4.12 chart.js: ^3.7.1 client-only: ^0.0.1 - eslint: ^8.22.0 - next: ^13.1.1 - next-auth: ^4.10.3 - next-i18next: ^11.3.0 - next-transpile-modules: ^10.0.0 + eslint: ^8.34.0 + next: ^13.4.6 + next-auth: ^4.22.1 + next-i18next: ^13.2.2 postcss: ^8.4.18 - prisma: ^4.8.1 + prisma: ^5.0.0 prisma-field-encryption: ^1.4.0 react: ^18.2.0 react-chartjs-2: ^4.0.1 react-dom: ^18.2.0 - react-hook-form: ^7.34.2 - react-live-chat-loader: ^2.7.3 + react-hook-form: ^7.43.3 + react-live-chat-loader: ^2.8.1 swr: ^1.2.2 - tailwindcss: ^3.2.1 - typescript: ^4.7.4 - zod: ^3.20.2 + tailwindcss: ^3.3.1 + typescript: ^4.9.4 + zod: ^3.22.2 languageName: unknown linkType: soft @@ -5478,24 +5532,6 @@ __metadata: languageName: node linkType: hard -"@eslint-community/eslint-utils@npm:^4.2.0": - version: 4.4.0 - resolution: "@eslint-community/eslint-utils@npm:4.4.0" - dependencies: - eslint-visitor-keys: ^3.3.0 - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - checksum: cdfe3ae42b4f572cbfb46d20edafe6f36fc5fb52bf2d90875c58aefe226892b9677fef60820e2832caf864a326fe4fc225714c46e8389ccca04d5f9288aabd22 - languageName: node - linkType: hard - -"@eslint-community/regexpp@npm:^4.6.1": - version: 4.8.1 - resolution: "@eslint-community/regexpp@npm:4.8.1" - checksum: 82d62c845ef42b810f268cfdc84d803a2da01735fb52e902fd34bdc09f92464a094fd8e4802839874b000b2f73f67c972859e813ba705233515d3e954f234bf2 - languageName: node - linkType: hard - "@eslint/eslintrc@npm:^1.0.5": version: 1.3.0 resolution: "@eslint/eslintrc@npm:1.3.0" @@ -5530,30 +5566,6 @@ __metadata: languageName: node linkType: hard -"@eslint/eslintrc@npm:^2.1.2": - version: 2.1.2 - resolution: "@eslint/eslintrc@npm:2.1.2" - dependencies: - ajv: ^6.12.4 - debug: ^4.3.2 - espree: ^9.6.0 - globals: ^13.19.0 - ignore: ^5.2.0 - import-fresh: ^3.2.1 - js-yaml: ^4.1.0 - minimatch: ^3.1.2 - strip-json-comments: ^3.1.1 - checksum: bc742a1e3b361f06fedb4afb6bf32cbd27171292ef7924f61c62f2aed73048367bcc7ac68f98c06d4245cd3fabc43270f844e3c1699936d4734b3ac5398814a7 - languageName: node - linkType: hard - -"@eslint/js@npm:8.49.0": - version: 8.49.0 - resolution: "@eslint/js@npm:8.49.0" - checksum: a6601807c8aeeefe866926ad92ed98007c034a735af20ff709009e39ad1337474243d47908500a3bde04e37bfba16bcf1d3452417f962e1345bc8756edd6b830 - languageName: node - linkType: hard - "@ethereumjs/common@npm:^2.5.0, @ethereumjs/common@npm:^2.6.3": version: 2.6.3 resolution: "@ethereumjs/common@npm:2.6.3" @@ -6646,17 +6658,6 @@ __metadata: languageName: node linkType: hard -"@humanwhocodes/config-array@npm:^0.11.11": - version: 0.11.11 - resolution: "@humanwhocodes/config-array@npm:0.11.11" - dependencies: - "@humanwhocodes/object-schema": ^1.2.1 - debug: ^4.1.1 - minimatch: ^3.0.5 - checksum: db84507375ab77b8ffdd24f498a5b49ad6b64391d30dd2ac56885501d03964d29637e05b1ed5aefa09d57ac667e28028bc22d2da872bfcd619652fbdb5f4ca19 - languageName: node - linkType: hard - "@humanwhocodes/config-array@npm:^0.11.8": version: 0.11.8 resolution: "@humanwhocodes/config-array@npm:0.11.8" @@ -7767,13 +7768,6 @@ __metadata: languageName: node linkType: hard -"@next/env@npm:13.5.2": - version: 13.5.2 - resolution: "@next/env@npm:13.5.2" - checksum: f6ef14b7643049dafc2d53b5091e3f74eed0af14743cfd61f1db7782a99e69b5bef63f36ba700034b23656a264c7ec498aac8fa4f9377dad01e544ffa507388f - languageName: node - linkType: hard - "@next/eslint-plugin-next@npm:13.2.1": version: 13.2.1 resolution: "@next/eslint-plugin-next@npm:13.2.1" @@ -7790,13 +7784,6 @@ __metadata: languageName: node linkType: hard -"@next/swc-darwin-arm64@npm:13.5.2": - version: 13.5.2 - resolution: "@next/swc-darwin-arm64@npm:13.5.2" - conditions: os=darwin & cpu=arm64 - languageName: node - linkType: hard - "@next/swc-darwin-x64@npm:13.4.6": version: 13.4.6 resolution: "@next/swc-darwin-x64@npm:13.4.6" @@ -7804,13 +7791,6 @@ __metadata: languageName: node linkType: hard -"@next/swc-darwin-x64@npm:13.5.2": - version: 13.5.2 - resolution: "@next/swc-darwin-x64@npm:13.5.2" - conditions: os=darwin & cpu=x64 - languageName: node - linkType: hard - "@next/swc-linux-arm64-gnu@npm:13.4.6": version: 13.4.6 resolution: "@next/swc-linux-arm64-gnu@npm:13.4.6" @@ -7818,13 +7798,6 @@ __metadata: languageName: node linkType: hard -"@next/swc-linux-arm64-gnu@npm:13.5.2": - version: 13.5.2 - resolution: "@next/swc-linux-arm64-gnu@npm:13.5.2" - conditions: os=linux & cpu=arm64 & libc=glibc - languageName: node - linkType: hard - "@next/swc-linux-arm64-musl@npm:13.4.6": version: 13.4.6 resolution: "@next/swc-linux-arm64-musl@npm:13.4.6" @@ -7832,13 +7805,6 @@ __metadata: languageName: node linkType: hard -"@next/swc-linux-arm64-musl@npm:13.5.2": - version: 13.5.2 - resolution: "@next/swc-linux-arm64-musl@npm:13.5.2" - conditions: os=linux & cpu=arm64 & libc=musl - languageName: node - linkType: hard - "@next/swc-linux-x64-gnu@npm:13.4.6": version: 13.4.6 resolution: "@next/swc-linux-x64-gnu@npm:13.4.6" @@ -7846,13 +7812,6 @@ __metadata: languageName: node linkType: hard -"@next/swc-linux-x64-gnu@npm:13.5.2": - version: 13.5.2 - resolution: "@next/swc-linux-x64-gnu@npm:13.5.2" - conditions: os=linux & cpu=x64 & libc=glibc - languageName: node - linkType: hard - "@next/swc-linux-x64-musl@npm:13.4.6": version: 13.4.6 resolution: "@next/swc-linux-x64-musl@npm:13.4.6" @@ -7860,13 +7819,6 @@ __metadata: languageName: node linkType: hard -"@next/swc-linux-x64-musl@npm:13.5.2": - version: 13.5.2 - resolution: "@next/swc-linux-x64-musl@npm:13.5.2" - conditions: os=linux & cpu=x64 & libc=musl - languageName: node - linkType: hard - "@next/swc-win32-arm64-msvc@npm:13.4.6": version: 13.4.6 resolution: "@next/swc-win32-arm64-msvc@npm:13.4.6" @@ -7874,13 +7826,6 @@ __metadata: languageName: node linkType: hard -"@next/swc-win32-arm64-msvc@npm:13.5.2": - version: 13.5.2 - resolution: "@next/swc-win32-arm64-msvc@npm:13.5.2" - conditions: os=win32 & cpu=arm64 - languageName: node - linkType: hard - "@next/swc-win32-ia32-msvc@npm:13.4.6": version: 13.4.6 resolution: "@next/swc-win32-ia32-msvc@npm:13.4.6" @@ -7888,13 +7833,6 @@ __metadata: languageName: node linkType: hard -"@next/swc-win32-ia32-msvc@npm:13.5.2": - version: 13.5.2 - resolution: "@next/swc-win32-ia32-msvc@npm:13.5.2" - conditions: os=win32 & cpu=ia32 - languageName: node - linkType: hard - "@next/swc-win32-x64-msvc@npm:13.4.6": version: 13.4.6 resolution: "@next/swc-win32-x64-msvc@npm:13.4.6" @@ -7902,13 +7840,6 @@ __metadata: languageName: node linkType: hard -"@next/swc-win32-x64-msvc@npm:13.5.2": - version: 13.5.2 - resolution: "@next/swc-win32-x64-msvc@npm:13.5.2" - conditions: os=win32 & cpu=x64 - languageName: node - linkType: hard - "@noble/curves@npm:1.1.0, @noble/curves@npm:~1.1.0": version: 1.1.0 resolution: "@noble/curves@npm:1.1.0" @@ -7925,7 +7856,7 @@ __metadata: languageName: node linkType: hard -"@noble/hashes@npm:~1.3.0, @noble/hashes@npm:~1.3.1": +"@noble/hashes@npm:^1.1.5, @noble/hashes@npm:~1.3.0, @noble/hashes@npm:~1.3.1": version: 1.3.2 resolution: "@noble/hashes@npm:1.3.2" checksum: fe23536b436539d13f90e4b9be843cc63b1b17666a07634a2b1259dded6f490be3d050249e6af98076ea8f2ea0d56f578773c2197f2aa0eeaa5fba5bc18ba474 @@ -8029,6 +7960,13 @@ __metadata: languageName: node linkType: hard +"@opentelemetry/api@npm:1.4.1": + version: 1.4.1 + resolution: "@opentelemetry/api@npm:1.4.1" + checksum: e783c40d1a518abf9c4c5d65223237c1392cd9a6c53ac6e2c3ef0c05ff7266e3dfc4fd9874316dae0dcb7a97950878deb513bcbadfaad653d48f0215f2a0911b + languageName: node + linkType: hard + "@otplib/core@npm:^12.0.1": version: 12.0.1 resolution: "@otplib/core@npm:12.0.1" @@ -8077,6 +8015,13 @@ __metadata: languageName: node linkType: hard +"@panva/hkdf@npm:1.0.2": + version: 1.0.2 + resolution: "@panva/hkdf@npm:1.0.2" + checksum: 75183b4d5ea816ef516dcea70985c610683579a9e2ac540c2d59b9a3ed27eedaff830a43a1c43c1683556a457c92ac66e09109ee995ab173090e4042c4c4bb03 + languageName: node + linkType: hard + "@panva/hkdf@npm:^1.0.2": version: 1.0.4 resolution: "@panva/hkdf@npm:1.0.4" @@ -8084,6 +8029,15 @@ __metadata: languageName: node linkType: hard +"@paralleldrive/cuid2@npm:2.2.2": + version: 2.2.2 + resolution: "@paralleldrive/cuid2@npm:2.2.2" + dependencies: + "@noble/hashes": ^1.1.5 + checksum: f7f6ac70e0268ec2c72e555719240d5c2c9a859ce541ac1c637eed3f3ee971b42881d299dedafbded53e7365b9e98176c5a31c442c1112f7e9e7306f2fd0ecbb + languageName: node + linkType: hard + "@peculiar/asn1-schema@npm:^2.3.6": version: 2.3.6 resolution: "@peculiar/asn1-schema@npm:2.3.6" @@ -8193,17 +8147,17 @@ __metadata: languageName: node linkType: hard -"@prisma/client@npm:^4.8.1": - version: 4.16.2 - resolution: "@prisma/client@npm:4.16.2" +"@prisma/client@npm:^5.0.0": + version: 5.3.1 + resolution: "@prisma/client@npm:5.3.1" dependencies: - "@prisma/engines-version": 4.16.1-1.4bc8b6e1b66cb932731fb1bdbbc550d1e010de81 + "@prisma/engines-version": 5.3.1-2.61e140623197a131c2a6189271ffee05a7aa9a59 peerDependencies: prisma: "*" peerDependenciesMeta: prisma: optional: true - checksum: 38e1356644a764946c69c8691ea4bbed0ba37739d833a435625bd5435912bed4b9bdd7c384125f3a4ab8128faf566027985c0f0840a42741c338d72e40b5d565 + checksum: 8017b721a231ab7b2b0f932507b02bf075aae2c6f6e630a69d7089ff33bab44fa50b50dd8a81655b7202092ffc19717d484ae5f183fc4f2a1822b0d228991a7c languageName: node linkType: hard @@ -8232,6 +8186,17 @@ __metadata: languageName: node linkType: hard +"@prisma/debug@npm:5.1.1": + version: 5.1.1 + resolution: "@prisma/debug@npm:5.1.1" + dependencies: + "@types/debug": 4.1.8 + debug: 4.3.4 + strip-ansi: 6.0.1 + checksum: ab073c70aac7f8240b70c6021b7f46b34a0bbbe6a220fe503cd34bfd6c7b9890916f42c6bc7b7345a3225f8ea9d9744064e716fe39fb2d40e44c752689ee3415 + languageName: node + linkType: hard + "@prisma/debug@npm:5.2.0": version: 5.2.0 resolution: "@prisma/debug@npm:5.2.0" @@ -8254,13 +8219,6 @@ __metadata: languageName: node linkType: hard -"@prisma/engines-version@npm:4.16.1-1.4bc8b6e1b66cb932731fb1bdbbc550d1e010de81": - version: 4.16.1-1.4bc8b6e1b66cb932731fb1bdbbc550d1e010de81 - resolution: "@prisma/engines-version@npm:4.16.1-1.4bc8b6e1b66cb932731fb1bdbbc550d1e010de81" - checksum: b42c6abe7c1928e546f15449e40ffa455701ef2ab1f62973628ecb4e19ff3652e34609a0d83196d1cbd0864adb44c55e082beec852b11929acf1c15fb57ca45a - languageName: node - linkType: hard - "@prisma/engines-version@npm:5.2.0-25.2804dc98259d2ea960602aca6b8e7fdc03c1758f": version: 5.2.0-25.2804dc98259d2ea960602aca6b8e7fdc03c1758f resolution: "@prisma/engines-version@npm:5.2.0-25.2804dc98259d2ea960602aca6b8e7fdc03c1758f" @@ -8268,10 +8226,17 @@ __metadata: languageName: node linkType: hard -"@prisma/engines@npm:4.16.2": - version: 4.16.2 - resolution: "@prisma/engines@npm:4.16.2" - checksum: f423e6092c3e558cd089a68ae87459fba7fd390c433df087342b3269c3b04163965b50845150dfe47d01f811781bfff89d5ae81c95ca603c59359ab69ebd810f +"@prisma/engines-version@npm:5.3.1-2.61e140623197a131c2a6189271ffee05a7aa9a59": + version: 5.3.1-2.61e140623197a131c2a6189271ffee05a7aa9a59 + resolution: "@prisma/engines-version@npm:5.3.1-2.61e140623197a131c2a6189271ffee05a7aa9a59" + checksum: c1adf540c9330a54a000c3005a4621c5d8355f2e3b159121587d33f82bf5992f567ac453f0ce76dd48fac427ec4dbc55942228fcee10d522b0d2c03bddbe422a + languageName: node + linkType: hard + +"@prisma/engines@npm:5.1.1": + version: 5.1.1 + resolution: "@prisma/engines@npm:5.1.1" + checksum: 561c85def110279eb7c764e7c1ea1f8f54d9a2948ed5e5db9b11321c2bef362d178b756e39e7e022ee27cb2be5ac8d9b835967ce341ad8f6a1e8502f988140ee languageName: node linkType: hard @@ -8282,6 +8247,13 @@ __metadata: languageName: node linkType: hard +"@prisma/engines@npm:5.3.1": + version: 5.3.1 + resolution: "@prisma/engines@npm:5.3.1" + checksum: a231adad60ac42569b560ea9782bc181818d8ad15e65283d1317bda5d7aa754e5b536a3f9365ce1eda8961e1eff4eca5978c456fa9764a867fe4339d123e7371 + languageName: node + linkType: hard + "@prisma/extension-accelerate@npm:^0.6.2": version: 0.6.2 resolution: "@prisma/extension-accelerate@npm:0.6.2" @@ -8291,6 +8263,43 @@ __metadata: languageName: node linkType: hard +"@prisma/fetch-engine@npm:5.1.1": + version: 5.1.1 + resolution: "@prisma/fetch-engine@npm:5.1.1" + dependencies: + "@prisma/debug": 5.1.1 + "@prisma/get-platform": 5.1.1 + execa: 5.1.1 + find-cache-dir: 3.3.2 + fs-extra: 11.1.1 + hasha: 5.2.2 + http-proxy-agent: 7.0.0 + https-proxy-agent: 7.0.1 + kleur: 4.1.5 + node-fetch: 2.6.12 + p-filter: 2.1.0 + p-map: 4.0.0 + p-retry: 4.6.2 + progress: 2.0.3 + rimraf: 3.0.2 + temp-dir: 2.0.0 + tempy: 1.0.1 + checksum: 7a98c05f8417183fcc211d94dc004dab425887c49199c20b277e487f66b2882039da31cb946dcd41e0fc78a47c298760f31f446883592027270627f7b83ce016 + languageName: node + linkType: hard + +"@prisma/generator-helper@npm:5.1.1": + version: 5.1.1 + resolution: "@prisma/generator-helper@npm:5.1.1" + dependencies: + "@prisma/debug": 5.1.1 + "@types/cross-spawn": 6.0.2 + cross-spawn: 7.0.3 + kleur: 4.1.5 + checksum: e576e0396031e0f90442c5fd37c0ee13894d03d89d4d9c572e787268156fcda960a969773c37030f3d66271c56424c34c988ac15c75be5dde5f71c6be7266bf6 + languageName: node + linkType: hard + "@prisma/generator-helper@npm:^5.0.0": version: 5.3.1 resolution: "@prisma/generator-helper@npm:5.3.1" @@ -8327,6 +8336,81 @@ __metadata: languageName: node linkType: hard +"@prisma/get-platform@npm:5.1.1": + version: 5.1.1 + resolution: "@prisma/get-platform@npm:5.1.1" + dependencies: + "@prisma/debug": 5.1.1 + escape-string-regexp: 4.0.0 + execa: 5.1.1 + fs-jetpack: 5.1.0 + kleur: 4.1.5 + replace-string: 3.1.0 + strip-ansi: 6.0.1 + tempy: 1.0.1 + terminal-link: 2.1.1 + ts-pattern: 4.3.0 + checksum: 9d94496b3acec6553331016e92519b692ad60994a33bc1e3b3fe9758d606b0741b846d6ed99fb164fb7d59912a844f15ce6f46cb29b20ca0ce6c8e9db54d91aa + languageName: node + linkType: hard + +"@prisma/internals@npm:5.1.1": + version: 5.1.1 + resolution: "@prisma/internals@npm:5.1.1" + dependencies: + "@antfu/ni": 0.21.5 + "@opentelemetry/api": 1.4.1 + "@prisma/debug": 5.1.1 + "@prisma/engines": 5.1.1 + "@prisma/fetch-engine": 5.1.1 + "@prisma/generator-helper": 5.1.1 + "@prisma/get-platform": 5.1.1 + "@prisma/prisma-schema-wasm": 5.1.1-1.6a3747c37ff169c90047725a05a6ef02e32ac97e + archiver: 5.3.1 + arg: 5.0.2 + checkpoint-client: 1.1.27 + cli-truncate: 2.1.0 + dotenv: 16.0.3 + escape-string-regexp: 4.0.0 + execa: 5.1.1 + find-up: 5.0.0 + fp-ts: 2.16.0 + fs-extra: 11.1.1 + fs-jetpack: 5.1.0 + global-dirs: 3.0.1 + globby: 11.1.0 + indent-string: 4.0.0 + is-windows: 1.0.2 + is-wsl: 2.2.0 + kleur: 4.1.5 + new-github-issue-url: 0.2.1 + node-fetch: 2.6.12 + npm-packlist: 5.1.3 + open: 7.4.2 + p-map: 4.0.0 + prompts: 2.4.2 + read-pkg-up: 7.0.1 + replace-string: 3.1.0 + resolve: 1.22.2 + string-width: 4.2.3 + strip-ansi: 6.0.1 + strip-indent: 3.0.0 + temp-dir: 2.0.0 + tempy: 1.0.1 + terminal-link: 2.1.1 + tmp: 0.2.1 + ts-pattern: 4.3.0 + checksum: 6c6059ed996e333a925c1ca650a7f84f010494451355798ca76102ecd1e2d05218b6800f36448d94ac885c8b15f00903652e1398b300c53c9d2f53aac6fec515 + languageName: node + linkType: hard + +"@prisma/prisma-schema-wasm@npm:5.1.1-1.6a3747c37ff169c90047725a05a6ef02e32ac97e": + version: 5.1.1-1.6a3747c37ff169c90047725a05a6ef02e32ac97e + resolution: "@prisma/prisma-schema-wasm@npm:5.1.1-1.6a3747c37ff169c90047725a05a6ef02e32ac97e" + checksum: 72191fe2bc4ec28287ab0ea119506eb890b536cf1498b516f5576055489a9fec4853ba285408a05dfcb963076987ffd40088ac144a0d13d3377ca9971f94276e + languageName: node + linkType: hard + "@radix-ui/number@npm:0.1.0": version: 0.1.0 resolution: "@radix-ui/number@npm:0.1.0" @@ -11965,15 +12049,6 @@ __metadata: languageName: node linkType: hard -"@swc/helpers@npm:0.5.2": - version: 0.5.2 - resolution: "@swc/helpers@npm:0.5.2" - dependencies: - tslib: ^2.4.0 - checksum: 51d7e3d8bd56818c49d6bfbd715f0dbeedc13cf723af41166e45c03e37f109336bbcb57a1f2020f4015957721aeb21e1a7fff281233d797ff7d3dd1f447fa258 - languageName: node - linkType: hard - "@szmarczak/http-timer@npm:^4.0.5": version: 4.0.6 resolution: "@szmarczak/http-timer@npm:4.0.6" @@ -14802,6 +14877,57 @@ __metadata: languageName: node linkType: hard +"archiver-utils@npm:^2.1.0": + version: 2.1.0 + resolution: "archiver-utils@npm:2.1.0" + dependencies: + glob: ^7.1.4 + graceful-fs: ^4.2.0 + lazystream: ^1.0.0 + lodash.defaults: ^4.2.0 + lodash.difference: ^4.5.0 + lodash.flatten: ^4.4.0 + lodash.isplainobject: ^4.0.6 + lodash.union: ^4.6.0 + normalize-path: ^3.0.0 + readable-stream: ^2.0.0 + checksum: 5665f40bde87ee82cb638177bdccca8cc6e55edea1b94338f7e6b56a1d9367b0d9a39e42b47866eaf84b8c67669a7d250900a226207ecc30fa163b52aae859a5 + languageName: node + linkType: hard + +"archiver-utils@npm:^3.0.4": + version: 3.0.4 + resolution: "archiver-utils@npm:3.0.4" + dependencies: + glob: ^7.2.3 + graceful-fs: ^4.2.0 + lazystream: ^1.0.0 + lodash.defaults: ^4.2.0 + lodash.difference: ^4.5.0 + lodash.flatten: ^4.4.0 + lodash.isplainobject: ^4.0.6 + lodash.union: ^4.6.0 + normalize-path: ^3.0.0 + readable-stream: ^3.6.0 + checksum: 5c6568f1185fb6c4b85282ad3281a5a024761bf27e525de1ec54255d15ca98e19532e7b5403930273911a5c8c961aa0c1e9148d6c2810784fa6bd8a97c0021a7 + languageName: node + linkType: hard + +"archiver@npm:5.3.1": + version: 5.3.1 + resolution: "archiver@npm:5.3.1" + dependencies: + archiver-utils: ^2.1.0 + async: ^3.2.3 + buffer-crc32: ^0.2.1 + readable-stream: ^3.6.0 + readdir-glob: ^1.0.0 + tar-stream: ^2.2.0 + zip-stream: ^4.1.0 + checksum: 905b198ed04d26c951b80545d45c7f2e0432ef89977a93af8a762501d659886e39dda0fbffb0d517ff3fa450a3d09a29146e4273c2170624e1988f889fb5302c + languageName: node + linkType: hard + "are-we-there-yet@npm:^2.0.0": version: 2.0.0 resolution: "are-we-there-yet@npm:2.0.0" @@ -14829,6 +14955,13 @@ __metadata: languageName: node linkType: hard +"arg@npm:5.0.2, arg@npm:^5.0.2": + version: 5.0.2 + resolution: "arg@npm:5.0.2" + checksum: 6c69ada1a9943d332d9e5382393e897c500908d91d5cb735a01120d5f71daf1b339b7b8980cbeaba8fd1afc68e658a739746179e4315a26e8a28951ff9930078 + languageName: node + linkType: hard + "arg@npm:^4.1.0": version: 4.1.3 resolution: "arg@npm:4.1.3" @@ -14836,13 +14969,6 @@ __metadata: languageName: node linkType: hard -"arg@npm:^5.0.2": - version: 5.0.2 - resolution: "arg@npm:5.0.2" - checksum: 6c69ada1a9943d332d9e5382393e897c500908d91d5cb735a01120d5f71daf1b339b7b8980cbeaba8fd1afc68e658a739746179e4315a26e8a28951ff9930078 - languageName: node - linkType: hard - "argparse@npm:^1.0.10, argparse@npm:^1.0.7": version: 1.0.10 resolution: "argparse@npm:1.0.10" @@ -15238,7 +15364,7 @@ __metadata: languageName: node linkType: hard -"async@npm:^3.2.4": +"async@npm:^3.2.3, async@npm:^3.2.4": version: 3.2.4 resolution: "async@npm:3.2.4" checksum: 43d07459a4e1d09b84a20772414aa684ff4de085cbcaec6eea3c7a8f8150e8c62aa6cd4e699fe8ee93c3a5b324e777d34642531875a0817a35697522c1b02e89 @@ -15806,7 +15932,7 @@ __metadata: languageName: node linkType: hard -"bl@npm:^4.1.0": +"bl@npm:^4.0.3, bl@npm:^4.1.0": version: 4.1.0 resolution: "bl@npm:4.1.0" dependencies: @@ -16194,6 +16320,13 @@ __metadata: languageName: node linkType: hard +"bson@npm:6.1.0": + version: 6.1.0 + resolution: "bson@npm:6.1.0" + checksum: 8250c8158c22d2a0ca0e7677c0cbef9fa6341c176382b835dbf4c7f8aebdfd74d530f7225c6f3b98ca78a68aab4f1ad3d2fad54160a98b3e6ed9823b80db2e48 + languageName: node + linkType: hard + "bson@npm:^5.0.0": version: 5.0.1 resolution: "bson@npm:5.0.1" @@ -16210,6 +16343,13 @@ __metadata: languageName: node linkType: hard +"buffer-crc32@npm:^0.2.1, buffer-crc32@npm:^0.2.13": + version: 0.2.13 + resolution: "buffer-crc32@npm:0.2.13" + checksum: 06252347ae6daca3453b94e4b2f1d3754a3b146a111d81c68924c22d91889a40623264e95e67955b1cb4a68cbedf317abeabb5140a9766ed248973096db5ce1c + languageName: node + linkType: hard + "buffer-equal-constant-time@npm:1.0.1": version: 1.0.1 resolution: "buffer-equal-constant-time@npm:1.0.1" @@ -16538,6 +16678,7 @@ __metadata: lucide-react: ^0.171.0 mailhog: ^4.16.0 prettier: ^2.8.6 + prismock: ^1.21.1 tsc-absolute: ^1.0.0 turbo: ^1.10.1 typescript: ^4.9.4 @@ -16931,6 +17072,20 @@ __metadata: languageName: node linkType: hard +"checkpoint-client@npm:1.1.27": + version: 1.1.27 + resolution: "checkpoint-client@npm:1.1.27" + dependencies: + ci-info: 3.8.0 + env-paths: 2.2.1 + make-dir: 4.0.0 + ms: 2.1.3 + node-fetch: 2.6.12 + uuid: 9.0.0 + checksum: 178b4160f7c6d3ca0deecee6d0820c859fd73ff6e3c11b34b27411617c5b32ffb31194d5f9c880b3600376ddf9f0845e89889e519d17a870db69e7e2851c1e90 + languageName: node + linkType: hard + "chokidar@npm:^2.1.8": version: 2.1.8 resolution: "chokidar@npm:2.1.8" @@ -16994,6 +17149,13 @@ __metadata: languageName: node linkType: hard +"ci-info@npm:3.8.0, ci-info@npm:^3.1.0": + version: 3.8.0 + resolution: "ci-info@npm:3.8.0" + checksum: d0a4d3160497cae54294974a7246202244fff031b0a6ea20dd57b10ec510aa17399c41a1b0982142c105f3255aff2173e5c0dd7302ee1b2f28ba3debda375098 + languageName: node + linkType: hard + "ci-info@npm:^2.0.0": version: 2.0.0 resolution: "ci-info@npm:2.0.0" @@ -17001,13 +17163,6 @@ __metadata: languageName: node linkType: hard -"ci-info@npm:^3.1.0": - version: 3.8.0 - resolution: "ci-info@npm:3.8.0" - checksum: d0a4d3160497cae54294974a7246202244fff031b0a6ea20dd57b10ec510aa17399c41a1b0982142c105f3255aff2173e5c0dd7302ee1b2f28ba3debda375098 - languageName: node - linkType: hard - "ci-info@npm:^3.2.0": version: 3.3.0 resolution: "ci-info@npm:3.3.0" @@ -17179,7 +17334,7 @@ __metadata: languageName: node linkType: hard -"cli-truncate@npm:^2.1.0": +"cli-truncate@npm:2.1.0, cli-truncate@npm:^2.1.0": version: 2.1.0 resolution: "cli-truncate@npm:2.1.0" dependencies: @@ -17598,6 +17753,18 @@ __metadata: languageName: node linkType: hard +"compress-commons@npm:^4.1.2": + version: 4.1.2 + resolution: "compress-commons@npm:4.1.2" + dependencies: + buffer-crc32: ^0.2.13 + crc32-stream: ^4.0.2 + normalize-path: ^3.0.0 + readable-stream: ^3.6.0 + checksum: b50c4b5d6b8917ea164eef81d414b1824f27e02427f9266926c80aad55f9e15f81f74c274770773c2b732c22d1081b81dedce4f133271a466151f7f36b8e9dc9 + languageName: node + linkType: hard + "compressible@npm:~2.0.16": version: 2.0.18 resolution: "compressible@npm:2.0.18" @@ -18005,6 +18172,16 @@ __metadata: languageName: node linkType: hard +"crc32-stream@npm:^4.0.2": + version: 4.0.3 + resolution: "crc32-stream@npm:4.0.3" + dependencies: + crc-32: ^1.2.0 + readable-stream: ^3.4.0 + checksum: d44d0ec6f04d8a1bed899ac3e4fbb82111ed567ea6d506be39147362af45c747887fce1032f4beca1646b4824e5a9614cd3332bfa94bbc5577ca5445e7f75ddd + languageName: node + linkType: hard + "create-ecdh@npm:^4.0.0": version: 4.0.4 resolution: "create-ecdh@npm:4.0.4" @@ -18157,6 +18334,13 @@ __metadata: languageName: node linkType: hard +"crypto-random-string@npm:^2.0.0": + version: 2.0.0 + resolution: "crypto-random-string@npm:2.0.0" + checksum: 0283879f55e7c16fdceacc181f87a0a65c53bc16ffe1d58b9d19a6277adcd71900d02bb2c4843dd55e78c51e30e89b0fec618a7f170ebcc95b33182c28f05fd6 + languageName: node + linkType: hard + "crypto@npm:^1.0.1": version: 1.0.1 resolution: "crypto@npm:1.0.1" @@ -18823,6 +19007,22 @@ __metadata: languageName: node linkType: hard +"del@npm:^6.0.0": + version: 6.1.1 + resolution: "del@npm:6.1.1" + dependencies: + globby: ^11.0.1 + graceful-fs: ^4.2.4 + is-glob: ^4.0.1 + is-path-cwd: ^2.2.0 + is-path-inside: ^3.0.2 + p-map: ^4.0.0 + rimraf: ^3.0.2 + slash: ^3.0.0 + checksum: 563288b73b8b19a7261c47fd21a330eeab6e2acd7c6208c49790dfd369127120dd7836cdf0c1eca216b77c94782a81507eac6b4734252d3bef2795cb366996b6 + languageName: node + linkType: hard + "delayed-stream@npm:~1.0.0": version: 1.0.0 resolution: "delayed-stream@npm:1.0.0" @@ -19267,6 +19467,13 @@ __metadata: languageName: node linkType: hard +"dotenv@npm:16.0.3, dotenv@npm:^16.0.3": + version: 16.0.3 + resolution: "dotenv@npm:16.0.3" + checksum: afcf03f373d7a6d62c7e9afea6328e62851d627a4e73f2e12d0a8deae1cd375892004f3021883f8aec85932cd2834b091f568ced92b4774625b321db83b827f8 + languageName: node + linkType: hard + "dotenv@npm:^10.0.0": version: 10.0.0 resolution: "dotenv@npm:10.0.0" @@ -19281,13 +19488,6 @@ __metadata: languageName: node linkType: hard -"dotenv@npm:^16.0.3": - version: 16.0.3 - resolution: "dotenv@npm:16.0.3" - checksum: afcf03f373d7a6d62c7e9afea6328e62851d627a4e73f2e12d0a8deae1cd375892004f3021883f8aec85932cd2834b091f568ced92b4774625b321db83b827f8 - languageName: node - linkType: hard - "dotenv@npm:^16.3.1": version: 16.3.1 resolution: "dotenv@npm:16.3.1" @@ -19500,7 +19700,7 @@ __metadata: languageName: node linkType: hard -"end-of-stream@npm:^1.0.0, end-of-stream@npm:^1.1.0": +"end-of-stream@npm:^1.0.0, end-of-stream@npm:^1.1.0, end-of-stream@npm:^1.4.1": version: 1.4.4 resolution: "end-of-stream@npm:1.4.4" dependencies: @@ -19603,7 +19803,7 @@ __metadata: languageName: node linkType: hard -"env-paths@npm:^2.2.0": +"env-paths@npm:2.2.1, env-paths@npm:^2.2.0": version: 2.2.1 resolution: "env-paths@npm:2.2.1" checksum: 65b5df55a8bab92229ab2b40dad3b387fad24613263d103a97f91c9fe43ceb21965cd3392b1ccb5d77088021e525c4e0481adb309625d0cb94ade1d1fb8dc17e @@ -19946,6 +20146,13 @@ __metadata: languageName: node linkType: hard +"escape-string-regexp@npm:4.0.0, escape-string-regexp@npm:^4.0.0": + version: 4.0.0 + resolution: "escape-string-regexp@npm:4.0.0" + checksum: 98b48897d93060f2322108bf29db0feba7dd774be96cd069458d1453347b25ce8682ecc39859d4bca2203cc0ab19c237bcc71755eff49a0f8d90beadeeba5cc5 + languageName: node + linkType: hard + "escape-string-regexp@npm:^1.0.2, escape-string-regexp@npm:^1.0.5": version: 1.0.5 resolution: "escape-string-regexp@npm:1.0.5" @@ -19960,13 +20167,6 @@ __metadata: languageName: node linkType: hard -"escape-string-regexp@npm:^4.0.0": - version: 4.0.0 - resolution: "escape-string-regexp@npm:4.0.0" - checksum: 98b48897d93060f2322108bf29db0feba7dd774be96cd069458d1453347b25ce8682ecc39859d4bca2203cc0ab19c237bcc71755eff49a0f8d90beadeeba5cc5 - languageName: node - linkType: hard - "escodegen@npm:^2.0.0": version: 2.0.0 resolution: "escodegen@npm:2.0.0" @@ -20279,16 +20479,6 @@ __metadata: languageName: node linkType: hard -"eslint-scope@npm:^7.2.2": - version: 7.2.2 - resolution: "eslint-scope@npm:7.2.2" - dependencies: - esrecurse: ^4.3.0 - estraverse: ^5.2.0 - checksum: ec97dbf5fb04b94e8f4c5a91a7f0a6dd3c55e46bfc7bbcd0e3138c3a76977570e02ed89a1810c778dcd72072ff0e9621ba1379b4babe53921d71e2e4486fda3e - languageName: node - linkType: hard - "eslint-utils@npm:^3.0.0": version: 3.0.0 resolution: "eslint-utils@npm:3.0.0" @@ -20314,13 +20504,6 @@ __metadata: languageName: node linkType: hard -"eslint-visitor-keys@npm:^3.4.1, eslint-visitor-keys@npm:^3.4.3": - version: 3.4.3 - resolution: "eslint-visitor-keys@npm:3.4.3" - checksum: 36e9ef87fca698b6fd7ca5ca35d7b2b6eeaaf106572e2f7fd31c12d3bfdaccdb587bba6d3621067e5aece31c8c3a348b93922ab8f7b2cbc6aaab5e1d89040c60 - languageName: node - linkType: hard - "eslint@npm:8.4.1": version: 8.4.1 resolution: "eslint@npm:8.4.1" @@ -20369,53 +20552,6 @@ __metadata: languageName: node linkType: hard -"eslint@npm:^8.22.0": - version: 8.49.0 - resolution: "eslint@npm:8.49.0" - dependencies: - "@eslint-community/eslint-utils": ^4.2.0 - "@eslint-community/regexpp": ^4.6.1 - "@eslint/eslintrc": ^2.1.2 - "@eslint/js": 8.49.0 - "@humanwhocodes/config-array": ^0.11.11 - "@humanwhocodes/module-importer": ^1.0.1 - "@nodelib/fs.walk": ^1.2.8 - ajv: ^6.12.4 - chalk: ^4.0.0 - cross-spawn: ^7.0.2 - debug: ^4.3.2 - doctrine: ^3.0.0 - escape-string-regexp: ^4.0.0 - eslint-scope: ^7.2.2 - eslint-visitor-keys: ^3.4.3 - espree: ^9.6.1 - esquery: ^1.4.2 - esutils: ^2.0.2 - fast-deep-equal: ^3.1.3 - file-entry-cache: ^6.0.1 - find-up: ^5.0.0 - glob-parent: ^6.0.2 - globals: ^13.19.0 - graphemer: ^1.4.0 - ignore: ^5.2.0 - imurmurhash: ^0.1.4 - is-glob: ^4.0.0 - is-path-inside: ^3.0.3 - js-yaml: ^4.1.0 - json-stable-stringify-without-jsonify: ^1.0.1 - levn: ^0.4.1 - lodash.merge: ^4.6.2 - minimatch: ^3.1.2 - natural-compare: ^1.4.0 - optionator: ^0.9.3 - strip-ansi: ^6.0.1 - text-table: ^0.2.0 - bin: - eslint: bin/eslint.js - checksum: 4dfe257e1e42da2f9da872b05aaaf99b0f5aa022c1a91eee8f2af1ab72651b596366320c575ccd4e0469f7b4c97aff5bb85ae3323ebd6a293c3faef4028b0d81 - languageName: node - linkType: hard - "eslint@npm:^8.34.0": version: 8.34.0 resolution: "eslint@npm:8.34.0" @@ -20516,17 +20652,6 @@ __metadata: languageName: node linkType: hard -"espree@npm:^9.6.0, espree@npm:^9.6.1": - version: 9.6.1 - resolution: "espree@npm:9.6.1" - dependencies: - acorn: ^8.9.0 - acorn-jsx: ^5.3.2 - eslint-visitor-keys: ^3.4.1 - checksum: eb8c149c7a2a77b3f33a5af80c10875c3abd65450f60b8af6db1bfcfa8f101e21c1e56a561c6dc13b848e18148d43469e7cd208506238554fb5395a9ea5a1ab9 - languageName: node - linkType: hard - "esprima@npm:^4.0.0, esprima@npm:^4.0.1": version: 4.0.1 resolution: "esprima@npm:4.0.1" @@ -20546,15 +20671,6 @@ __metadata: languageName: node linkType: hard -"esquery@npm:^1.4.2": - version: 1.5.0 - resolution: "esquery@npm:1.5.0" - dependencies: - estraverse: ^5.1.0 - checksum: aefb0d2596c230118656cd4ec7532d447333a410a48834d80ea648b1e7b5c9bc9ed8b5e33a89cb04e487b60d622f44cf5713bf4abed7c97343edefdc84a35900 - languageName: node - linkType: hard - "esrecurse@npm:^4.1.0, esrecurse@npm:^4.3.0": version: 4.3.0 resolution: "esrecurse@npm:4.3.0" @@ -20803,7 +20919,7 @@ __metadata: languageName: node linkType: hard -"execa@npm:^5.1.1": +"execa@npm:5.1.1, execa@npm:^5.1.1": version: 5.1.1 resolution: "execa@npm:5.1.1" dependencies: @@ -21436,6 +21552,17 @@ __metadata: languageName: node linkType: hard +"find-cache-dir@npm:3.3.2, find-cache-dir@npm:^3.3.1": + version: 3.3.2 + resolution: "find-cache-dir@npm:3.3.2" + dependencies: + commondir: ^1.0.1 + make-dir: ^3.0.2 + pkg-dir: ^4.1.0 + checksum: 1e61c2e64f5c0b1c535bd85939ae73b0e5773142713273818cc0b393ee3555fb0fd44e1a5b161b8b6c3e03e98c2fcc9c227d784850a13a90a8ab576869576817 + languageName: node + linkType: hard + "find-cache-dir@npm:^2.0.0, find-cache-dir@npm:^2.1.0": version: 2.1.0 resolution: "find-cache-dir@npm:2.1.0" @@ -21447,17 +21574,6 @@ __metadata: languageName: node linkType: hard -"find-cache-dir@npm:^3.3.1": - version: 3.3.2 - resolution: "find-cache-dir@npm:3.3.2" - dependencies: - commondir: ^1.0.1 - make-dir: ^3.0.2 - pkg-dir: ^4.1.0 - checksum: 1e61c2e64f5c0b1c535bd85939ae73b0e5773142713273818cc0b393ee3555fb0fd44e1a5b161b8b6c3e03e98c2fcc9c227d784850a13a90a8ab576869576817 - languageName: node - linkType: hard - "find-root@npm:^1.1.0": version: 1.1.0 resolution: "find-root@npm:1.1.0" @@ -21465,6 +21581,16 @@ __metadata: languageName: node linkType: hard +"find-up@npm:5.0.0, find-up@npm:^5.0.0": + version: 5.0.0 + resolution: "find-up@npm:5.0.0" + dependencies: + locate-path: ^6.0.0 + path-exists: ^4.0.0 + checksum: 07955e357348f34660bde7920783204ff5a26ac2cafcaa28bace494027158a97b9f56faaf2d89a6106211a8174db650dd9f503f9c0d526b1202d5554a00b9095 + languageName: node + linkType: hard + "find-up@npm:^1.0.0": version: 1.1.2 resolution: "find-up@npm:1.1.2" @@ -21503,16 +21629,6 @@ __metadata: languageName: node linkType: hard -"find-up@npm:^5.0.0": - version: 5.0.0 - resolution: "find-up@npm:5.0.0" - dependencies: - locate-path: ^6.0.0 - path-exists: ^4.0.0 - checksum: 07955e357348f34660bde7920783204ff5a26ac2cafcaa28bace494027158a97b9f56faaf2d89a6106211a8174db650dd9f503f9c0d526b1202d5554a00b9095 - languageName: node - linkType: hard - "find-yarn-workspace-root2@npm:1.2.16": version: 1.2.16 resolution: "find-yarn-workspace-root2@npm:1.2.16" @@ -21789,6 +21905,13 @@ __metadata: languageName: node linkType: hard +"fp-ts@npm:2.16.0": + version: 2.16.0 + resolution: "fp-ts@npm:2.16.0" + checksum: 0724a4d8b2171cc98e9d9a05e6fe7e0acbc8c79eaabe8e257cbe3a6a5a77acdbdd2572e2ca8135e91d6e02542237499db6e952a58829ba0796ab96063dd39eb2 + languageName: node + linkType: hard + "fraction.js@npm:^4.2.0": version: 4.2.0 resolution: "fraction.js@npm:4.2.0" @@ -21850,6 +21973,24 @@ __metadata: languageName: node linkType: hard +"fs-constants@npm:^1.0.0": + version: 1.0.0 + resolution: "fs-constants@npm:1.0.0" + checksum: 18f5b718371816155849475ac36c7d0b24d39a11d91348cfcb308b4494824413e03572c403c86d3a260e049465518c4f0d5bd00f0371cdfcad6d4f30a85b350d + languageName: node + linkType: hard + +"fs-extra@npm:11.1.1": + version: 11.1.1 + resolution: "fs-extra@npm:11.1.1" + dependencies: + graceful-fs: ^4.2.0 + jsonfile: ^6.0.1 + universalify: ^2.0.0 + checksum: fb883c68245b2d777fbc1f2082c9efb084eaa2bbf9fddaa366130d196c03608eebef7fb490541276429ee1ca99f317e2d73e96f5ca0999eefedf5a624ae1edfd + languageName: node + linkType: hard + "fs-extra@npm:^10.1.0": version: 10.1.0 resolution: "fs-extra@npm:10.1.0" @@ -21906,6 +22047,15 @@ __metadata: languageName: node linkType: hard +"fs-jetpack@npm:5.1.0": + version: 5.1.0 + resolution: "fs-jetpack@npm:5.1.0" + dependencies: + minimatch: ^5.1.0 + checksum: a30fc7884ded99619b981f16bf096d93693d36c53dab87f1d8339d63f6c9c89c45981c47ddd4d08f4045c3181cc843ee2a83ed4834683eb8cb96665332f89b36 + languageName: node + linkType: hard + "fs-minipass@npm:^1.2.7": version: 1.2.7 resolution: "fs-minipass@npm:1.2.7" @@ -22391,7 +22541,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^7.1.1, glob@npm:^7.2.0": +"glob@npm:^7.1.1, glob@npm:^7.2.0, glob@npm:^7.2.3": version: 7.2.3 resolution: "glob@npm:7.2.3" dependencies: @@ -22432,6 +22582,15 @@ __metadata: languageName: node linkType: hard +"global-dirs@npm:3.0.1": + version: 3.0.1 + resolution: "global-dirs@npm:3.0.1" + dependencies: + ini: 2.0.0 + checksum: 70147b80261601fd40ac02a104581432325c1c47329706acd773f3a6ce99bb36d1d996038c85ccacd482ad22258ec233c586b6a91535b1a116b89663d49d6438 + languageName: node + linkType: hard + "global@npm:^4.4.0, global@npm:~4.4.0": version: 4.4.0 resolution: "global@npm:4.4.0" @@ -22492,7 +22651,7 @@ __metadata: languageName: node linkType: hard -"globby@npm:^11.0.0, globby@npm:^11.0.2, globby@npm:^11.0.3, globby@npm:^11.1.0": +"globby@npm:11.1.0, globby@npm:^11.0.0, globby@npm:^11.0.1, globby@npm:^11.0.2, globby@npm:^11.0.3, globby@npm:^11.1.0": version: 11.1.0 resolution: "globby@npm:11.1.0" dependencies: @@ -22729,13 +22888,6 @@ __metadata: languageName: node linkType: hard -"graphemer@npm:^1.4.0": - version: 1.4.0 - resolution: "graphemer@npm:1.4.0" - checksum: bab8f0be9b568857c7bec9fda95a89f87b783546d02951c40c33f84d05bb7da3fd10f863a9beb901463669b6583173a8c8cc6d6b306ea2b9b9d5d3d943c3a673 - languageName: node - linkType: hard - "graphql-config@npm:^5.0.2": version: 5.0.2 resolution: "graphql-config@npm:5.0.2" @@ -23051,6 +23203,16 @@ __metadata: languageName: node linkType: hard +"hasha@npm:5.2.2": + version: 5.2.2 + resolution: "hasha@npm:5.2.2" + dependencies: + is-stream: ^2.0.0 + type-fest: ^0.8.0 + checksum: 06cc474bed246761ff61c19d629977eb5f53fa817be4313a255a64ae0f433e831a29e83acb6555e3f4592b348497596f1d1653751008dda4f21c9c21ca60ac5a + languageName: node + linkType: hard + "hast-to-hyperscript@npm:^9.0.0": version: 9.0.1 resolution: "hast-to-hyperscript@npm:9.0.1" @@ -23549,6 +23711,16 @@ __metadata: languageName: node linkType: hard +"http-proxy-agent@npm:7.0.0, http-proxy-agent@npm:^7.0.0": + version: 7.0.0 + resolution: "http-proxy-agent@npm:7.0.0" + dependencies: + agent-base: ^7.1.0 + debug: ^4.3.4 + checksum: 48d4fac997917e15f45094852b63b62a46d0c8a4f0b9c6c23ca26d27b8df8d178bed88389e604745e748bd9a01f5023e25093722777f0593c3f052009ff438b6 + languageName: node + linkType: hard + "http-proxy-agent@npm:^4.0.1": version: 4.0.1 resolution: "http-proxy-agent@npm:4.0.1" @@ -23571,16 +23743,6 @@ __metadata: languageName: node linkType: hard -"http-proxy-agent@npm:^7.0.0": - version: 7.0.0 - resolution: "http-proxy-agent@npm:7.0.0" - dependencies: - agent-base: ^7.1.0 - debug: ^4.3.4 - checksum: 48d4fac997917e15f45094852b63b62a46d0c8a4f0b9c6c23ca26d27b8df8d178bed88389e604745e748bd9a01f5023e25093722777f0593c3f052009ff438b6 - languageName: node - linkType: hard - "http-signature@npm:~1.2.0": version: 1.2.0 resolution: "http-signature@npm:1.2.0" @@ -23629,6 +23791,16 @@ __metadata: languageName: node linkType: hard +"https-proxy-agent@npm:7.0.1": + version: 7.0.1 + resolution: "https-proxy-agent@npm:7.0.1" + dependencies: + agent-base: ^7.0.2 + debug: 4 + checksum: 2d765c31865071373771f53abdd72912567b76015a4eff61094f586194192950cd89257d50f0e621807a16c083bc8cad5852e3885c6ba154d2ce721a18fac248 + languageName: node + linkType: hard + "https-proxy-agent@npm:^5.0.1": version: 5.0.1 resolution: "https-proxy-agent@npm:5.0.1" @@ -23713,13 +23885,6 @@ __metadata: languageName: node linkType: hard -"i18next-fs-backend@npm:^1.1.4": - version: 1.2.0 - resolution: "i18next-fs-backend@npm:1.2.0" - checksum: da74d20f2b007f8e34eaf442fa91ad12aaff3b9891e066c6addd6d111b37e370c62370dfbc656730ab2f8afd988f2e7ea1c48301ebb19ccb716fb5965600eddf - languageName: node - linkType: hard - "i18next-fs-backend@npm:^2.1.1": version: 2.1.3 resolution: "i18next-fs-backend@npm:2.1.3" @@ -23727,15 +23892,6 @@ __metadata: languageName: node linkType: hard -"i18next@npm:^21.8.13": - version: 21.10.0 - resolution: "i18next@npm:21.10.0" - dependencies: - "@babel/runtime": ^7.17.2 - checksum: f997985e2d4d15a62a0936a82ff6420b97f3f971e776fe685bdd50b4de0cb4dc2198bc75efe6b152844794ebd5040d8060d6d152506a687affad534834836d81 - languageName: node - linkType: hard - "i18next@npm:^23.2.3": version: 23.2.3 resolution: "i18next@npm:23.2.3" @@ -23849,6 +24005,15 @@ __metadata: languageName: node linkType: hard +"ignore-walk@npm:^5.0.1": + version: 5.0.1 + resolution: "ignore-walk@npm:5.0.1" + dependencies: + minimatch: ^5.0.1 + checksum: 1a4ef35174653a1aa6faab3d9f8781269166536aee36a04946f6e2b319b2475c1903a75ed42f04219274128242f49d0a10e20c4354ee60d9548e97031451150b + languageName: node + linkType: hard + "ignore@npm:^4.0.3, ignore@npm:^4.0.6": version: 4.0.6 resolution: "ignore@npm:4.0.6" @@ -23942,6 +24107,13 @@ __metadata: languageName: node linkType: hard +"indent-string@npm:4.0.0, indent-string@npm:^4.0.0": + version: 4.0.0 + resolution: "indent-string@npm:4.0.0" + checksum: 824cfb9929d031dabf059bebfe08cf3137365e112019086ed3dcff6a0a7b698cb80cf67ccccde0e25b9e2d7527aa6cc1fed1ac490c752162496caba3e6699612 + languageName: node + linkType: hard + "indent-string@npm:^2.1.0": version: 2.1.0 resolution: "indent-string@npm:2.1.0" @@ -23951,13 +24123,6 @@ __metadata: languageName: node linkType: hard -"indent-string@npm:^4.0.0": - version: 4.0.0 - resolution: "indent-string@npm:4.0.0" - checksum: 824cfb9929d031dabf059bebfe08cf3137365e112019086ed3dcff6a0a7b698cb80cf67ccccde0e25b9e2d7527aa6cc1fed1ac490c752162496caba3e6699612 - languageName: node - linkType: hard - "indexof@npm:~0.0.1": version: 0.0.1 resolution: "indexof@npm:0.0.1" @@ -24003,6 +24168,13 @@ __metadata: languageName: node linkType: hard +"ini@npm:2.0.0": + version: 2.0.0 + resolution: "ini@npm:2.0.0" + checksum: e7aadc5fb2e4aefc666d74ee2160c073995a4061556b1b5b4241ecb19ad609243b9cceafe91bae49c219519394bbd31512516cb22a3b1ca6e66d869e0447e84e + languageName: node + linkType: hard + "ink-select-input@npm:^4.2.1": version: 4.2.1 resolution: "ink-select-input@npm:4.2.1" @@ -24392,7 +24564,7 @@ __metadata: languageName: node linkType: hard -"is-core-module@npm:^2.13.0": +"is-core-module@npm:^2.11.0": version: 2.13.0 resolution: "is-core-module@npm:2.13.0" dependencies: @@ -24702,7 +24874,14 @@ __metadata: languageName: node linkType: hard -"is-path-inside@npm:^3.0.3": +"is-path-cwd@npm:^2.2.0": + version: 2.2.0 + resolution: "is-path-cwd@npm:2.2.0" + checksum: 46a840921bb8cc0dc7b5b423a14220e7db338072a4495743a8230533ce78812dc152548c86f4b828411fe98c5451959f07cf841c6a19f611e46600bd699e8048 + languageName: node + linkType: hard + +"is-path-inside@npm:^3.0.2, is-path-inside@npm:^3.0.3": version: 3.0.3 resolution: "is-path-inside@npm:3.0.3" checksum: abd50f06186a052b349c15e55b182326f1936c89a78bf6c8f2b707412517c097ce04bc49a0ca221787bc44e1049f51f09a2ffb63d22899051988d3a618ba13e9 @@ -24957,7 +25136,7 @@ __metadata: languageName: node linkType: hard -"is-windows@npm:^1.0.0, is-windows@npm:^1.0.1, is-windows@npm:^1.0.2": +"is-windows@npm:1.0.2, is-windows@npm:^1.0.0, is-windows@npm:^1.0.1, is-windows@npm:^1.0.2": version: 1.0.2 resolution: "is-windows@npm:1.0.2" checksum: 438b7e52656fe3b9b293b180defb4e448088e7023a523ec21a91a80b9ff8cdb3377ddb5b6e60f7c7de4fa8b63ab56e121b6705fe081b3cf1b828b0a380009ad7 @@ -24971,14 +25150,7 @@ __metadata: languageName: node linkType: hard -"is-wsl@npm:^1.1.0": - version: 1.1.0 - resolution: "is-wsl@npm:1.1.0" - checksum: ea157d232351e68c92bd62fc541771096942fe72f69dff452dd26dcc31466258c570a3b04b8cda2e01cd2968255b02951b8670d08ea4ed76d6b1a646061ac4fe - languageName: node - linkType: hard - -"is-wsl@npm:^2.1.1, is-wsl@npm:^2.2.0": +"is-wsl@npm:2.2.0, is-wsl@npm:^2.1.1, is-wsl@npm:^2.2.0": version: 2.2.0 resolution: "is-wsl@npm:2.2.0" dependencies: @@ -24987,6 +25159,13 @@ __metadata: languageName: node linkType: hard +"is-wsl@npm:^1.1.0": + version: 1.1.0 + resolution: "is-wsl@npm:1.1.0" + checksum: ea157d232351e68c92bd62fc541771096942fe72f69dff452dd26dcc31466258c570a3b04b8cda2e01cd2968255b02951b8670d08ea4ed76d6b1a646061ac4fe + languageName: node + linkType: hard + "is@npm:~0.2.6": version: 0.2.7 resolution: "is@npm:0.2.7" @@ -25369,6 +25548,13 @@ __metadata: languageName: node linkType: hard +"jose@npm:4.11.1": + version: 4.11.1 + resolution: "jose@npm:4.11.1" + checksum: cd15cba258d0fd20f6168631ce2e94fda8442df80e43c1033c523915cecdf390a1cc8efe0eab0c2d65935ca973d791c668aea80724d2aa9c2879d4e70f3081d7 + languageName: node + linkType: hard + "jose@npm:4.12.0": version: 4.12.0 resolution: "jose@npm:4.12.0" @@ -26290,6 +26476,15 @@ __metadata: languageName: node linkType: hard +"lazystream@npm:^1.0.0": + version: 1.0.1 + resolution: "lazystream@npm:1.0.1" + dependencies: + readable-stream: ^2.0.5 + checksum: 822c54c6b87701a6491c70d4fabc4cafcf0f87d6b656af168ee7bb3c45de9128a801cb612e6eeeefc64d298a7524a698dd49b13b0121ae50c2ae305f0dcc5310 + languageName: node + linkType: hard + "leac@npm:^0.6.0": version: 0.6.0 resolution: "leac@npm:0.6.0" @@ -26504,13 +26699,6 @@ __metadata: languageName: node linkType: hard -"lilconfig@npm:^2.1.0": - version: 2.1.0 - resolution: "lilconfig@npm:2.1.0" - checksum: 8549bb352b8192375fed4a74694cd61ad293904eee33f9d4866c2192865c44c4eb35d10782966242634e0cbc1e91fe62b1247f148dc5514918e3a966da7ea117 - languageName: node - linkType: hard - "limiter@npm:^1.1.5": version: 1.1.5 resolution: "limiter@npm:1.1.5" @@ -26797,6 +26985,27 @@ __metadata: languageName: node linkType: hard +"lodash.defaults@npm:^4.2.0": + version: 4.2.0 + resolution: "lodash.defaults@npm:4.2.0" + checksum: 84923258235592c8886e29de5491946ff8c2ae5c82a7ac5cddd2e3cb697e6fbdfbbb6efcca015795c86eec2bb953a5a2ee4016e3735a3f02720428a40efbb8f1 + languageName: node + linkType: hard + +"lodash.difference@npm:^4.5.0": + version: 4.5.0 + resolution: "lodash.difference@npm:4.5.0" + checksum: ecee276aa578f300e79350805a14a51be8d1f12b3c1389a19996d8ab516f814211a5f65c68331571ecdad96522b863ccc484b55504ce8c9947212a29f8857d5a + languageName: node + linkType: hard + +"lodash.flatten@npm:^4.4.0": + version: 4.4.0 + resolution: "lodash.flatten@npm:4.4.0" + checksum: 0ac34a393d4b795d4b7421153d27c13ae67e08786c9cbb60ff5b732210d46f833598eee3fb3844bb10070e8488efe390ea53bb567377e0cb47e9e630bf0811cb + languageName: node + linkType: hard + "lodash.get@npm:^4.4.2, lodash.get@npm:~4.4.2": version: 4.4.2 resolution: "lodash.get@npm:4.4.2" @@ -26902,6 +27111,13 @@ __metadata: languageName: node linkType: hard +"lodash.union@npm:^4.6.0": + version: 4.6.0 + resolution: "lodash.union@npm:4.6.0" + checksum: 1514dc6508b2614ec071a6470f36eb7a70f69bf1abb6d55bdfdc21069635a4517783654b28504c0f025059a7598d37529766888e6d5902b8ab28b712228f7b2a + languageName: node + linkType: hard + "lodash.uniq@npm:4.5.0": version: 4.5.0 resolution: "lodash.uniq@npm:4.5.0" @@ -27266,6 +27482,15 @@ __metadata: languageName: node linkType: hard +"make-dir@npm:4.0.0": + version: 4.0.0 + resolution: "make-dir@npm:4.0.0" + dependencies: + semver: ^7.5.3 + checksum: bf0731a2dd3aab4db6f3de1585cea0b746bb73eb5a02e3d8d72757e376e64e6ada190b1eddcde5b2f24a81b688a9897efd5018737d05e02e2a671dda9cff8a8a + languageName: node + linkType: hard + "make-dir@npm:^2.0.0, make-dir@npm:^2.1.0": version: 2.1.0 resolution: "make-dir@npm:2.1.0" @@ -28190,7 +28415,7 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^5.0.1": +"minimatch@npm:^5.0.1, minimatch@npm:^5.1.0": version: 5.1.6 resolution: "minimatch@npm:5.1.6" dependencies: @@ -28877,6 +29102,13 @@ __metadata: languageName: node linkType: hard +"new-github-issue-url@npm:0.2.1": + version: 0.2.1 + resolution: "new-github-issue-url@npm:0.2.1" + checksum: 123c34296521353883ac7c43109a01a565a56fbd6d8e876db86978a74c798a9e1703180a77196b6abb5f608fd54c9853dda97a1eb43148ebebe7b0c89269f46d + languageName: node + linkType: hard + "next-api-middleware@npm:^1.0.1": version: 1.0.1 resolution: "next-api-middleware@npm:1.0.1" @@ -28888,31 +29120,6 @@ __metadata: languageName: node linkType: hard -"next-auth@npm:^4.10.3": - version: 4.23.1 - resolution: "next-auth@npm:4.23.1" - dependencies: - "@babel/runtime": ^7.20.13 - "@panva/hkdf": ^1.0.2 - cookie: ^0.5.0 - jose: ^4.11.4 - oauth: ^0.9.15 - openid-client: ^5.4.0 - preact: ^10.6.3 - preact-render-to-string: ^5.1.19 - uuid: ^8.3.2 - peerDependencies: - next: ^12.2.5 || ^13 - nodemailer: ^6.6.5 - react: ^17.0.2 || ^18 - react-dom: ^17.0.2 || ^18 - peerDependenciesMeta: - nodemailer: - optional: true - checksum: 995114797c257ccf71a82d19fb6316fb7709b552aaaf66444591c505a4b8e00b0cae3f4db4316b63a8cc439076044cc391ab171c4f6ee2e086709c5b3bbfed24 - languageName: node - linkType: hard - "next-auth@npm:^4.22.1": version: 4.22.1 resolution: "next-auth@npm:4.22.1" @@ -28962,24 +29169,6 @@ __metadata: languageName: node linkType: hard -"next-i18next@npm:^11.3.0": - version: 11.3.0 - resolution: "next-i18next@npm:11.3.0" - dependencies: - "@babel/runtime": ^7.18.6 - "@types/hoist-non-react-statics": ^3.3.1 - core-js: ^3 - hoist-non-react-statics: ^3.3.2 - i18next: ^21.8.13 - i18next-fs-backend: ^1.1.4 - react-i18next: ^11.18.0 - peerDependencies: - next: ">= 10.0.0" - react: ">= 16.8.0" - checksum: fbce97a4fbf9ad846c08652471a833c7f173c3e7ddc7cafa1423625b4a684715bb85f76ae06fe9cbed3e70f12b8e78e2459e5bc1a3c3f5c517743f17648f8939 - languageName: node - linkType: hard - "next-i18next@npm:^13.2.2": version: 13.3.0 resolution: "next-i18next@npm:13.3.0" @@ -29042,15 +29231,6 @@ __metadata: languageName: node linkType: hard -"next-transpile-modules@npm:^10.0.0": - version: 10.0.1 - resolution: "next-transpile-modules@npm:10.0.1" - dependencies: - enhanced-resolve: ^5.10.0 - checksum: 6cfffd165769a04b153e60f49f4f3896d16aa688304616f66c7b94299e2229b04db368217e6a7300d3110fe988a6c6e68025bb2cbda3edf49e9b29109ff671bd - languageName: node - linkType: hard - "next-transpile-modules@npm:^8.0.0": version: 8.0.0 resolution: "next-transpile-modules@npm:8.0.0" @@ -29070,62 +29250,6 @@ __metadata: languageName: node linkType: hard -"next@npm:^13.1.1": - version: 13.5.2 - resolution: "next@npm:13.5.2" - dependencies: - "@next/env": 13.5.2 - "@next/swc-darwin-arm64": 13.5.2 - "@next/swc-darwin-x64": 13.5.2 - "@next/swc-linux-arm64-gnu": 13.5.2 - "@next/swc-linux-arm64-musl": 13.5.2 - "@next/swc-linux-x64-gnu": 13.5.2 - "@next/swc-linux-x64-musl": 13.5.2 - "@next/swc-win32-arm64-msvc": 13.5.2 - "@next/swc-win32-ia32-msvc": 13.5.2 - "@next/swc-win32-x64-msvc": 13.5.2 - "@swc/helpers": 0.5.2 - busboy: 1.6.0 - caniuse-lite: ^1.0.30001406 - postcss: 8.4.14 - styled-jsx: 5.1.1 - watchpack: 2.4.0 - zod: 3.21.4 - peerDependencies: - "@opentelemetry/api": ^1.1.0 - react: ^18.2.0 - react-dom: ^18.2.0 - sass: ^1.3.0 - dependenciesMeta: - "@next/swc-darwin-arm64": - optional: true - "@next/swc-darwin-x64": - optional: true - "@next/swc-linux-arm64-gnu": - optional: true - "@next/swc-linux-arm64-musl": - optional: true - "@next/swc-linux-x64-gnu": - optional: true - "@next/swc-linux-x64-musl": - optional: true - "@next/swc-win32-arm64-msvc": - optional: true - "@next/swc-win32-ia32-msvc": - optional: true - "@next/swc-win32-x64-msvc": - optional: true - peerDependenciesMeta: - "@opentelemetry/api": - optional: true - sass: - optional: true - bin: - next: dist/bin/next - checksum: cc0635ad5aaab9fc1f4315b9506361b1abf1a12146542d6054b9434e2e892e967f19fbabd3f3763ba5e227306aa91627c1d73af089e9b853b84c74e20bb0be00 - languageName: node - linkType: hard - "next@npm:^13.4.6": version: 13.4.6 resolution: "next@npm:13.4.6" @@ -29234,6 +29358,20 @@ __metadata: languageName: node linkType: hard +"node-fetch@npm:2.6.12": + version: 2.6.12 + resolution: "node-fetch@npm:2.6.12" + dependencies: + whatwg-url: ^5.0.0 + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + checksum: 3bc1655203d47ee8e313c0d96664b9673a3d4dd8002740318e9d27d14ef306693a4b2ef8d6525775056fd912a19e23f3ac0d7111ad8925877b7567b29a625592 + languageName: node + linkType: hard + "node-fetch@npm:2.6.7, node-fetch@npm:^2.6.0, node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.7": version: 2.6.7 resolution: "node-fetch@npm:2.6.7" @@ -29480,6 +29618,36 @@ __metadata: languageName: node linkType: hard +"npm-bundled@npm:^2.0.0": + version: 2.0.1 + resolution: "npm-bundled@npm:2.0.1" + dependencies: + npm-normalize-package-bin: ^2.0.0 + checksum: 7747293985c48c5268871efe691545b03731cb80029692000cbdb0b3344b9617be5187aa36281cabbe6b938e3651b4e87236d1c31f9e645eef391a1a779413e6 + languageName: node + linkType: hard + +"npm-normalize-package-bin@npm:^2.0.0": + version: 2.0.0 + resolution: "npm-normalize-package-bin@npm:2.0.0" + checksum: 7c5379f9b188b564c4332c97bdd9a5d6b7b15f02b5823b00989d6a0e6fb31eb0280f02b0a924f930e1fcaf00e60fae333aec8923d2a4c7747613c7d629d8aa25 + languageName: node + linkType: hard + +"npm-packlist@npm:5.1.3": + version: 5.1.3 + resolution: "npm-packlist@npm:5.1.3" + dependencies: + glob: ^8.0.1 + ignore-walk: ^5.0.1 + npm-bundled: ^2.0.0 + npm-normalize-package-bin: ^2.0.0 + bin: + npm-packlist: bin/index.js + checksum: 94cc9c66740e8f80243301de85eb0a2cec5bbd570c3f26b6ad7af1a3eca155f7e810580dc7ea4448f12a8fd82f6db307e7132a5fe69e157eb45b325acadeb22a + languageName: node + linkType: hard + "npm-run-all@npm:^4.1.5": version: 4.1.5 resolution: "npm-run-all@npm:4.1.5" @@ -29597,6 +29765,13 @@ __metadata: languageName: node linkType: hard +"oauth4webapi@npm:2.0.5": + version: 2.0.5 + resolution: "oauth4webapi@npm:2.0.5" + checksum: 32d0cb7b1cca42d51dfb88075ca2d69fe33172a807e8ea50e317d17cab3bc80588ab8ebcb7eb4600c371a70af4674595b4b341daf6f3a655f1efa1ab715bb6c9 + languageName: node + linkType: hard + "oauth@npm:^0.9.15": version: 0.9.15 resolution: "oauth@npm:0.9.15" @@ -29902,7 +30077,7 @@ __metadata: languageName: node linkType: hard -"open@npm:^7.0.3": +"open@npm:7.4.2, open@npm:^7.0.3": version: 7.4.2 resolution: "open@npm:7.4.2" dependencies: @@ -30018,20 +30193,6 @@ __metadata: languageName: node linkType: hard -"optionator@npm:^0.9.3": - version: 0.9.3 - resolution: "optionator@npm:0.9.3" - dependencies: - "@aashutoshrathi/word-wrap": ^1.2.3 - deep-is: ^0.1.3 - fast-levenshtein: ^2.0.6 - levn: ^0.4.1 - prelude-ls: ^1.2.1 - type-check: ^0.4.0 - checksum: 09281999441f2fe9c33a5eeab76700795365a061563d66b098923eb719251a42bdbe432790d35064d0816ead9296dbeb1ad51a733edf4167c96bd5d0882e428a - languageName: node - linkType: hard - "options-defaults@npm:^2.0.39": version: 2.0.40 resolution: "options-defaults@npm:2.0.40" @@ -30148,7 +30309,7 @@ __metadata: languageName: node linkType: hard -"p-filter@npm:^2.1.0": +"p-filter@npm:2.1.0, p-filter@npm:^2.1.0": version: 2.1.0 resolution: "p-filter@npm:2.1.0" dependencies: @@ -30236,6 +30397,15 @@ __metadata: languageName: node linkType: hard +"p-map@npm:4.0.0, p-map@npm:^4.0.0": + version: 4.0.0 + resolution: "p-map@npm:4.0.0" + dependencies: + aggregate-error: ^3.0.0 + checksum: cb0ab21ec0f32ddffd31dfc250e3afa61e103ef43d957cc45497afe37513634589316de4eb88abdfd969fe6410c22c0b93ab24328833b8eb1ccc087fc0442a1c + languageName: node + linkType: hard + "p-map@npm:^2.0.0": version: 2.1.0 resolution: "p-map@npm:2.1.0" @@ -30252,15 +30422,6 @@ __metadata: languageName: node linkType: hard -"p-map@npm:^4.0.0": - version: 4.0.0 - resolution: "p-map@npm:4.0.0" - dependencies: - aggregate-error: ^3.0.0 - checksum: cb0ab21ec0f32ddffd31dfc250e3afa61e103ef43d957cc45497afe37513634589316de4eb88abdfd969fe6410c22c0b93ab24328833b8eb1ccc087fc0442a1c - languageName: node - linkType: hard - "p-queue@npm:^6.6.2": version: 6.6.2 resolution: "p-queue@npm:6.6.2" @@ -30271,7 +30432,7 @@ __metadata: languageName: node linkType: hard -"p-retry@npm:4": +"p-retry@npm:4, p-retry@npm:4.6.2": version: 4.6.2 resolution: "p-retry@npm:4.6.2" dependencies: @@ -31110,19 +31271,6 @@ __metadata: languageName: node linkType: hard -"postcss-import@npm:^15.1.0": - version: 15.1.0 - resolution: "postcss-import@npm:15.1.0" - dependencies: - postcss-value-parser: ^4.0.0 - read-cache: ^1.0.0 - resolve: ^1.1.7 - peerDependencies: - postcss: ^8.0.0 - checksum: 7bd04bd8f0235429009d0022cbf00faebc885de1d017f6d12ccb1b021265882efc9302006ba700af6cab24c46bfa2f3bc590be3f9aee89d064944f171b04e2a3 - languageName: node - linkType: hard - "postcss-js@npm:^4.0.0": version: 4.0.0 resolution: "postcss-js@npm:4.0.0" @@ -31134,17 +31282,6 @@ __metadata: languageName: node linkType: hard -"postcss-js@npm:^4.0.1": - version: 4.0.1 - resolution: "postcss-js@npm:4.0.1" - dependencies: - camelcase-css: ^2.0.1 - peerDependencies: - postcss: ^8.4.21 - checksum: 5c1e83efeabeb5a42676193f4357aa9c88f4dc1b3c4a0332c132fe88932b33ea58848186db117cf473049fc233a980356f67db490bd0a7832ccba9d0b3fd3491 - languageName: node - linkType: hard - "postcss-load-config@npm:^3.1.4": version: 3.1.4 resolution: "postcss-load-config@npm:3.1.4" @@ -31163,24 +31300,6 @@ __metadata: languageName: node linkType: hard -"postcss-load-config@npm:^4.0.1": - version: 4.0.1 - resolution: "postcss-load-config@npm:4.0.1" - dependencies: - lilconfig: ^2.0.5 - yaml: ^2.1.1 - peerDependencies: - postcss: ">=8.0.9" - ts-node: ">=9.0.0" - peerDependenciesMeta: - postcss: - optional: true - ts-node: - optional: true - checksum: b61f890499ed7dcda1e36c20a9582b17d745bad5e2b2c7bc96942465e406bc43ae03f270c08e60d1e29dab1ee50cb26970b5eb20c9aae30e066e20bd607ae4e4 - languageName: node - linkType: hard - "postcss-loader@npm:^4.2.0": version: 4.3.0 resolution: "postcss-loader@npm:4.3.0" @@ -31307,17 +31426,6 @@ __metadata: languageName: node linkType: hard -"postcss-nested@npm:^6.0.1": - version: 6.0.1 - resolution: "postcss-nested@npm:6.0.1" - dependencies: - postcss-selector-parser: ^6.0.11 - peerDependencies: - postcss: ^8.2.14 - checksum: 7ddb0364cd797de01e38f644879189e0caeb7ea3f78628c933d91cc24f327c56d31269384454fc02ecaf503b44bfa8e08870a7c4cc56b23bc15640e1894523fa - languageName: node - linkType: hard - "postcss-pseudo-companion-classes@npm:^0.1.1": version: 0.1.1 resolution: "postcss-pseudo-companion-classes@npm:0.1.1" @@ -31445,6 +31553,17 @@ __metadata: languageName: node linkType: hard +"preact-render-to-string@npm:5.2.3": + version: 5.2.3 + resolution: "preact-render-to-string@npm:5.2.3" + dependencies: + pretty-format: ^3.8.0 + peerDependencies: + preact: ">=10" + checksum: 6e46288d8956adde35b9fe3a21aecd9dea29751b40f0f155dea62f3896f27cb8614d457b32f48d33909d2da81135afcca6c55077520feacd7d15164d1371fb44 + languageName: node + linkType: hard + "preact-render-to-string@npm:^5.1.19": version: 5.2.6 resolution: "preact-render-to-string@npm:5.2.6" @@ -31456,7 +31575,7 @@ __metadata: languageName: node linkType: hard -"preact@npm:^10.6.3": +"preact@npm:10.11.3, preact@npm:^10.6.3": version: 10.11.3 resolution: "preact@npm:10.11.3" checksum: 9387115aa0581e8226309e6456e9856f17dfc0e3d3e63f774de80f3d462a882ba7c60914c05942cb51d51e23e120dcfe904b8d392d46f29ad15802941fe7a367 @@ -31690,15 +31809,14 @@ __metadata: languageName: node linkType: hard -"prisma@npm:^4.8.1": - version: 4.16.2 - resolution: "prisma@npm:4.16.2" +"prisma@npm:^5.0.0": + version: 5.3.1 + resolution: "prisma@npm:5.3.1" dependencies: - "@prisma/engines": 4.16.2 + "@prisma/engines": 5.3.1 bin: prisma: build/index.js - prisma2: build/index.js - checksum: 1d0ed616abd7f8de22441e333b976705f1cb05abcb206965df3fc6a7ea03911ef467dd484a4bc51fdc6cff72dd9857b9852be5f232967a444af0a98c49bfdb76 + checksum: e825adbcb4eec81de276de5507fb7e5486db7788c8c9de36ba6ed73f9e87d9f56b64d0e183a31dc6b80f6737ae1fbcdb110aac44ab89299af646aeb966655bef languageName: node linkType: hard @@ -31727,6 +31845,20 @@ __metadata: languageName: node linkType: hard +"prismock@npm:^1.21.1": + version: 1.21.1 + resolution: "prismock@npm:1.21.1" + dependencies: + "@paralleldrive/cuid2": 2.2.2 + "@prisma/generator-helper": 5.1.1 + "@prisma/internals": 5.1.1 + bson: 6.1.0 + peerDependencies: + "@prisma/client": "*" + checksum: d64211ea4167cf15ce4c8de5d8b6d2705250d984f3221470c6be970184fbec5eeaf33529da270b6c9466b12619e30a41392fe4d54fef3b7ffaaad3aff3c2d7d6 + languageName: node + linkType: hard + "process-es6@npm:^0.11.2": version: 0.11.6 resolution: "process-es6@npm:0.11.6" @@ -31755,7 +31887,7 @@ __metadata: languageName: node linkType: hard -"progress@npm:^2.0.0, progress@npm:^2.0.3": +"progress@npm:2.0.3, progress@npm:^2.0.0, progress@npm:^2.0.3": version: 2.0.3 resolution: "progress@npm:2.0.3" checksum: f67403fe7b34912148d9252cb7481266a354bd99ce82c835f79070643bb3c6583d10dbcfda4d41e04bbc1d8437e9af0fb1e1f2135727878f5308682a579429b7 @@ -31813,7 +31945,7 @@ __metadata: languageName: node linkType: hard -"prompts@npm:^2.4.0": +"prompts@npm:2.4.2, prompts@npm:^2.4.0": version: 2.4.2 resolution: "prompts@npm:2.4.2" dependencies: @@ -32564,15 +32696,6 @@ __metadata: languageName: node linkType: hard -"react-hook-form@npm:^7.34.2": - version: 7.46.2 - resolution: "react-hook-form@npm:7.46.2" - peerDependencies: - react: ^16.8.0 || ^17 || ^18 - checksum: 5ad2b478337fed3ab1a7bee79844d89a85485626fb1afd4acba54e28adb8f6663d37efefa9000b6ddb33f7de23b765bfac9a1d12e10c63e71463317a307134cf - languageName: node - linkType: hard - "react-hook-form@npm:^7.43.3": version: 7.43.3 resolution: "react-hook-form@npm:7.43.3" @@ -32594,24 +32717,6 @@ __metadata: languageName: node linkType: hard -"react-i18next@npm:^11.18.0": - version: 11.18.6 - resolution: "react-i18next@npm:11.18.6" - dependencies: - "@babel/runtime": ^7.14.5 - html-parse-stringify: ^3.0.1 - peerDependencies: - i18next: ">= 19.0.0" - react: ">= 16.8.0" - peerDependenciesMeta: - react-dom: - optional: true - react-native: - optional: true - checksum: 624c0a0313fac4e0d18560b83c99a8bd0a83abc02e5db8d01984e0643ac409d178668aa3a4720d01f7a0d9520d38598dcbff801d6f69a970bae67461de6cd852 - languageName: node - linkType: hard - "react-i18next@npm:^12.2.0": version: 12.3.1 resolution: "react-i18next@npm:12.3.1" @@ -32735,7 +32840,7 @@ __metadata: languageName: node linkType: hard -"react-live-chat-loader@npm:^2.7.3, react-live-chat-loader@npm:^2.8.1": +"react-live-chat-loader@npm:^2.8.1": version: 2.8.1 resolution: "react-live-chat-loader@npm:2.8.1" peerDependencies: @@ -33211,17 +33316,7 @@ __metadata: languageName: node linkType: hard -"read-pkg-up@npm:^1.0.1": - version: 1.0.1 - resolution: "read-pkg-up@npm:1.0.1" - dependencies: - find-up: ^1.0.0 - read-pkg: ^1.0.0 - checksum: d18399a0f46e2da32beb2f041edd0cda49d2f2cc30195a05c759ef3ed9b5e6e19ba1ad1bae2362bdec8c6a9f2c3d18f4d5e8c369e808b03d498d5781cb9122c7 - languageName: node - linkType: hard - -"read-pkg-up@npm:^7.0.1": +"read-pkg-up@npm:7.0.1, read-pkg-up@npm:^7.0.1": version: 7.0.1 resolution: "read-pkg-up@npm:7.0.1" dependencies: @@ -33232,6 +33327,16 @@ __metadata: languageName: node linkType: hard +"read-pkg-up@npm:^1.0.1": + version: 1.0.1 + resolution: "read-pkg-up@npm:1.0.1" + dependencies: + find-up: ^1.0.0 + read-pkg: ^1.0.0 + checksum: d18399a0f46e2da32beb2f041edd0cda49d2f2cc30195a05c759ef3ed9b5e6e19ba1ad1bae2362bdec8c6a9f2c3d18f4d5e8c369e808b03d498d5781cb9122c7 + languageName: node + linkType: hard + "read-pkg@npm:^1.0.0": version: 1.1.0 resolution: "read-pkg@npm:1.1.0" @@ -33305,6 +33410,17 @@ __metadata: languageName: node linkType: hard +"readable-stream@npm:^3.1.1": + version: 3.6.2 + resolution: "readable-stream@npm:3.6.2" + dependencies: + inherits: ^2.0.3 + string_decoder: ^1.1.1 + util-deprecate: ^1.0.1 + checksum: bdcbe6c22e846b6af075e32cf8f4751c2576238c5043169a1c221c92ee2878458a816a4ea33f4c67623c0b6827c8a400409bfb3cf0bf3381392d0b1dfb52ac8d + languageName: node + linkType: hard + "readable-stream@npm:^3.4.0, readable-stream@npm:^3.6.0": version: 3.6.0 resolution: "readable-stream@npm:3.6.0" @@ -33328,6 +33444,15 @@ __metadata: languageName: node linkType: hard +"readdir-glob@npm:^1.0.0": + version: 1.1.3 + resolution: "readdir-glob@npm:1.1.3" + dependencies: + minimatch: ^5.1.0 + checksum: 1dc0f7440ff5d9378b593abe9d42f34ebaf387516615e98ab410cf3a68f840abbf9ff1032d15e0a0dbffa78f9e2c46d4fafdbaac1ca435af2efe3264e3f21874 + languageName: node + linkType: hard + "readdirp@npm:^2.2.1": version: 2.2.1 resolution: "readdirp@npm:2.2.1" @@ -33817,6 +33942,13 @@ __metadata: languageName: node linkType: hard +"replace-string@npm:3.1.0": + version: 3.1.0 + resolution: "replace-string@npm:3.1.0" + checksum: 39eebbb8ec24220bdd44677708989895ccf0628388e283232a78fb2ef72097084dd603ce354e5105a819c517f01e040539419275f0deecfc6be9e9d398969f71 + languageName: node + linkType: hard + "request@npm:^2.66.0, request@npm:^2.72.0, request@npm:^2.79.0, request@npm:^2.88.0": version: 2.88.2 resolution: "request@npm:2.88.2" @@ -33921,6 +34053,19 @@ __metadata: languageName: node linkType: hard +"resolve@npm:1.22.2": + version: 1.22.2 + resolution: "resolve@npm:1.22.2" + dependencies: + is-core-module: ^2.11.0 + path-parse: ^1.0.7 + supports-preserve-symlinks-flag: ^1.0.0 + bin: + resolve: bin/resolve + checksum: 7e5df75796ebd429445d102d5824482ee7e567f0070b2b45897b29bb4f613dcbc262e0257b8aeedb3089330ccaea0d6a0464df1a77b2992cf331dcda0f4cb549 + languageName: node + linkType: hard + "resolve@npm:^1.1.7, resolve@npm:^1.14.2, resolve@npm:^1.17.0, resolve@npm:^1.19.0, resolve@npm:^1.22.1, resolve@npm:^1.3.2": version: 1.22.1 resolution: "resolve@npm:1.22.1" @@ -33947,19 +34092,6 @@ __metadata: languageName: node linkType: hard -"resolve@npm:^1.22.2": - version: 1.22.6 - resolution: "resolve@npm:1.22.6" - dependencies: - is-core-module: ^2.13.0 - path-parse: ^1.0.7 - supports-preserve-symlinks-flag: ^1.0.0 - bin: - resolve: bin/resolve - checksum: d13bf66d4e2ee30d291491f16f2fa44edd4e0cefb85d53249dd6f93e70b2b8c20ec62f01b18662e3cd40e50a7528f18c4087a99490048992a3bb954cf3201a5b - languageName: node - linkType: hard - "resolve@npm:^2.0.0-next.3": version: 2.0.0-next.3 resolution: "resolve@npm:2.0.0-next.3" @@ -33970,6 +34102,19 @@ __metadata: languageName: node linkType: hard +"resolve@patch:resolve@1.22.2#~builtin": + version: 1.22.2 + resolution: "resolve@patch:resolve@npm%3A1.22.2#~builtin::version=1.22.2&hash=c3c19d" + dependencies: + is-core-module: ^2.11.0 + path-parse: ^1.0.7 + supports-preserve-symlinks-flag: ^1.0.0 + bin: + resolve: bin/resolve + checksum: 66cc788f13b8398de18eb4abb3aed90435c84bb8935953feafcf7231ba4cd191b2c10b4a87b1e9681afc34fb138c705f91f7330ff90bfa36f457e5584076a2b8 + languageName: node + linkType: hard + "resolve@patch:resolve@^1.1.7#~builtin, resolve@patch:resolve@^1.14.2#~builtin, resolve@patch:resolve@^1.17.0#~builtin, resolve@patch:resolve@^1.19.0#~builtin, resolve@patch:resolve@^1.22.1#~builtin, resolve@patch:resolve@^1.3.2#~builtin": version: 1.22.1 resolution: "resolve@patch:resolve@npm%3A1.22.1#~builtin::version=1.22.1&hash=c3c19d" @@ -33996,19 +34141,6 @@ __metadata: languageName: node linkType: hard -"resolve@patch:resolve@^1.22.2#~builtin": - version: 1.22.6 - resolution: "resolve@patch:resolve@npm%3A1.22.6#~builtin::version=1.22.6&hash=c3c19d" - dependencies: - is-core-module: ^2.13.0 - path-parse: ^1.0.7 - supports-preserve-symlinks-flag: ^1.0.0 - bin: - resolve: bin/resolve - checksum: 9d3b3c67aefd12cecbe5f10ca4d1f51ea190891096497c43f301b086883b426466918c3a64f1bbf1788fabb52b579d58809614006c5d0b49186702b3b8fb746a - languageName: node - linkType: hard - "resolve@patch:resolve@^2.0.0-next.3#~builtin": version: 2.0.0-next.3 resolution: "resolve@patch:resolve@npm%3A2.0.0-next.3#~builtin::version=2.0.0-next.3&hash=c3c19d" @@ -34097,6 +34229,17 @@ __metadata: languageName: node linkType: hard +"rimraf@npm:3.0.2, rimraf@npm:^3.0.0, rimraf@npm:^3.0.2": + version: 3.0.2 + resolution: "rimraf@npm:3.0.2" + dependencies: + glob: ^7.1.3 + bin: + rimraf: bin.js + checksum: 87f4164e396f0171b0a3386cc1877a817f572148ee13a7e113b238e48e8a9f2f31d009a92ec38a591ff1567d9662c6b67fd8818a2dbbaed74bc26a87a2a4a9a0 + languageName: node + linkType: hard + "rimraf@npm:^2.5.4, rimraf@npm:^2.6.3": version: 2.7.1 resolution: "rimraf@npm:2.7.1" @@ -34108,17 +34251,6 @@ __metadata: languageName: node linkType: hard -"rimraf@npm:^3.0.0, rimraf@npm:^3.0.2": - version: 3.0.2 - resolution: "rimraf@npm:3.0.2" - dependencies: - glob: ^7.1.3 - bin: - rimraf: bin.js - checksum: 87f4164e396f0171b0a3386cc1877a817f572148ee13a7e113b238e48e8a9f2f31d009a92ec38a591ff1567d9662c6b67fd8818a2dbbaed74bc26a87a2a4a9a0 - languageName: node - linkType: hard - "ripemd160@npm:2.0.2, ripemd160@npm:^2.0.0, ripemd160@npm:^2.0.1": version: 2.0.2 resolution: "ripemd160@npm:2.0.2" @@ -34621,6 +34753,17 @@ __metadata: languageName: node linkType: hard +"semver@npm:^7.5.3": + version: 7.5.4 + resolution: "semver@npm:7.5.4" + dependencies: + lru-cache: ^6.0.0 + bin: + semver: bin/semver.js + checksum: 12d8ad952fa353b0995bf180cdac205a4068b759a140e5d3c608317098b3575ac2f1e09182206bf2eb26120e1c0ed8fb92c48c592f6099680de56bb071423ca3 + languageName: node + linkType: hard + "semver@npm:~2.3.1": version: 2.3.2 resolution: "semver@npm:2.3.2" @@ -35729,7 +35872,7 @@ __metadata: languageName: node linkType: hard -"string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.0.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.2, string-width@npm:^4.2.3": +"string-width@npm:4.2.3, string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.0.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.2, string-width@npm:^4.2.3": version: 4.2.3 resolution: "string-width@npm:4.2.3" dependencies: @@ -35973,6 +36116,15 @@ __metadata: languageName: node linkType: hard +"strip-indent@npm:3.0.0, strip-indent@npm:^3.0.0": + version: 3.0.0 + resolution: "strip-indent@npm:3.0.0" + dependencies: + min-indent: ^1.0.0 + checksum: 18f045d57d9d0d90cd16f72b2313d6364fd2cb4bf85b9f593523ad431c8720011a4d5f08b6591c9d580f446e78855c5334a30fb91aa1560f5d9f95ed1b4a0530 + languageName: node + linkType: hard + "strip-indent@npm:^1.0.1": version: 1.0.1 resolution: "strip-indent@npm:1.0.1" @@ -35984,15 +36136,6 @@ __metadata: languageName: node linkType: hard -"strip-indent@npm:^3.0.0": - version: 3.0.0 - resolution: "strip-indent@npm:3.0.0" - dependencies: - min-indent: ^1.0.0 - checksum: 18f045d57d9d0d90cd16f72b2313d6364fd2cb4bf85b9f593523ad431c8720011a4d5f08b6591c9d580f446e78855c5334a30fb91aa1560f5d9f95ed1b4a0530 - languageName: node - linkType: hard - "strip-json-comments@npm:^3.1.0, strip-json-comments@npm:^3.1.1": version: 3.1.1 resolution: "strip-json-comments@npm:3.1.1" @@ -36116,24 +36259,6 @@ __metadata: languageName: node linkType: hard -"sucrase@npm:^3.32.0": - version: 3.34.0 - resolution: "sucrase@npm:3.34.0" - dependencies: - "@jridgewell/gen-mapping": ^0.3.2 - commander: ^4.0.0 - glob: 7.1.6 - lines-and-columns: ^1.1.6 - mz: ^2.7.0 - pirates: ^4.0.1 - ts-interface-checker: ^0.1.9 - bin: - sucrase: bin/sucrase - sucrase-node: bin/sucrase-node - checksum: 61860063bdf6103413698e13247a3074d25843e91170825a9752e4af7668ffadd331b6e99e92fc32ee5b3c484ee134936f926fa9039d5711fafff29d017a2110 - languageName: node - linkType: hard - "superagent@npm:^5.1.1": version: 5.3.1 resolution: "superagent@npm:5.3.1" @@ -36212,6 +36337,16 @@ __metadata: languageName: node linkType: hard +"supports-hyperlinks@npm:^2.0.0": + version: 2.3.0 + resolution: "supports-hyperlinks@npm:2.3.0" + dependencies: + has-flag: ^4.0.0 + supports-color: ^7.0.0 + checksum: 9ee0de3c8ce919d453511b2b1588a8205bd429d98af94a01df87411391010fe22ca463f268c84b2ce2abad019dfff8452aa02806eeb5c905a8d7ad5c4f4c52b8 + languageName: node + linkType: hard + "supports-preserve-symlinks-flag@npm:^1.0.0": version: 1.0.0 resolution: "supports-preserve-symlinks-flag@npm:1.0.0" @@ -36485,39 +36620,6 @@ __metadata: languageName: node linkType: hard -"tailwindcss@npm:^3.2.1": - version: 3.3.3 - resolution: "tailwindcss@npm:3.3.3" - dependencies: - "@alloc/quick-lru": ^5.2.0 - arg: ^5.0.2 - chokidar: ^3.5.3 - didyoumean: ^1.2.2 - dlv: ^1.1.3 - fast-glob: ^3.2.12 - glob-parent: ^6.0.2 - is-glob: ^4.0.3 - jiti: ^1.18.2 - lilconfig: ^2.1.0 - micromatch: ^4.0.5 - normalize-path: ^3.0.0 - object-hash: ^3.0.0 - picocolors: ^1.0.0 - postcss: ^8.4.23 - postcss-import: ^15.1.0 - postcss-js: ^4.0.1 - postcss-load-config: ^4.0.1 - postcss-nested: ^6.0.1 - postcss-selector-parser: ^6.0.11 - resolve: ^1.22.2 - sucrase: ^3.32.0 - bin: - tailwind: lib/cli.js - tailwindcss: lib/cli.js - checksum: 0195c7a3ebb0de5e391d2a883d777c78a4749f0c532d204ee8aea9129f2ed8e701d8c0c276aa5f7338d07176a3c2a7682c1d0ab9c8a6c2abe6d9325c2954eb50 - languageName: node - linkType: hard - "tailwindcss@npm:^3.3.1": version: 3.3.1 resolution: "tailwindcss@npm:3.3.1" @@ -36569,6 +36671,19 @@ __metadata: languageName: node linkType: hard +"tar-stream@npm:^2.2.0": + version: 2.2.0 + resolution: "tar-stream@npm:2.2.0" + dependencies: + bl: ^4.0.3 + end-of-stream: ^1.4.1 + fs-constants: ^1.0.0 + inherits: ^2.0.3 + readable-stream: ^3.1.1 + checksum: 699831a8b97666ef50021c767f84924cfee21c142c2eb0e79c63254e140e6408d6d55a065a2992548e72b06de39237ef2b802b99e3ece93ca3904a37622a66f3 + languageName: node + linkType: hard + "tar@npm:^4.0.2": version: 4.4.19 resolution: "tar@npm:4.4.19" @@ -36655,6 +36770,26 @@ __metadata: languageName: node linkType: hard +"temp-dir@npm:2.0.0, temp-dir@npm:^2.0.0": + version: 2.0.0 + resolution: "temp-dir@npm:2.0.0" + checksum: cc4f0404bf8d6ae1a166e0e64f3f409b423f4d1274d8c02814a59a5529f07db6cd070a749664141b992b2c1af337fa9bb451a460a43bb9bcddc49f235d3115aa + languageName: node + linkType: hard + +"tempy@npm:1.0.1": + version: 1.0.1 + resolution: "tempy@npm:1.0.1" + dependencies: + del: ^6.0.0 + is-stream: ^2.0.0 + temp-dir: ^2.0.0 + type-fest: ^0.16.0 + unique-string: ^2.0.0 + checksum: e77ca4440af18e42dc64d8903b7ed0be673455b76680ff94a7d7c6ee7c16f7604bdcdee3c39436342b1082c23eda010dbe48f6094e836e0bd53c8b1aa63e5b95 + languageName: node + linkType: hard + "term-size@npm:^2.1.0": version: 2.2.1 resolution: "term-size@npm:2.2.1" @@ -36669,6 +36804,16 @@ __metadata: languageName: node linkType: hard +"terminal-link@npm:2.1.1": + version: 2.1.1 + resolution: "terminal-link@npm:2.1.1" + dependencies: + ansi-escapes: ^4.2.1 + supports-hyperlinks: ^2.0.0 + checksum: ce3d2cd3a438c4a9453947aa664581519173ea40e77e2534d08c088ee6dda449eabdbe0a76d2a516b8b73c33262fedd10d5270ccf7576ae316e3db170ce6562f + languageName: node + linkType: hard + "terser-webpack-plugin@npm:^1.4.3": version: 1.4.5 resolution: "terser-webpack-plugin@npm:1.4.5" @@ -36988,6 +37133,15 @@ __metadata: languageName: node linkType: hard +"tmp@npm:0.2.1, tmp@npm:^0.2.0": + version: 0.2.1 + resolution: "tmp@npm:0.2.1" + dependencies: + rimraf: ^3.0.0 + checksum: 8b1214654182575124498c87ca986ac53dc76ff36e8f0e0b67139a8d221eaecfdec108c0e6ec54d76f49f1f72ab9325500b246f562b926f85bcdfca8bf35df9e + languageName: node + linkType: hard + "tmp@npm:^0.0.33": version: 0.0.33 resolution: "tmp@npm:0.0.33" @@ -36997,15 +37151,6 @@ __metadata: languageName: node linkType: hard -"tmp@npm:^0.2.0": - version: 0.2.1 - resolution: "tmp@npm:0.2.1" - dependencies: - rimraf: ^3.0.0 - checksum: 8b1214654182575124498c87ca986ac53dc76ff36e8f0e0b67139a8d221eaecfdec108c0e6ec54d76f49f1f72ab9325500b246f562b926f85bcdfca8bf35df9e - languageName: node - linkType: hard - "tmpl@npm:1.0.5": version: 1.0.5 resolution: "tmpl@npm:1.0.5" @@ -37294,6 +37439,13 @@ __metadata: languageName: node linkType: hard +"ts-pattern@npm:4.3.0": + version: 4.3.0 + resolution: "ts-pattern@npm:4.3.0" + checksum: f5167f6f721212c1e22f8590bb1fc21e27fd591d6fc5af0e467778f8e5b2dc1ae490cf5c633c83a3021a61283a671a53faf250b081c17c0f025e73eb775e5fe3 + languageName: node + linkType: hard + "ts-pnp@npm:^1.1.6": version: 1.2.0 resolution: "ts-pnp@npm:1.2.0" @@ -37661,6 +37813,13 @@ __metadata: languageName: node linkType: hard +"type-fest@npm:^0.16.0": + version: 0.16.0 + resolution: "type-fest@npm:0.16.0" + checksum: 1a4102c06dc109db00418c753062e206cab65befd469d000ece4452ee649bf2a9cf57686d96fb42326bc9d918d9a194d4452897b486dcc41989e5c99e4e87094 + languageName: node + linkType: hard + "type-fest@npm:^0.18.0": version: 0.18.1 resolution: "type-fest@npm:0.18.1" @@ -37696,7 +37855,7 @@ __metadata: languageName: node linkType: hard -"type-fest@npm:^0.8.1": +"type-fest@npm:^0.8.0, type-fest@npm:^0.8.1": version: 0.8.1 resolution: "type-fest@npm:0.8.1" checksum: d61c4b2eba24009033ae4500d7d818a94fd6d1b481a8111612ee141400d5f1db46f199c014766b9fa9b31a6a7374d96fc748c6d688a78a3ce5a33123839becb7 @@ -37846,16 +38005,6 @@ __metadata: languageName: node linkType: hard -"typescript@npm:^4.7.4": - version: 4.9.5 - resolution: "typescript@npm:4.9.5" - bin: - tsc: bin/tsc - tsserver: bin/tsserver - checksum: ee000bc26848147ad423b581bd250075662a354d84f0e06eb76d3b892328d8d4440b7487b5a83e851b12b255f55d71835b008a66cbf8f255a11e4400159237db - languageName: node - linkType: hard - "typescript@npm:^4.9.4": version: 4.9.4 resolution: "typescript@npm:4.9.4" @@ -37866,16 +38015,6 @@ __metadata: languageName: node linkType: hard -"typescript@patch:typescript@^4.7.4#~builtin": - version: 4.9.5 - resolution: "typescript@patch:typescript@npm%3A4.9.5#~builtin::version=4.9.5&hash=23ec76" - bin: - tsc: bin/tsc - tsserver: bin/tsserver - checksum: ab417a2f398380c90a6cf5a5f74badd17866adf57f1165617d6a551f059c3ba0a3e4da0d147b3ac5681db9ac76a303c5876394b13b3de75fdd5b1eaa06181c9d - languageName: node - linkType: hard - "typescript@patch:typescript@^4.9.4#~builtin": version: 4.9.4 resolution: "typescript@patch:typescript@npm%3A4.9.4#~builtin::version=4.9.4&hash=23ec76" @@ -38112,6 +38251,15 @@ __metadata: languageName: node linkType: hard +"unique-string@npm:^2.0.0": + version: 2.0.0 + resolution: "unique-string@npm:2.0.0" + dependencies: + crypto-random-string: ^2.0.0 + checksum: ef68f639136bcfe040cf7e3cd7a8dff076a665288122855148a6f7134092e6ed33bf83a7f3a9185e46c98dddc445a0da6ac25612afa1a7c38b8b654d6c02498e + languageName: node + linkType: hard + "unist-builder@npm:2.0.3, unist-builder@npm:^2.0.0": version: 2.0.3 resolution: "unist-builder@npm:2.0.3" @@ -38703,6 +38851,15 @@ __metadata: languageName: node linkType: hard +"uuid@npm:9.0.0, uuid@npm:^9.0.0": + version: 9.0.0 + resolution: "uuid@npm:9.0.0" + bin: + uuid: dist/bin/uuid + checksum: 8dd2c83c43ddc7e1c71e36b60aea40030a6505139af6bee0f382ebcd1a56f6cd3028f7f06ffb07f8cf6ced320b76aea275284b224b002b289f89fe89c389b028 + languageName: node + linkType: hard + "uuid@npm:^3.3.2": version: 3.4.0 resolution: "uuid@npm:3.4.0" @@ -38721,15 +38878,6 @@ __metadata: languageName: node linkType: hard -"uuid@npm:^9.0.0": - version: 9.0.0 - resolution: "uuid@npm:9.0.0" - bin: - uuid: dist/bin/uuid - checksum: 8dd2c83c43ddc7e1c71e36b60aea40030a6505139af6bee0f382ebcd1a56f6cd3028f7f06ffb07f8cf6ced320b76aea275284b224b002b289f89fe89c389b028 - languageName: node - linkType: hard - "uvu@npm:^0.5.0": version: 0.5.6 resolution: "uvu@npm:0.5.6" @@ -40367,13 +40515,6 @@ __metadata: languageName: node linkType: hard -"yaml@npm:^2.1.1, yaml@npm:^2.3.1": - version: 2.3.2 - resolution: "yaml@npm:2.3.2" - checksum: acd80cc24df12c808c6dec8a0176d404ef9e6f08ad8786f746ecc9d8974968c53c6e8a67fdfabcc5f99f3dc59b6bb0994b95646ff03d18e9b1dcd59eccc02146 - languageName: node - linkType: hard - "yaml@npm:^2.2.1": version: 2.3.1 resolution: "yaml@npm:2.3.1" @@ -40381,6 +40522,13 @@ __metadata: languageName: node linkType: hard +"yaml@npm:^2.3.1": + version: 2.3.2 + resolution: "yaml@npm:2.3.2" + checksum: acd80cc24df12c808c6dec8a0176d404ef9e6f08ad8786f746ecc9d8974968c53c6e8a67fdfabcc5f99f3dc59b6bb0994b95646ff03d18e9b1dcd59eccc02146 + languageName: node + linkType: hard + "yargs-parser@npm:^18.1.2, yargs-parser@npm:^18.1.3": version: 18.1.3 resolution: "yargs-parser@npm:18.1.3" @@ -40567,6 +40715,17 @@ __metadata: languageName: node linkType: hard +"zip-stream@npm:^4.1.0": + version: 4.1.1 + resolution: "zip-stream@npm:4.1.1" + dependencies: + archiver-utils: ^3.0.4 + compress-commons: ^4.1.2 + readable-stream: ^3.6.0 + checksum: 33bd5ee7017656c2ad728b5d4ba510e15bd65ce1ec180c5bbdc7a5f063256353ec482e6a2bc74de7515219d8494147924b9aae16e63fdaaf37cdf7d1ee8df125 + languageName: node + linkType: hard + "zod-prisma@npm:^0.5.4": version: 0.5.4 resolution: "zod-prisma@npm:0.5.4" @@ -40603,7 +40762,7 @@ __metadata: languageName: node linkType: hard -"zod@npm:^3.20.2, zod@npm:^3.21.4, zod@npm:^3.22.2": +"zod@npm:^3.21.4, zod@npm:^3.22.2": version: 3.22.2 resolution: "zod@npm:3.22.2" checksum: 231e2180c8eabb56e88680d80baff5cf6cbe6d64df3c44c50ebe52f73081ecd0229b1c7215b9552537f537a36d9e36afac2737ddd86dc14e3519bdbc777e82b9