diff --git a/apps/web/lib/webhooks/sendPayload.tsx b/apps/web/lib/webhooks/sendPayload.tsx index 5eb1d019cf..be765d53ed 100644 --- a/apps/web/lib/webhooks/sendPayload.tsx +++ b/apps/web/lib/webhooks/sendPayload.tsx @@ -2,10 +2,55 @@ import { Webhook } from "@prisma/client"; import { createHmac } from "crypto"; import { compile } from "handlebars"; +import { getHumanReadableLocationValue } from "@calcom/app-store/locations"; import type { CalendarEvent } from "@calcom/types/Calendar"; +import { getTranslation } from "@server/lib/i18n"; + type ContentType = "application/json" | "application/x-www-form-urlencoded"; +export type EventTypeInfo = { + eventTitle?: string | null; + eventDescription?: string | null; + requiresConfirmation?: boolean | null; + price?: number | null; + currency?: string | null; + length?: number | null; +}; + +function getZapierPayload(data: CalendarEvent & EventTypeInfo & { status?: string }): string { + const attendees = data.attendees.map((attendee) => { + return { + name: attendee.name, + email: attendee.email, + timeZone: attendee.timeZone, + }; + }); + + const t = data.organizer.language.translate; + const location = getHumanReadableLocationValue(data.location || "", t); + + const body = { + title: data.title, + description: data.description, + customInputs: data.customInputs, + startTime: data.startTime, + endTime: data.endTime, + location: location, + status: data.status, + eventType: { + title: data.eventTitle, + description: data.eventDescription, + requiresConfirmation: data.requiresConfirmation, + price: data.price, + currency: data.currency, + length: data.length, + }, + attendees: attendees, + }; + return JSON.stringify(body); +} + function applyTemplate(template: string, data: CalendarEvent, contentType: ContentType) { const compiled = compile(template)(data); if (contentType === "application/json") { @@ -28,11 +73,13 @@ const sendPayload = async ( triggerEvent: string, createdAt: string, webhook: Pick, - data: CalendarEvent & { - metadata?: { [key: string]: string }; - rescheduleUid?: string; - bookingId?: number; - } + data: CalendarEvent & + EventTypeInfo & { + metadata?: { [key: string]: string }; + rescheduleUid?: string; + bookingId?: number; + status?: string; + } ) => { const { appId, payloadTemplate: template } = webhook; @@ -45,7 +92,7 @@ const sendPayload = async ( /* Zapier id is hardcoded in the DB, we send the raw data for this case */ if (appId === "zapier") { - body = JSON.stringify(data); + body = getZapierPayload(data); } else if (template) { body = applyTemplate(template, data, contentType); } else { diff --git a/apps/web/pages/api/book/event.ts b/apps/web/pages/api/book/event.ts index aa8341f090..d9a5fe0007 100644 --- a/apps/web/pages/api/book/event.ts +++ b/apps/web/pages/api/book/event.ts @@ -37,7 +37,7 @@ import type { EventResult, PartialReference } from "@calcom/types/EventManager"; import { getSession } from "@lib/auth"; import { HttpError } from "@lib/core/http/error"; -import sendPayload from "@lib/webhooks/sendPayload"; +import sendPayload, { EventTypeInfo } from "@lib/webhooks/sendPayload"; import { getTranslation } from "@server/lib/i18n"; @@ -845,13 +845,25 @@ async function handler(req: NextApiRequest) { metadata: reqBody.metadata, }); const bookingId = booking?.id; + + const eventTypeInfo: EventTypeInfo = { + eventTitle: eventType.title, + eventDescription: eventType.description, + requiresConfirmation: eventType.requiresConfirmation || null, + price: eventType.price, + currency: eventType.currency, + length: eventType.length, + }; + const promises = subscribers.map((sub) => sendPayload(sub.secret, eventTrigger, new Date().toISOString(), sub, { ...evt, + ...eventTypeInfo, bookingId, rescheduleUid, metadata: reqBody.metadata, eventTypeId, + status: eventType.requiresConfirmation ? "PENDING" : "ACCEPTED", }).catch((e) => { console.error(`Error executing webhook for event: ${eventTrigger}, URL: ${sub.subscriberUrl}`, e); }) diff --git a/apps/web/pages/api/cancel.ts b/apps/web/pages/api/cancel.ts index 275b266272..b615e1b713 100644 --- a/apps/web/pages/api/cancel.ts +++ b/apps/web/pages/api/cancel.ts @@ -27,7 +27,7 @@ import prisma, { bookingMinimalSelect } from "@calcom/prisma"; import type { CalendarEvent } from "@calcom/types/Calendar"; import { getSession } from "@lib/auth"; -import sendPayload from "@lib/webhooks/sendPayload"; +import sendPayload, { EventTypeInfo } from "@lib/webhooks/sendPayload"; import getWebhooks from "@lib/webhooks/subscriptions"; import { getTranslation } from "@server/lib/i18n"; @@ -75,6 +75,11 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { select: { recurringEvent: true, title: true, + description: true, + requiresConfirmation: true, + price: true, + currency: true, + length: true, workflows: { include: { workflow: { @@ -165,9 +170,23 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { eventTypeId: (bookingToDelete.eventTypeId as number) || 0, triggerEvent: eventTrigger, }; + + const eventTypeInfo: EventTypeInfo = { + eventTitle: bookingToDelete?.eventType?.title || null, + eventDescription: bookingToDelete?.eventType?.description || null, + requiresConfirmation: bookingToDelete?.eventType?.requiresConfirmation || null, + price: bookingToDelete?.eventType?.price || null, + currency: bookingToDelete?.eventType?.currency || null, + length: bookingToDelete?.eventType?.length || null, + }; + const webhooks = await getWebhooks(subscriberOptions); const promises = webhooks.map((webhook) => - sendPayload(webhook.secret, eventTrigger, new Date().toISOString(), webhook, evt).catch((e) => { + sendPayload(webhook.secret, eventTrigger, new Date().toISOString(), webhook, { + ...evt, + ...eventTypeInfo, + status: "CANCELLED", + }).catch((e) => { console.error(`Error executing webhook for event: ${eventTrigger}, URL: ${webhook.subscriberUrl}`, e); }) ); diff --git a/apps/web/playwright/integrations.e2e.ts-snapshots/webhookResponse--calcom-web.txt b/apps/web/playwright/integrations.e2e.ts-snapshots/webhookResponse--calcom-web.txt index e295671895..66acf4008b 100644 --- a/apps/web/playwright/integrations.e2e.ts-snapshots/webhookResponse--calcom-web.txt +++ b/apps/web/playwright/integrations.e2e.ts-snapshots/webhookResponse--calcom-web.txt @@ -1 +1 @@ -{"triggerEvent":"BOOKING_CREATED","createdAt":"[redacted/dynamic]","payload":{"type":"30 min","title":"30 min between PRO and Test Testson","description":"","additionalNotes":"","customInputs":{},"startTime":"[redacted/dynamic]","endTime":"[redacted/dynamic]","organizer":{"name":"PRO","email":"[redacted/dynamic]","timeZone":"[redacted/dynamic]","language":"[redacted/dynamic]"},"attendees":[{"email":"test@example.com","name":"Test Testson","timeZone":"[redacted/dynamic]","language":"[redacted/dynamic]"}],"location":"[redacted/dynamic]","destinationCalendar":null,"hideCalendarNotes":false,"requiresConfirmation":"[redacted/dynamic]","eventTypeId":"[redacted/dynamic]","uid":"[redacted/dynamic]","bookingId":"[redacted/dynamic]","metadata":{},"additionalInformation":"[redacted/dynamic]"}} \ No newline at end of file +{"triggerEvent":"BOOKING_CREATED","createdAt":"[redacted/dynamic]","payload":{"type":"30 min","title":"30 min between PRO and Test Testson","description":"","additionalNotes":"","customInputs":{},"startTime":"[redacted/dynamic]","endTime":"[redacted/dynamic]","organizer":{"name":"PRO","email":"[redacted/dynamic]","timeZone":"[redacted/dynamic]","language":"[redacted/dynamic]"},"attendees":[{"email":"test@example.com","name":"Test Testson","timeZone":"[redacted/dynamic]","language":"[redacted/dynamic]"}],"location":"[redacted/dynamic]","destinationCalendar":null,"hideCalendarNotes":false,"requiresConfirmation":"[redacted/dynamic]","eventTypeId":"[redacted/dynamic]","uid":"[redacted/dynamic]","bookingId":"[redacted/dynamic]","metadata":{},"additionalInformation":"[redacted/dynamic]"}} diff --git a/packages/app-store/zapier/api/subscriptions/listBookings.ts b/packages/app-store/zapier/api/subscriptions/listBookings.ts index 76b3c2f332..981e03d8ba 100644 --- a/packages/app-store/zapier/api/subscriptions/listBookings.ts +++ b/packages/app-store/zapier/api/subscriptions/listBookings.ts @@ -1,8 +1,9 @@ import type { NextApiRequest, NextApiResponse } from "next"; +import { getHumanReadableLocationValue } from "@calcom/core/location"; import findValidApiKey from "@calcom/features/ee/api-keys/lib/findValidApiKey"; -import { defaultHandler, defaultResponder } from "@calcom/lib/server"; -import prisma, { bookingMinimalSelect } from "@calcom/prisma"; +import { defaultHandler, defaultResponder, getTranslation } from "@calcom/lib/server"; +import prisma from "@calcom/prisma"; async function handler(req: NextApiRequest, res: NextApiResponse) { const apiKey = req.query.apiKey as string; @@ -24,8 +25,25 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { userId: validKey.userId, }, select: { - ...bookingMinimalSelect, + title: true, + description: true, + customInputs: true, + startTime: true, + endTime: true, location: true, + cancellationReason: true, + status: true, + user: true, + eventType: { + select: { + title: true, + description: true, + requiresConfirmation: true, + price: true, + currency: true, + length: true, + }, + }, attendees: { select: { name: true, @@ -36,7 +54,13 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { }, }); - res.status(201).json(bookings); + const t = await getTranslation(bookings[0].user?.locale ?? "en", "common"); + + const updatedBookings = bookings.map((booking) => { + return { ...booking, location: getHumanReadableLocationValue(booking.location || "", t) }; + }); + + res.status(201).json(updatedBookings); } catch (error) { console.error(error); return res.status(500).json({ message: "Unable to get bookings." });