feat: booking no show webhook (#15502)

* WIP

Signed-off-by: zomars <zomars@me.com>

* WIP

Signed-off-by: zomars <zomars@me.com>

* WIP

Signed-off-by: zomars <zomars@me.com>

* Type fixes

* Update webhook.e2e.ts

* Update noShow.handler.ts

* Log failed results

* Updates tests

* Show generic error on frontend.

* test: add basic webhook service test

* fix: webhook end to end test

* fix: type error

* fix: test

---------

Signed-off-by: zomars <zomars@me.com>
Co-authored-by: sean-brydon <sean@cal.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit222001@gmail.com>
This commit is contained in:
Omar López
2024-06-27 20:12:09 +00:00
committed by GitHub
co-authored by sean-brydon Udit Takkar Udit Takkar
parent 6207afdf03
commit 3948b8d203
14 changed files with 371 additions and 156 deletions
+13 -18
View File
@@ -1,6 +1,6 @@
import Link from "next/link";
import { useState } from "react";
import { useForm, Controller, useFieldArray } from "react-hook-form";
import { Controller, useFieldArray, useForm } from "react-hook-form";
import type { EventLocationType, getEventLocationValue } from "@calcom/app-store/locations";
import {
@@ -31,20 +31,20 @@ import {
DialogClose,
DialogContent,
DialogFooter,
Dropdown,
DropdownItem,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
Icon,
MeetingTimeInTimezones,
showToast,
TableActions,
TextAreaField,
Tooltip,
Dropdown,
DropdownMenuContent,
DropdownMenuTrigger,
DropdownMenuItem,
DropdownItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuCheckboxItem,
} from "@calcom/ui";
import { ChargeCardDialog } from "@components/dialog/ChargeCardDialog";
@@ -691,13 +691,8 @@ const Attendee = (attendeeProps: AttendeeProps & NoShowProps) => {
const { copyToClipboard, isCopied } = useCopy();
const noShowMutation = trpc.viewer.public.noShow.useMutation({
onSuccess: async () => {
showToast(
t(noShow ? "x_marked_as_no_show" : "x_unmarked_as_no_show", {
x: name || email,
}),
"success"
);
onSuccess: async (data) => {
showToast(t(data.message, { x: name || email }), "success");
},
onError: (err) => {
showToast(err.message, "error");
@@ -804,8 +799,8 @@ const GroupedAttendees = (groupedAttendeeProps: GroupedAttendeeProps) => {
});
const { t } = useLocale();
const noShowMutation = trpc.viewer.public.noShow.useMutation({
onSuccess: async () => {
showToast(t("no_show_updated"), "success");
onSuccess: async (data) => {
showToast(t(data.message), "success");
},
onError: (err) => {
showToast(err.message, "error");
+19 -2
View File
@@ -63,7 +63,7 @@ test.describe("Bookings", () => {
});
});
test.describe("Past bookings", () => {
test("Mark first guest as no-show", async ({ page, users, bookings }) => {
test("Mark first guest as no-show", async ({ page, users, bookings, webhooks }) => {
const firstUser = await users.create();
const secondUser = await users.create();
@@ -81,8 +81,8 @@ test.describe("Bookings", () => {
],
});
const bookingWhereFirstUserIsOrganizer = await bookingWhereFirstUserIsOrganizerFixture.self();
await firstUser.apiLogin();
const webhookReceiver = await webhooks.createReceiver();
await page.goto(`/bookings/past`);
const pastBookings = page.locator('[data-testid="past-bookings"]');
const firstPastBooking = pastBookings.locator('[data-testid="booking-item"]').nth(0);
@@ -95,6 +95,23 @@ test.describe("Bookings", () => {
await firstGuest.click();
await expect(titleAndAttendees.locator('[data-testid="unmark-no-show"]')).toBeVisible();
await expect(titleAndAttendees.locator('[data-testid="mark-no-show"]')).toBeHidden();
await webhookReceiver.waitForRequestCount(1);
const [request] = webhookReceiver.requestList;
const body = request.body;
// remove dynamic properties that differs depending on where you run the tests
const dynamic = "[redacted/dynamic]";
// @ts-expect-error we are modifying the object
body.createdAt = dynamic;
expect(body).toMatchObject({
triggerEvent: "BOOKING_NO_SHOW_UPDATED",
createdAt: "[redacted/dynamic]",
payload: {
message: "first@cal.com marked as no-show",
attendees: [{ email: "first@cal.com", noShow: true, utcOffset: null }],
bookingUid: bookingWhereFirstUserIsOrganizer?.uid,
},
});
webhookReceiver.close();
});
test("Mark 3rd attendee as no-show", async ({ page, users, bookings }) => {
const firstUser = await users.create();
+39
View File
@@ -0,0 +1,39 @@
import { expect, type Page } from "@playwright/test";
import { createHttpServer } from "../lib/testUtils";
export function createWebhookPageFixture(page: Page) {
return {
createTeamReceiver: async () => {
const webhookReceiver = createHttpServer();
await page.goto(`/settings/developer/webhooks`);
await page.click('[data-testid="new_webhook"]');
await page.click('[data-testid="option-team-1"]');
await page.waitForURL((u) => u.pathname === "/settings/developer/webhooks/new");
const url = page.url();
const teamId = Number(new URL(url).searchParams.get("teamId")) as number;
await page.click('[data-testid="new_webhook"]');
await page.fill('[name="subscriberUrl"]', webhookReceiver.url);
await page.fill('[name="secret"]', "secret");
await Promise.all([
page.click("[type=submit]"),
page.waitForURL((url) => url.pathname.endsWith("/settings/developer/webhooks")),
]);
expect(page.locator(`text='${webhookReceiver.url}'`)).toBeDefined();
return { webhookReceiver, teamId };
},
createReceiver: async () => {
const webhookReceiver = createHttpServer();
await page.goto(`/settings/developer/webhooks`);
await page.click('[data-testid="new_webhook"]');
await page.fill('[name="subscriberUrl"]', webhookReceiver.url);
await page.fill('[name="secret"]', "secret");
await Promise.all([
page.click("[type=submit]"),
page.waitForURL((url) => url.pathname.endsWith("/settings/developer/webhooks")),
]);
expect(page.locator(`text='${webhookReceiver.url}'`)).toBeDefined();
return webhookReceiver;
},
};
}
+6
View File
@@ -16,6 +16,7 @@ import { createBookingPageFixture } from "../fixtures/regularBookings";
import { createRoutingFormsFixture } from "../fixtures/routingForms";
import { createServersFixture } from "../fixtures/servers";
import { createUsersFixture } from "../fixtures/users";
import { createWebhookPageFixture } from "../fixtures/webhooks";
import { createWorkflowPageFixture } from "../fixtures/workflows";
export interface Fixtures {
@@ -34,6 +35,7 @@ export interface Fixtures {
features: ReturnType<typeof createFeatureFixture>;
eventTypePage: ReturnType<typeof createEventTypeFixture>;
appsPage: ReturnType<typeof createAppsFixture>;
webhooks: ReturnType<typeof createWebhookPageFixture>;
}
declare global {
@@ -110,4 +112,8 @@ export const test = base.extend<Fixtures>({
const appsPage = createAppsFixture(page);
await use(appsPage);
},
webhooks: async ({ page }, use) => {
const webhooks = createWebhookPageFixture(page);
await use(webhooks);
},
});
+20 -118
View File
@@ -1,4 +1,3 @@
import type { Page } from "@playwright/test";
import { expect } from "@playwright/test";
import { v4 as uuidv4 } from "uuid";
@@ -10,7 +9,6 @@ import { test } from "./lib/fixtures";
import {
bookOptinEvent,
bookTimeSlot,
createHttpServer,
createUserWithSeatedEventAndAttendees,
gotoRoutingLink,
selectFirstAvailableTimeSlotNextMonth,
@@ -24,54 +22,16 @@ test.afterEach(async ({ users }) => {
await users.deleteAll();
});
async function createWebhookReceiver(page: Page) {
const webhookReceiver = createHttpServer();
await page.goto(`/settings/developer/webhooks`);
// --- add webhook
await page.click('[data-testid="new_webhook"]');
await page.fill('[name="subscriberUrl"]', webhookReceiver.url);
await page.fill('[name="secret"]', "secret");
await Promise.all([
page.click("[type=submit]"),
page.waitForURL((url) => url.pathname.endsWith("/settings/developer/webhooks")),
]);
// page contains the url
expect(page.locator(`text='${webhookReceiver.url}'`)).toBeDefined();
return webhookReceiver;
}
test.describe("BOOKING_CREATED", async () => {
test("add webhook & test that creating an event triggers a webhook call", async ({
page,
users,
webhooks,
}, _testInfo) => {
const webhookReceiver = createHttpServer();
const user = await users.create();
const [eventType] = user.eventTypes;
await user.apiLogin();
await page.goto(`/settings/developer/webhooks`);
// --- add webhook
await page.click('[data-testid="new_webhook"]');
await page.fill('[name="subscriberUrl"]', webhookReceiver.url);
await page.fill('[name="secret"]', "secret");
await Promise.all([
page.click("[type=submit]"),
page.waitForURL((url) => url.pathname.endsWith("/settings/developer/webhooks")),
]);
// page contains the url
expect(page.locator(`text='${webhookReceiver.url}'`)).toBeDefined();
const webhookReceiver = await webhooks.createReceiver();
// --- Book the first available day next month in the pro user's "30min"-event
await page.goto(`/${user.username}/${eventType.slug}`);
@@ -169,8 +129,8 @@ test.describe("BOOKING_REJECTED", async () => {
test("can book an event that requires confirmation and then that booking can be rejected by organizer", async ({
page,
users,
webhooks,
}) => {
const webhookReceiver = createHttpServer();
// --- create a user
const user = await users.create();
@@ -182,24 +142,7 @@ test.describe("BOOKING_REJECTED", async () => {
// --- login as that user
await user.apiLogin();
await page.goto(`/settings/developer/webhooks`);
// --- add webhook
await page.click('[data-testid="new_webhook"]');
await page.fill('[name="subscriberUrl"]', webhookReceiver.url);
await page.fill('[name="secret"]', "secret");
await Promise.all([
page.click("[type=submit]"),
page.waitForURL((url) => url.pathname.endsWith("/settings/developer/webhooks")),
]);
// page contains the url
expect(page.locator(`text='${webhookReceiver.url}'`)).toBeDefined();
const webhookReceiver = await webhooks.createReceiver();
await page.goto("/bookings/unconfirmed");
await page.click('[data-testid="reject"]');
await page.click('[data-testid="rejection-confirm"]');
@@ -293,30 +236,14 @@ test.describe("BOOKING_REQUESTED", async () => {
test("can book an event that requires confirmation and get a booking requested event", async ({
page,
users,
webhooks,
}) => {
const webhookReceiver = createHttpServer();
// --- create a user
const user = await users.create();
// --- login as that user
await user.apiLogin();
await page.goto(`/settings/developer/webhooks`);
// --- add webhook
await page.click('[data-testid="new_webhook"]');
await page.fill('[name="subscriberUrl"]', webhookReceiver.url);
await page.fill('[name="secret"]', "secret");
await Promise.all([
page.click("[type=submit]"),
page.waitForURL((url) => url.pathname.endsWith("/settings/developer/webhooks")),
]);
// page contains the url
expect(page.locator(`text='${webhookReceiver.url}'`)).toBeDefined();
const webhookReceiver = await webhooks.createReceiver();
// --- visit user page
await page.goto(`/${user.username}`);
@@ -410,13 +337,18 @@ test.describe("BOOKING_REQUESTED", async () => {
});
test.describe("BOOKING_RESCHEDULED", async () => {
test("can reschedule a booking and get a booking rescheduled event", async ({ page, users, bookings }) => {
test("can reschedule a booking and get a booking rescheduled event", async ({
page,
users,
bookings,
webhooks,
}) => {
const user = await users.create();
const [eventType] = user.eventTypes;
await user.apiLogin();
const webhookReceiver = await createWebhookReceiver(page);
const webhookReceiver = await webhooks.createReceiver();
const booking = await bookings.create(user.id, user.username, eventType.id, {
status: BookingStatus.ACCEPTED,
@@ -451,6 +383,7 @@ test.describe("BOOKING_RESCHEDULED", async () => {
page,
users,
bookings,
webhooks,
}) => {
const { user, eventType, booking } = await createUserWithSeatedEventAndAttendees({ users, bookings }, [
{ name: "John First", email: "first+seats@cal.com", timeZone: "Europe/Berlin" },
@@ -464,7 +397,7 @@ test.describe("BOOKING_RESCHEDULED", async () => {
await user.apiLogin();
const webhookReceiver = await createWebhookReceiver(page);
const webhookReceiver = await webhooks.createReceiver();
const bookingAttendees = await prisma.attendee.findMany({
where: { bookingId: booking.id },
@@ -670,24 +603,11 @@ test.describe("MEETING_ENDED, MEETING_STARTED", async () => {
});
test.describe("FORM_SUBMITTED", async () => {
test("on submitting user form, triggers user webhook", async ({ page, users, routingForms }) => {
const webhookReceiver = createHttpServer();
const user = await users.create(null, {
hasTeam: true,
});
test("on submitting user form, triggers user webhook", async ({ page, users, routingForms, webhooks }) => {
const user = await users.create();
await user.apiLogin();
await page.goto(`/settings/developer/webhooks/new`);
// Add webhook
await page.fill('[name="subscriberUrl"]', webhookReceiver.url);
await page.fill('[name="secret"]', "secret");
await page.click("[type=submit]");
// Page contains the url
expect(page.locator(`text='${webhookReceiver.url}'`)).toBeDefined();
const webhookReceiver = await webhooks.createReceiver();
await page.waitForLoadState("networkidle");
const form = await routingForms.create({
@@ -736,21 +656,12 @@ test.describe("FORM_SUBMITTED", async () => {
webhookReceiver.close();
});
test("on submitting team form, triggers team webhook", async ({ page, users, routingForms }) => {
const webhookReceiver = createHttpServer();
test("on submitting team form, triggers team webhook", async ({ page, users, routingForms, webhooks }) => {
const user = await users.create(null, {
hasTeam: true,
});
await user.apiLogin();
await page.goto(`/settings/developer/webhooks`);
const teamId = await clickFirstTeamWebhookCta(page);
// Add webhook
await page.fill('[name="subscriberUrl"]', webhookReceiver.url);
await page.fill('[name="secret"]', "secret");
await page.click("[type=submit]");
const { webhookReceiver, teamId } = await webhooks.createTeamReceiver();
const form = await routingForms.create({
name: "Test Form",
userId: user.id,
@@ -797,12 +708,3 @@ test.describe("FORM_SUBMITTED", async () => {
webhookReceiver.close();
});
});
async function clickFirstTeamWebhookCta(page: Page) {
await page.click('[data-testid="new_webhook"]');
await page.click('[data-testid="option-team-1"]');
await page.waitForURL((u) => u.pathname === "/settings/developer/webhooks/new");
const url = page.url();
const teamId = Number(new URL(url).searchParams.get("teamId")) as number;
return teamId;
}
@@ -471,6 +471,7 @@
"meeting_ended": "Meeting Ended",
"form_submitted": "Form Submitted",
"booking_paid": "Booking Paid",
"booking_no_show_updated": "Booking No-Show Updated",
"event_triggers": "Event Triggers",
"subscriber_url": "Subscriber URL",
"create_new_webhook": "Create a new webhook",
@@ -38,6 +38,7 @@ const WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP_V2: Record<string, WebhookTriggerEve
{ value: WebhookTriggerEvents.BOOKING_PAYMENT_INITIATED, label: "booking_payment_initiated" },
{ value: WebhookTriggerEvents.BOOKING_RESCHEDULED, label: "booking_rescheduled" },
{ value: WebhookTriggerEvents.BOOKING_PAID, label: "booking_paid" },
{ value: WebhookTriggerEvents.BOOKING_NO_SHOW_UPDATED, label: "booking_no_show_updated" },
{ value: WebhookTriggerEvents.MEETING_ENDED, label: "meeting_ended" },
{ value: WebhookTriggerEvents.MEETING_STARTED, label: "meeting_started" },
{ value: WebhookTriggerEvents.RECORDING_READY, label: "recording_ready" },
@@ -0,0 +1,40 @@
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import getWebhooks from "./getWebhooks";
import sendOrSchedulePayload from "./sendOrSchedulePayload";
const log = logger.getSubLogger({ prefix: ["[WebhookService] "] });
/** This is a WIP. With minimal methods until the API matures and stabilizes */
export class WebhookService {
private options = {} as Parameters<typeof getWebhooks>[0];
private webhooks: Awaited<ReturnType<typeof getWebhooks>> = [];
constructor(options: Parameters<typeof getWebhooks>[0]) {
return (async (): Promise<WebhookService> => {
this.options = options;
this.webhooks = await getWebhooks(options);
return this;
})() as unknown as WebhookService;
}
async getWebhooks() {
return this.webhooks;
}
async sendPayload(payload: Parameters<typeof sendOrSchedulePayload>[4]) {
const promises = this.webhooks.map((sub) =>
sendOrSchedulePayload(
sub.secret,
this.options.triggerEvent,
new Date().toISOString(),
sub,
payload
).catch((e) => {
log.error(
`Error executing webhook for event: ${this.options.triggerEvent}, URL: ${sub.subscriberUrl}`,
safeStringify(e)
);
})
);
await Promise.allSettled(promises);
}
}
@@ -15,6 +15,7 @@ export const WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP = {
WebhookTriggerEvents.BOOKING_REJECTED,
WebhookTriggerEvents.RECORDING_READY,
WebhookTriggerEvents.INSTANT_MEETING,
WebhookTriggerEvents.BOOKING_NO_SHOW_UPDATED,
] as const,
"routing-forms": [WebhookTriggerEvents.FORM_SUBMITTED] as const,
};
@@ -29,7 +29,14 @@ export type WithUTCOffsetType<T> = T & {
attendees?: (Person & UTCOffset)[];
};
export type BookingNoShowUpdatedPayload = {
message: string;
bookingUid: string;
attendees: { email: string; noShow: boolean }[];
};
export type WebhookDataType = CalendarEvent &
// BookingNoShowUpdatedPayload & // This breaks all other webhooks
EventTypeInfo & {
metadata?: { [key: string]: string | number | boolean | null };
bookingId?: number;
@@ -0,0 +1,141 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { WebhookTriggerEvents } from "@calcom/prisma/enums";
import { WebhookService } from "../WebhookService";
import getWebhooks from "../getWebhooks";
vi.mock("../getWebhooks");
vi.mock("../sendOrSchedulePayload");
vi.mock("@calcom/lib/logger", async () => {
const actual = await vi.importActual<typeof import("@calcom/lib/logger")>("@calcom/lib/logger");
return {
...actual,
getSubLogger: vi.fn(() => ({
error: vi.fn(),
})),
};
});
vi.mock("@calcom/lib/safeStringify", () => ({
safeStringify: JSON.stringify,
}));
describe("WebhookService", () => {
const mockOptions = {
id: "mockOptionsId",
subscriberUrl: "subUrl",
payloadTemplate: "PayloadTemplate",
appId: "AppId",
secret: "WhSecret",
triggerEvent: WebhookTriggerEvents.BOOKING_CREATED,
};
beforeEach(() => {
vi.resetAllMocks();
});
it("should initialize with options and webhooks", async () => {
const mockWebhooks = [
{
id: "webhookId",
subscriberUrl: "url",
secret: "secret",
appId: "appId",
payloadTemplate: "payloadTemplate",
},
];
vi.mocked(getWebhooks).mockResolvedValue(mockWebhooks);
// Has to be called with await due to the iffi being async
const service = await new WebhookService(mockOptions);
expect(service).toBeInstanceOf(WebhookService);
expect(await service.getWebhooks()).toEqual(mockWebhooks);
expect(getWebhooks).toHaveBeenCalledWith(mockOptions);
});
// it("should send payload to all webhooks", async () => {
// const mockWebhooks = [
// {
// id: "webhookId",
// subscriberUrl: "url",
// secret: "secret",
// appId: "appId",
// payloadTemplate: "payloadTemplate",
// },
// {
// id: "webhookId2",
// subscriberUrl: "url",
// secret: "secret2",
// appId: "appId2",
// payloadTemplate: "payloadTemplate",
// },
// ];
// vi.mocked(getWebhooks).mockResolvedValue(mockWebhooks);
// const service = await new WebhookService(mockOptions);
//
// const payload = {
// secretKey: "secret",
// triggerEvent: "triggerEvent",
// createdAt: "now",
// webhook: {
// subscriberUrl: "url",
// appId: "appId",
// payloadTemplate: "payloadTemplate",
// },
// data: "test",
// };
//
// await service.sendPayload(payload as any);
//
// expect(sendOrSchedulePayload).toHaveBeenCalledTimes(mockWebhooks.length);
//
// mockWebhooks.forEach((webhook) => {
// expect(sendOrSchedulePayload).toHaveBeenCalledWith(
// webhook.secret,
// mockOptions.triggerEvent,
// expect.any(String),
// webhook,
// payload
// );
// });
// });
//
// it("should log error when sending payload fails", async () => {
// const mockWebhooks = [
// {
// id: "webhookId",
// subscriberUrl: "url",
// secret: "secret",
// appId: "appId",
// payloadTemplate: "payloadTemplate",
// },
// ];
// vi.mocked(getWebhooks).mockResolvedValue(mockWebhooks);
//
// const logError = vi.fn();
//
// (sendOrSchedulePayload as any).mockImplementation(() => {
// throw new Error("Failure");
// });
//
// const service = new WebhookService(mockOptions);
//
// const payload = {
// secretKey: "secret", triggerEvent: "triggerEvent", createdAt: "now", webhook: {
// subscriberUrl: "url",
// appId: "appId",
// payloadTemplate: "payloadTemplate"
// },
// data: "test"
// };
//
// await service.sendPayload(payload as any);
//
// expect(logError).toHaveBeenCalledWith(
// `Error executing webhook for event: ${mockOptions.triggerEvent}, URL: ${mockWebhooks[0].subscriberUrl}`,
// JSON.stringify(new Error("Failure"))
// );
// });
});
@@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "WebhookTriggerEvents" ADD VALUE 'BOOKING_NO_SHOW_UPDATED';
+1
View File
@@ -708,6 +708,7 @@ enum WebhookTriggerEvents {
BOOKING_REQUESTED
BOOKING_CANCELLED
BOOKING_REJECTED
BOOKING_NO_SHOW_UPDATED
FORM_SUBMITTED
MEETING_ENDED
MEETING_STARTED
@@ -1,5 +1,9 @@
import { WebhookService } from "@calcom/features/webhooks/lib/WebhookService";
import getOrgIdFromMemberOrTeamId from "@calcom/lib/getOrgIdFromMemberOrTeamId";
import logger from "@calcom/lib/logger";
import { getTranslation } from "@calcom/lib/server/i18n";
import { prisma } from "@calcom/prisma";
import { WebhookTriggerEvents } from "@calcom/prisma/client";
import type { TNoShowInputSchema } from "./noShow.schema";
@@ -7,6 +11,24 @@ type NoShowOptions = {
input: TNoShowInputSchema;
};
const getResultPayload = async (attendees: { email: string; noShow: boolean }[]) => {
if (attendees.length === 1) {
const [attendee] = attendees;
return {
message: attendee.noShow ? "x_marked_as_no_show" : "x_unmarked_as_no_show",
attendees: [attendee],
};
}
return { message: "no_show_updated", attendees: attendees };
};
const logFailedResults = (results: PromiseSettledResult<any>[]) => {
const failed = results.filter((x) => x.status === "rejected") as PromiseRejectedResult[];
if (failed.length < 1) return;
const failedMessage = failed.map((r) => r.reason);
console.error("Failed to update no-show status", failedMessage.join(","));
};
export const noShowHandler = async ({ input }: NoShowOptions) => {
const { bookingUid, attendees } = input;
@@ -31,33 +53,73 @@ export const noShowHandler = async ({ input }: NoShowOptions) => {
email: true,
},
});
const allAttendeesMap = allAttendees.reduce((acc, attendee) => {
acc[attendee.email] = attendee;
return acc;
}, {} as Record<string, { id: number; email: string }>);
const updatePromises = attendees.map((attendee) => {
const attendeeToUpdate = allAttendees.find((a) => a.email === attendee.email);
if (attendeeToUpdate) {
return prisma.attendee.update({
where: { id: attendeeToUpdate.id },
data: { noShow: attendee.noShow },
});
}
const attendeeToUpdate = allAttendeesMap[attendee.email];
if (!attendeeToUpdate) return;
return prisma.attendee.update({
where: { id: attendeeToUpdate.id },
data: { noShow: attendee.noShow },
});
});
await Promise.all(updatePromises);
} else {
await prisma.booking.update({
where: {
uid: bookingUid,
},
data: {
noShowHost: true,
const results = await Promise.allSettled(updatePromises);
logFailedResults(results);
const _attendees = results
.filter((x) => x.status === "fulfilled")
.map((x) => (x as PromiseFulfilledResult<{ noShow: boolean; email: string }>).value)
.map((x) => ({ email: x.email, noShow: x.noShow }));
const payload = await getResultPayload(_attendees);
const booking = await prisma.booking.findUnique({
where: { uid: bookingUid },
select: {
eventType: {
select: {
id: true,
teamId: true,
userId: true,
},
},
},
});
const orgId = await getOrgIdFromMemberOrTeamId({
memberId: booking?.eventType?.userId,
teamId: booking?.eventType?.teamId,
});
const webhooks = await new WebhookService({
teamId: booking?.eventType?.teamId,
userId: booking?.eventType?.userId,
eventTypeId: booking?.eventType?.id,
orgId,
triggerEvent: WebhookTriggerEvents.BOOKING_NO_SHOW_UPDATED,
});
const t = await getTranslation("en", "common");
await webhooks.sendPayload({
...payload,
/** We send webhook message pre-translated, on client we already handle this */
// @ts-expect-error payload is too booking specific, we need to refactor this
message: t(payload.message, { x: payload.attendees[0]?.email || "User" }),
bookingUid,
});
return payload;
}
await prisma.booking.update({
where: {
uid: bookingUid,
},
data: {
noShowHost: true,
},
});
return { message: "No-show status updated", noShowHost: true };
} catch (error) {
if (error instanceof Error) {
logger.error(error.message);
}
return { message: "Failed to update no-show status" };
}
};