Improves Zapier response and payload (#3892)

* improve listBookings API call

* improve payload for zapier call

* use more readable location for integrations

* remove cancellation reason

* convert location to readbale string

* code clean up

* code clean up

* updated expected webhook response for e2e tests

* fix failing e2e

* revert webhook response

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
This commit is contained in:
Carina Wollendorfer
2022-09-08 13:18:00 -06:00
committed by GitHub
co-authored by CarinaWolli kodiakhq[bot]
parent 0b8eb3b8ec
commit bb24539731
5 changed files with 116 additions and 14 deletions
+53 -6
View File
@@ -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<Webhook, "subscriberUrl" | "appId" | "payloadTemplate">,
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 {
+13 -1
View File
@@ -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);
})
+21 -2
View File
@@ -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);
})
);
@@ -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]"}}
{"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]"}}
@@ -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." });