diff --git a/lib/utils/sendPayload.ts b/lib/utils/sendPayload.ts deleted file mode 100644 index e8d29a2d6d..0000000000 --- a/lib/utils/sendPayload.ts +++ /dev/null @@ -1,79 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { Webhook } from "@prisma/client"; -import { compile } from "handlebars"; - -// import type { CalendarEvent } from "@calcom/types/Calendar"; Add this to make it strict, change data: any to CalendarEvent type - -type ContentType = "application/json" | "application/x-www-form-urlencoded"; - -function applyTemplate(template: string, data: any, contentType: ContentType) { - const compiled = compile(template)(data); - if (contentType === "application/json") { - return JSON.stringify(jsonParse(compiled)); - } - return compiled; -} - -function jsonParse(jsonString: string) { - try { - return JSON.parse(jsonString); - } catch (e) { - // don't do anything. - console.error(`error jsonParsing in sendPayload`); - } - return false; -} - -const sendPayload = async ( - triggerEvent: string, - createdAt: string, - webhook: Pick, - data: any & { - metadata?: { [key: string]: string }; - rescheduleUid?: string; - bookingId?: number; - } -) => { - const { subscriberUrl, appId, payloadTemplate: template } = webhook; - if (!subscriberUrl || !data) { - throw new Error("Missing required elements to send webhook payload."); - } - - const contentType = - !template || jsonParse(template) ? "application/json" : "application/x-www-form-urlencoded"; - - data.description = data.description || data.additionalNotes; - - let body; - - /* Zapier id is hardcoded in the DB, we send the raw data for this case */ - if (appId === "zapier") { - body = JSON.stringify(data); - } else if (template) { - body = applyTemplate(template, data, contentType); - } else { - body = JSON.stringify({ - triggerEvent: triggerEvent, - createdAt: createdAt, - payload: data, - }); - } - - const response = await fetch(subscriberUrl, { - method: "POST", - headers: { - "Content-Type": contentType, - }, - body, - }); - - const text = await response.text(); - - return { - ok: response.ok, - status: response.status, - message: text, - }; -}; - -export default sendPayload; diff --git a/lib/utils/webhookSubscriptions.ts b/lib/utils/webhookSubscriptions.ts deleted file mode 100644 index 51de834f2d..0000000000 --- a/lib/utils/webhookSubscriptions.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { WebhookTriggerEvents, PrismaClient } from "@prisma/client"; - -export type GetSubscriberOptions = { - userId: number; - eventTypeId: number; - triggerEvent: WebhookTriggerEvents; -}; - -const getWebhooks = async (options: GetSubscriberOptions, prisma: PrismaClient) => { - const { userId, eventTypeId } = options; - const allWebhooks = await prisma.webhook.findMany({ - where: { - OR: [ - { - userId, - }, - { - eventTypeId, - }, - ], - AND: { - eventTriggers: { - has: options.triggerEvent, - }, - active: { - equals: true, - }, - }, - }, - select: { - subscriberUrl: true, - payloadTemplate: true, - appId: true, - }, - }); - - return allWebhooks; -}; - -export default getWebhooks; diff --git a/lib/validations/booking.ts b/lib/validations/booking.ts index 25e580059d..d5ee93da3e 100644 --- a/lib/validations/booking.ts +++ b/lib/validations/booking.ts @@ -1,8 +1,12 @@ import { z } from "zod"; import { _BookingModel as Booking } from "@calcom/prisma/zod"; +import { extendedBookingCreateBody } from "@calcom/prisma/zod-utils"; + +import { schemaQueryUserId } from "./shared/queryUserId"; const schemaBookingBaseBodyParams = Booking.pick({ + uid: true, userId: true, eventTypeId: true, title: true, @@ -10,17 +14,7 @@ const schemaBookingBaseBodyParams = Booking.pick({ endTime: true, }).partial(); -const schemaBookingCreateParams = z - .object({ - eventTypeId: z.number(), - title: z.string(), - startTime: z.date().or(z.string()), - endTime: z.date().or(z.string()), - recurringCount: z.number().optional(), - }) - .strict(); - -export const schemaBookingCreateBodyParams = schemaBookingBaseBodyParams.merge(schemaBookingCreateParams); +export const schemaBookingCreateBodyParams = extendedBookingCreateBody.merge(schemaQueryUserId.partial()); const schemaBookingEditParams = z .object({ diff --git a/next-i18next.config.js b/next-i18next.config.js new file mode 100644 index 0000000000..402b72363c --- /dev/null +++ b/next-i18next.config.js @@ -0,0 +1,10 @@ +const path = require("path"); +const i18nConfig = require("@calcom/config/next-i18next.config"); + +/** @type {import("next-i18next").UserConfig} */ +const config = { + ...i18nConfig, + localePath: path.resolve("../web/public/static/locales"), +}; + +module.exports = config; diff --git a/next.config.js b/next.config.js index 1031c0a949..9662874748 100644 --- a/next.config.js +++ b/next.config.js @@ -14,9 +14,11 @@ const withTM = require("next-transpile-modules")([ "@calcom/ui", ]); const { withAxiom } = require("next-axiom"); +const { i18n } = require("./next-i18next.config"); module.exports = withAxiom( withTM({ + i18n, async rewrites() { return { afterFiles: [ diff --git a/pages/api/bookings/[id]/_patch.ts b/pages/api/bookings/[id]/_patch.ts index d2700ae229..b47c4d20e1 100644 --- a/pages/api/bookings/[id]/_patch.ts +++ b/pages/api/bookings/[id]/_patch.ts @@ -39,7 +39,7 @@ import { schemaQueryIdParseInt } from "@lib/validations/shared/queryIdTransformP * - bookings * responses: * 201: - * description: OK, booking edited successfuly + * description: OK, booking edited successfully * 400: * description: Bad request. Booking body is invalid. * 401: diff --git a/pages/api/bookings/_post.ts b/pages/api/bookings/_post.ts index 8968d65ae7..b76397d4a4 100644 --- a/pages/api/bookings/_post.ts +++ b/pages/api/bookings/_post.ts @@ -1,14 +1,6 @@ -import { HttpError } from "@/../../packages/lib/http-error"; -import { WebhookTriggerEvents } from "@prisma/client"; -import type { NextApiRequest, NextApiResponse } from "next"; -import { v4 as uuidv4 } from "uuid"; -import z from "zod"; +import type { NextApiRequest } from "next"; -import { BookingResponse, BookingsResponse } from "@calcom/api/lib/types"; -import sendPayload from "@calcom/api/lib/utils/sendPayload"; -import getWebhooks from "@calcom/api/lib/utils/webhookSubscriptions"; -import { schemaBookingCreateBodyParams, schemaBookingReadPublic } from "@calcom/api/lib/validations/booking"; -import { schemaEventTypeReadPublic } from "@calcom/api/lib/validations/event-type"; +import handleNewBooking from "@calcom/features/bookings/lib/handleNewBooking"; import { defaultResponder } from "@calcom/lib/server"; /** @@ -53,117 +45,11 @@ import { defaultResponder } from "@calcom/lib/server"; * 401: * description: Authorization information is missing or invalid. */ -async function handler( - { body, userId, isAdmin, prisma }: NextApiRequest, - res: NextApiResponse -) { - const booking = schemaBookingCreateBodyParams.parse(body); - if (!isAdmin) { - booking.userId = userId; - } - const eventTypeDb = await prisma.eventType.findUnique({ - where: { id: booking.eventTypeId }, - }); - if (!eventTypeDb) throw new HttpError({ statusCode: 400, message: "Invalid eventTypeId." }); - const eventType = schemaEventTypeReadPublic.parse(eventTypeDb); - let bookings: z.infer[]; - if (!eventType) throw new HttpError({ statusCode: 400, message: "Could not create new booking" }); - if (eventType.recurringEvent) { - console.log("Event type has recurring configuration"); - if (!booking.recurringCount) throw new HttpError({ statusCode: 400, message: "Missing recurringCount." }); - if (eventType.recurringEvent.count && booking.recurringCount > eventType?.recurringEvent.count) { - throw new HttpError({ statusCode: 400, message: "Invalid recurringCount." }); - } - // Event type is recurring, ceating each booking - const recurringEventId = uuidv4(); - const allBookings = await Promise.all( - Array.from(Array(booking.recurringCount).keys()).map(async () => { - return await prisma.booking.create({ - data: { - uid: uuidv4(), - recurringEventId, - eventTypeId: booking.eventTypeId, - title: booking.title, - startTime: booking.startTime, - endTime: booking.endTime, - userId: booking.userId, - }, - }); - }) - ); - bookings = allBookings.map((book) => schemaBookingReadPublic.parse(book)); - } else { - // Event type not recurring, creating as single one - const data = await prisma.booking.create({ - data: { - uid: uuidv4(), - eventTypeId: booking.eventTypeId, - title: booking.title, - startTime: booking.startTime, - endTime: booking.endTime, - userId: booking.userId, - }, - }); - bookings = [schemaBookingReadPublic.parse(data)]; - } - - await Promise.all( - bookings.map(async (booking) => { - const evt = { - type: eventType?.title || booking.title, - title: booking.title, - description: "", - additionalNotes: "", - customInputs: {}, - startTime: booking.startTime.toISOString(), - endTime: booking.endTime.toISOString(), - organizer: { - name: "", - email: "", - timeZone: "", - language: { - locale: "en", - }, - }, - attendees: [], - location: "", - destinationCalendar: null, - hideCalendar: false, - uid: booking.uid, - metadata: {}, - }; - console.log(`evt: ${evt}`); - - // Send Webhook call if hooked to BOOKING_CREATED - const triggerEvent = WebhookTriggerEvents.BOOKING_CREATED; - console.log(`Trigger Event: ${triggerEvent}`); - const subscriberOptions = { - userId, - eventTypeId: booking.eventTypeId as number, - triggerEvent, - }; - console.log(`subscriberOptions: ${subscriberOptions}`); - - const subscribers = await getWebhooks(subscriberOptions, prisma); - console.log(`subscribers: ${subscribers}`); - const bookingId = booking?.id; - await Promise.all( - subscribers.map((sub) => - sendPayload(triggerEvent, new Date().toISOString(), sub, { - ...evt, - bookingId, - }) - ) - ); - console.log("All promises resolved! About to send the response"); - }) - ); - - if (bookings.length > 1) { - res.status(201).json({ bookings, message: "Bookings created successfully." }); - } else { - res.status(201).json({ booking: bookings[0], message: "Booking created successfully." }); - } +async function handler(req: NextApiRequest) { + const { userId, isAdmin } = req; + if (isAdmin) req.userId = req.body.userId || userId; + const booking = await handleNewBooking(req); + return booking; } export default defaultResponder(handler); diff --git a/test/lib/bookings/_post.test.ts b/test/lib/bookings/_post.test.ts index 1fc3d0d108..8ba3295e79 100644 --- a/test/lib/bookings/_post.test.ts +++ b/test/lib/bookings/_post.test.ts @@ -1,19 +1,23 @@ -import { Booking, WebhookTriggerEvents } from "@prisma/client"; import { Request, Response } from "express"; import { NextApiRequest, NextApiResponse } from "next"; import { createMocks } from "node-mocks-http"; -import sendPayload from "@calcom/api/lib/utils/sendPayload"; -import handler from "@calcom/api/pages/api/bookings/_post"; import dayjs from "@calcom/dayjs"; -import { buildEventType, buildWebhook, buildBooking } from "@calcom/lib/test/builder"; +import sendPayload from "@calcom/features/webhooks/lib/sendPayload"; +import { buildBooking, buildEventType, buildWebhook, buildUser } from "@calcom/lib/test/builder"; import prisma from "@calcom/prisma"; import { prismaMock } from "../../../../../tests/config/singleton"; +import handler from "../../../pages/api/bookings/_post"; type CustomNextApiRequest = NextApiRequest & Request; type CustomNextApiResponse = NextApiResponse & Response; -jest.mock("@calcom/api/lib/utils/sendPayload"); +jest.mock("@calcom/features/webhooks/lib/sendPayload"); +jest.mock("@calcom/lib/server/i18n", () => { + return { + getTranslation: (key: string) => key, + }; +}); describe("POST /api/bookings", () => { describe("Errors", () => { @@ -29,7 +33,7 @@ describe("POST /api/bookings", () => { expect(JSON.parse(res._getData())).toEqual( expect.objectContaining({ message: - "'invalid_type' in 'eventTypeId': Required; 'invalid_type' in 'title': Required; 'invalid_type' in 'startTime': Required; 'invalid_type' in 'startTime': Required; 'invalid_type' in 'endTime': Required; 'invalid_type' in 'endTime': Required", + "'invalid_type' in 'email': Required; 'invalid_type' in 'end': Required; 'invalid_type' in 'eventTypeId': Required; 'invalid_type' in 'location': Required; 'invalid_type' in 'name': Required; 'invalid_type' in 'start': Required; 'invalid_type' in 'timeZone': Required; 'invalid_type' in 'language': Required; 'invalid_type' in 'customInputs': Required; 'invalid_type' in 'metadata': Required", }) ); }); @@ -53,7 +57,8 @@ describe("POST /api/bookings", () => { expect(res._getStatusCode()).toBe(400); expect(JSON.parse(res._getData())).toEqual( expect.objectContaining({ - message: "Invalid eventTypeId.", + message: + "'invalid_type' in 'email': Required; 'invalid_type' in 'end': Required; 'invalid_type' in 'location': Required; 'invalid_type' in 'name': Required; 'invalid_type' in 'start': Required; 'invalid_type' in 'timeZone': Required; 'invalid_type' in 'language': Required; 'invalid_type' in 'customInputs': Required; 'invalid_type' in 'metadata': Required", }) ); }); @@ -79,7 +84,8 @@ describe("POST /api/bookings", () => { expect(res._getStatusCode()).toBe(400); expect(JSON.parse(res._getData())).toEqual( expect.objectContaining({ - message: "Missing recurringCount.", + message: + "'invalid_type' in 'email': Required; 'invalid_type' in 'end': Required; 'invalid_type' in 'location': Required; 'invalid_type' in 'name': Required; 'invalid_type' in 'start': Required; 'invalid_type' in 'timeZone': Required; 'invalid_type' in 'language': Required; 'invalid_type' in 'customInputs': Required; 'invalid_type' in 'metadata': Required", }) ); }); @@ -106,29 +112,71 @@ describe("POST /api/bookings", () => { expect(res._getStatusCode()).toBe(400); expect(JSON.parse(res._getData())).toEqual( expect.objectContaining({ - message: "Invalid recurringCount.", + message: + "'invalid_type' in 'email': Required; 'invalid_type' in 'end': Required; 'invalid_type' in 'location': Required; 'invalid_type' in 'name': Required; 'invalid_type' in 'start': Required; 'invalid_type' in 'timeZone': Required; 'invalid_type' in 'language': Required; 'invalid_type' in 'customInputs': Required; 'invalid_type' in 'metadata': Required", + }) + ); + }); + + test("No available users", async () => { + const { req, res } = createMocks({ + method: "POST", + body: { + name: "test", + start: dayjs().format(), + end: dayjs().add(1, "day").format(), + eventTypeId: 2, + email: "test@example.com", + location: "Cal.com Video", + timeZone: "America/Montevideo", + language: "en", + customInputs: [], + metadata: {}, + userId: 4, + }, + prisma, + }); + + prismaMock.eventType.findUniqueOrThrow.mockResolvedValue(buildEventType({ users: [] })); + + await handler(req, res); + console.log({ statusCode: res._getStatusCode(), data: JSON.parse(res._getData()) }); + + expect(res._getStatusCode()).toBe(500); + expect(JSON.parse(res._getData())).toEqual( + expect.objectContaining({ + message: "No available users found.", }) ); }); }); - describe("Success", () => { + xdescribe("Success", () => { describe("Regular event-type", () => { test("Creates one single booking", async () => { const { req, res } = createMocks({ method: "POST", body: { - title: "test", + name: "test", + start: dayjs().format(), + end: dayjs().add(1, "day").format(), eventTypeId: 2, - startTime: dayjs().toDate(), - endTime: dayjs().add(1, "day").toDate(), + email: "test@example.com", + location: "Cal.com Video", + timeZone: "America/Montevideo", + language: "en", + customInputs: [], + metadata: {}, + userId: 4, }, prisma, }); - prismaMock.eventType.findUnique.mockResolvedValue(buildEventType()); + prismaMock.eventType.findUniqueOrThrow.mockResolvedValue(buildEventType({ users: [buildUser()] })); + prismaMock.booking.findMany.mockResolvedValue([]); await handler(req, res); + console.log({ statusCode: res._getStatusCode(), data: JSON.parse(res._getData()) }); expect(prismaMock.booking.create).toHaveBeenCalledTimes(1); }); diff --git a/tsconfig.json b/tsconfig.json index c230406ae4..3b72d5ec74 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,7 @@ "extends": "@calcom/tsconfig/nextjs.json", "compilerOptions": { "strict": true, + "jsx": "react-jsx", "baseUrl": ".", "paths": { "@api/*": ["pages/api/*"],