Skip to content

Commit a82cc88

Browse files
zomarssean-brydon
andauthored
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 --------- Signed-off-by: zomars <zomars@me.com> Co-authored-by: sean-brydon <sean@cal.com>
1 parent 6632c26 commit a82cc88

File tree

14 files changed

+370
-153
lines changed

14 files changed

+370
-153
lines changed

apps/web/components/booking/BookingListItem.tsx

+13-18
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import Link from "next/link";
22
import { useState } from "react";
3-
import { useForm, Controller, useFieldArray } from "react-hook-form";
3+
import { Controller, useFieldArray, useForm } from "react-hook-form";
44

55
import type { EventLocationType, getEventLocationValue } from "@calcom/app-store/locations";
66
import {
@@ -31,20 +31,20 @@ import {
3131
DialogClose,
3232
DialogContent,
3333
DialogFooter,
34+
Dropdown,
35+
DropdownItem,
36+
DropdownMenuCheckboxItem,
37+
DropdownMenuContent,
38+
DropdownMenuItem,
39+
DropdownMenuLabel,
40+
DropdownMenuSeparator,
41+
DropdownMenuTrigger,
3442
Icon,
3543
MeetingTimeInTimezones,
3644
showToast,
3745
TableActions,
3846
TextAreaField,
3947
Tooltip,
40-
Dropdown,
41-
DropdownMenuContent,
42-
DropdownMenuTrigger,
43-
DropdownMenuItem,
44-
DropdownItem,
45-
DropdownMenuLabel,
46-
DropdownMenuSeparator,
47-
DropdownMenuCheckboxItem,
4848
} from "@calcom/ui";
4949

5050
import { ChargeCardDialog } from "@components/dialog/ChargeCardDialog";
@@ -691,13 +691,8 @@ const Attendee = (attendeeProps: AttendeeProps & NoShowProps) => {
691691
const { copyToClipboard, isCopied } = useCopy();
692692

693693
const noShowMutation = trpc.viewer.public.noShow.useMutation({
694-
onSuccess: async () => {
695-
showToast(
696-
t(noShow ? "x_marked_as_no_show" : "x_unmarked_as_no_show", {
697-
x: name || email,
698-
}),
699-
"success"
700-
);
694+
onSuccess: async (data) => {
695+
showToast(t(data.message, { x: name || email }), "success");
701696
},
702697
onError: (err) => {
703698
showToast(err.message, "error");
@@ -804,8 +799,8 @@ const GroupedAttendees = (groupedAttendeeProps: GroupedAttendeeProps) => {
804799
});
805800
const { t } = useLocale();
806801
const noShowMutation = trpc.viewer.public.noShow.useMutation({
807-
onSuccess: async () => {
808-
showToast(t("no_show_updated"), "success");
802+
onSuccess: async (data) => {
803+
showToast(t(data.message), "success");
809804
},
810805
onError: (err) => {
811806
showToast(err.message, "error");

apps/web/playwright/bookings-list.e2e.ts

+19-2
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ test.describe("Bookings", () => {
6363
});
6464
});
6565
test.describe("Past bookings", () => {
66-
test("Mark first guest as no-show", async ({ page, users, bookings }) => {
66+
test("Mark first guest as no-show", async ({ page, users, bookings, webhooks }) => {
6767
const firstUser = await users.create();
6868
const secondUser = await users.create();
6969

@@ -81,8 +81,8 @@ test.describe("Bookings", () => {
8181
],
8282
});
8383
const bookingWhereFirstUserIsOrganizer = await bookingWhereFirstUserIsOrganizerFixture.self();
84-
8584
await firstUser.apiLogin();
85+
const webhookReceiver = await webhooks.createReceiver();
8686
await page.goto(`/bookings/past`);
8787
const pastBookings = page.locator('[data-testid="past-bookings"]');
8888
const firstPastBooking = pastBookings.locator('[data-testid="booking-item"]').nth(0);
@@ -95,6 +95,23 @@ test.describe("Bookings", () => {
9595
await firstGuest.click();
9696
await expect(titleAndAttendees.locator('[data-testid="unmark-no-show"]')).toBeVisible();
9797
await expect(titleAndAttendees.locator('[data-testid="mark-no-show"]')).toBeHidden();
98+
await webhookReceiver.waitForRequestCount(1);
99+
const [request] = webhookReceiver.requestList;
100+
const body = request.body;
101+
// remove dynamic properties that differs depending on where you run the tests
102+
const dynamic = "[redacted/dynamic]";
103+
// @ts-expect-error we are modifying the object
104+
body.createdAt = dynamic;
105+
expect(body).toMatchObject({
106+
triggerEvent: "BOOKING_NO_SHOW_UPDATED",
107+
createdAt: "[redacted/dynamic]",
108+
payload: {
109+
message: "first@cal.com marked as no-show",
110+
attendees: [{ email: "first@cal.com", noShow: true, utcOffset: null }],
111+
bookingUid: bookingWhereFirstUserIsOrganizer?.uid,
112+
},
113+
});
114+
webhookReceiver.close();
98115
});
99116
test("Mark 3rd attendee as no-show", async ({ page, users, bookings }) => {
100117
const firstUser = await users.create();
+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { expect, type Page } from "@playwright/test";
2+
3+
import { createHttpServer } from "../lib/testUtils";
4+
5+
export function createWebhookPageFixture(page: Page) {
6+
return {
7+
createTeamReceiver: async () => {
8+
const webhookReceiver = createHttpServer();
9+
await page.goto(`/settings/developer/webhooks`);
10+
await page.click('[data-testid="new_webhook"]');
11+
await page.click('[data-testid="option-team-1"]');
12+
await page.waitForURL((u) => u.pathname === "/settings/developer/webhooks/new");
13+
const url = page.url();
14+
const teamId = Number(new URL(url).searchParams.get("teamId")) as number;
15+
await page.click('[data-testid="new_webhook"]');
16+
await page.fill('[name="subscriberUrl"]', webhookReceiver.url);
17+
await page.fill('[name="secret"]', "secret");
18+
await Promise.all([
19+
page.click("[type=submit]"),
20+
page.waitForURL((url) => url.pathname.endsWith("/settings/developer/webhooks")),
21+
]);
22+
expect(page.locator(`text='${webhookReceiver.url}'`)).toBeDefined();
23+
return { webhookReceiver, teamId };
24+
},
25+
createReceiver: async () => {
26+
const webhookReceiver = createHttpServer();
27+
await page.goto(`/settings/developer/webhooks`);
28+
await page.click('[data-testid="new_webhook"]');
29+
await page.fill('[name="subscriberUrl"]', webhookReceiver.url);
30+
await page.fill('[name="secret"]', "secret");
31+
await Promise.all([
32+
page.click("[type=submit]"),
33+
page.waitForURL((url) => url.pathname.endsWith("/settings/developer/webhooks")),
34+
]);
35+
expect(page.locator(`text='${webhookReceiver.url}'`)).toBeDefined();
36+
return webhookReceiver;
37+
},
38+
};
39+
}

apps/web/playwright/lib/fixtures.ts

+6
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { createBookingPageFixture } from "../fixtures/regularBookings";
1616
import { createRoutingFormsFixture } from "../fixtures/routingForms";
1717
import { createServersFixture } from "../fixtures/servers";
1818
import { createUsersFixture } from "../fixtures/users";
19+
import { createWebhookPageFixture } from "../fixtures/webhooks";
1920
import { createWorkflowPageFixture } from "../fixtures/workflows";
2021

2122
export interface Fixtures {
@@ -34,6 +35,7 @@ export interface Fixtures {
3435
features: ReturnType<typeof createFeatureFixture>;
3536
eventTypePage: ReturnType<typeof createEventTypeFixture>;
3637
appsPage: ReturnType<typeof createAppsFixture>;
38+
webhooks: ReturnType<typeof createWebhookPageFixture>;
3739
}
3840

3941
declare global {
@@ -110,4 +112,8 @@ export const test = base.extend<Fixtures>({
110112
const appsPage = createAppsFixture(page);
111113
await use(appsPage);
112114
},
115+
webhooks: async ({ page }, use) => {
116+
const webhooks = createWebhookPageFixture(page);
117+
await use(webhooks);
118+
},
113119
});

0 commit comments

Comments
 (0)