Compare commits

...

54 Commits

Author SHA1 Message Date
kodiakhq[bot] 6132c87c72
Merge branch 'main' into add_booking_confirmed_webhook_event 2022-01-13 19:54:49 +00:00
kodiakhq[bot] 98fe63625a
Merge branch 'main' into add_booking_confirmed_webhook_event 2022-01-13 19:52:02 +00:00
kodiakhq[bot] 4b0f2e56ef
Merge branch 'main' into add_booking_confirmed_webhook_event 2022-01-13 19:48:30 +00:00
kodiakhq[bot] 3667ea27d4
Merge branch 'main' into add_booking_confirmed_webhook_event 2022-01-13 19:38:09 +00:00
kodiakhq[bot] 5abd634a82
Merge branch 'main' into add_booking_confirmed_webhook_event 2022-01-13 18:10:14 +00:00
kodiakhq[bot] 9bc900f8f7
Merge branch 'main' into add_booking_confirmed_webhook_event 2022-01-13 16:53:27 +00:00
kodiakhq[bot] 8fa3cb3bed
Merge branch 'main' into add_booking_confirmed_webhook_event 2022-01-12 22:23:05 +00:00
kodiakhq[bot] 57d87873a1
Merge branch 'main' into add_booking_confirmed_webhook_event 2022-01-12 22:18:27 +00:00
kodiakhq[bot] c56b4a618d
Merge branch 'main' into add_booking_confirmed_webhook_event 2022-01-12 22:18:04 +00:00
kodiakhq[bot] a4df8dc99d
Merge branch 'main' into add_booking_confirmed_webhook_event 2022-01-12 21:55:19 +00:00
kodiakhq[bot] 24ab2bd843
Merge branch 'main' into add_booking_confirmed_webhook_event 2022-01-12 13:58:23 +00:00
kodiakhq[bot] 5579a89613
Merge branch 'main' into add_booking_confirmed_webhook_event 2022-01-12 13:47:30 +00:00
kodiakhq[bot] 55847647ff
Merge branch 'main' into add_booking_confirmed_webhook_event 2022-01-12 13:13:57 +00:00
kodiakhq[bot] 91e2b49c5f
Merge branch 'main' into add_booking_confirmed_webhook_event 2022-01-12 13:07:26 +00:00
kodiakhq[bot] f53fcce859
Merge branch 'main' into add_booking_confirmed_webhook_event 2022-01-12 12:35:35 +00:00
kodiakhq[bot] ed63a315f1
Merge branch 'main' into add_booking_confirmed_webhook_event 2022-01-12 09:29:50 +00:00
kodiakhq[bot] eda9cd3d15
Merge branch 'main' into add_booking_confirmed_webhook_event 2022-01-11 16:27:18 +00:00
kodiakhq[bot] 926f91d435
Merge branch 'main' into add_booking_confirmed_webhook_event 2022-01-11 11:39:21 +00:00
kodiakhq[bot] b000b970b5
Merge branch 'main' into add_booking_confirmed_webhook_event 2022-01-11 10:33:18 +00:00
kodiakhq[bot] 047e417a6a
Merge branch 'main' into add_booking_confirmed_webhook_event 2022-01-11 10:25:24 +00:00
kodiakhq[bot] 9141f0a66d
Merge branch 'main' into add_booking_confirmed_webhook_event 2022-01-11 08:54:36 +00:00
kodiakhq[bot] 0d0d03d693
Merge branch 'main' into add_booking_confirmed_webhook_event 2022-01-10 23:33:41 +00:00
kodiakhq[bot] ce42c572b8
Merge branch 'main' into add_booking_confirmed_webhook_event 2022-01-10 23:25:38 +00:00
kodiakhq[bot] 9d9bd3283a
Merge branch 'main' into add_booking_confirmed_webhook_event 2022-01-08 16:54:42 +00:00
kodiakhq[bot] 42677cfac4
Merge branch 'main' into add_booking_confirmed_webhook_event 2022-01-07 22:25:22 +00:00
kodiakhq[bot] cd0ad9c1da
Merge branch 'main' into add_booking_confirmed_webhook_event 2022-01-07 20:24:12 +00:00
kodiakhq[bot] ff105c72ac
Merge branch 'main' into add_booking_confirmed_webhook_event 2022-01-07 10:24:20 +00:00
kodiakhq[bot] e5c5ee6ecb
Merge branch 'main' into add_booking_confirmed_webhook_event 2022-01-06 22:14:24 +00:00
kodiakhq[bot] ada30fd353
Merge branch 'main' into add_booking_confirmed_webhook_event 2022-01-06 22:07:12 +00:00
kodiakhq[bot] 565a82649c
Merge branch 'main' into add_booking_confirmed_webhook_event 2022-01-06 17:29:10 +00:00
kodiakhq[bot] 6d8d2c2ed0
Merge branch 'main' into add_booking_confirmed_webhook_event 2022-01-06 10:39:56 +00:00
kodiakhq[bot] 40892067d7
Merge branch 'main' into add_booking_confirmed_webhook_event 2022-01-06 10:24:56 +00:00
kodiakhq[bot] c7e47171ac
Merge branch 'main' into add_booking_confirmed_webhook_event 2022-01-06 10:13:03 +00:00
kodiakhq[bot] fe9f1f56e0
Merge branch 'main' into add_booking_confirmed_webhook_event 2022-01-04 17:20:00 +00:00
kodiakhq[bot] e6d034a476
Merge branch 'main' into add_booking_confirmed_webhook_event 2022-01-04 17:19:21 +00:00
kodiakhq[bot] d9a46bbd5e
Merge branch 'main' into add_booking_confirmed_webhook_event 2022-01-04 10:06:49 +00:00
kodiakhq[bot] f1f86d8c32
Merge branch 'main' into add_booking_confirmed_webhook_event 2022-01-04 10:05:58 +00:00
kodiakhq[bot] 1e92d908dc
Merge branch 'main' into add_booking_confirmed_webhook_event 2022-01-03 23:19:43 +00:00
kodiakhq[bot] c72629cffe
Merge branch 'main' into add_booking_confirmed_webhook_event 2022-01-03 22:51:37 +00:00
kodiakhq[bot] 3059163979
Merge branch 'main' into add_booking_confirmed_webhook_event 2022-01-03 12:01:38 +00:00
kodiakhq[bot] 8dc4c7c25c
Merge branch 'main' into add_booking_confirmed_webhook_event 2021-12-30 17:16:59 +00:00
Bill Gale 41031aedd6
feat: add confirmed booking webhook event (#1296)
* Add booking confirmed webhook event tests

* Speeds up subsequent local testing

* Adds tests for BOOKING_CONFIRMED webhook

* Blank success page in January bookings (#1399)

* added 1 to UTC month conversion to make it 1 to 12

* with as numtype

* Zomars/cal 798 issue with billing portal (#1392)

* Uses stripeCustomerId from used metadata in billing portal

* Uses stripeCustomerId from used metadata in billing portal

# Conflicts:
#	ee/pages/api/integrations/stripepayment/portal.ts

Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2021-12-30 10:12:20 -07:00
zomars 31b1e8b044 Adds tests for BOOKING_CONFIRMED webhook 2021-12-29 18:20:26 -07:00
zomars cd2b279308 Speeds up subsequent local testing 2021-12-29 11:52:43 -07:00
Syed Ali Shahbaz 410bc2a87a
Merge branch 'main' into add_booking_confirmed_webhook_event 2021-12-29 16:32:03 +05:30
Bill Gale 0a0715dfbc
Merge branch 'main' into add_booking_confirmed_webhook_event 2021-12-28 15:04:56 +00:00
Peer Richelsen 82fb4a9dcd
Merge branch 'main' into add_booking_confirmed_webhook_event 2021-12-27 18:06:15 +01:00
Syed Ali Shahbaz 57dfde4e4d
Merge branch 'main' into add_booking_confirmed_webhook_event 2021-12-27 21:25:51 +05:30
Peer Richelsen fdd9e99b4d
Merge branch 'main' into add_booking_confirmed_webhook_event 2021-12-27 13:18:39 +01:00
Bill Gale c02cdacebb
Merge branch 'main' into add_booking_confirmed_webhook_event 2021-12-23 12:26:59 +00:00
Bill Gale 6b4e04b4fd
Merge branch 'main' into add_booking_confirmed_webhook_event 2021-12-15 13:29:07 +00:00
Bill Gale 67e0592c66
Merge branch 'main' into add_booking_confirmed_webhook_event 2021-12-13 15:52:10 +00:00
Bill Gale 6455bbd4d4
Merge branch 'main' into add_booking_confirmed_webhook_event 2021-12-13 11:04:47 +00:00
Bill Gale ff568f4aa4
feat: add confirmed booking webhook event 2021-12-11 16:12:32 +00:00
10 changed files with 131 additions and 24 deletions

View File

@ -5,6 +5,7 @@ import { useMutation } from "react-query";
import { HttpError } from "@lib/core/http/error"; import { HttpError } from "@lib/core/http/error";
import { useLocale } from "@lib/hooks/useLocale"; import { useLocale } from "@lib/hooks/useLocale";
import showToast from "@lib/notification";
import { inferQueryOutput, trpc } from "@lib/trpc"; import { inferQueryOutput, trpc } from "@lib/trpc";
import TableActions, { ActionType } from "@components/ui/TableActions"; import TableActions, { ActionType } from "@components/ui/TableActions";
@ -27,6 +28,7 @@ function BookingListItem(booking: BookingItem) {
if (!res.ok) { if (!res.ok) {
throw new HttpError({ statusCode: res.status }); throw new HttpError({ statusCode: res.status });
} }
showToast(t("booking_confirmed"), "success");
}, },
{ {
async onSettled() { async onSettled() {

View File

@ -5,4 +5,5 @@ export const WEBHOOK_TRIGGER_EVENTS = [
WebhookTriggerEvents.BOOKING_CANCELLED, WebhookTriggerEvents.BOOKING_CANCELLED,
WebhookTriggerEvents.BOOKING_CREATED, WebhookTriggerEvents.BOOKING_CREATED,
WebhookTriggerEvents.BOOKING_RESCHEDULED, WebhookTriggerEvents.BOOKING_RESCHEDULED,
] as ["BOOKING_CANCELLED", "BOOKING_CREATED", "BOOKING_RESCHEDULED"]; WebhookTriggerEvents.BOOKING_CONFIRMED,
] as ["BOOKING_CANCELLED", "BOOKING_CREATED", "BOOKING_RESCHEDULED", "BOOKING_CONFIRMED"];

View File

@ -11,6 +11,8 @@ import { CalendarEvent, AdditionInformation } from "@lib/integrations/calendar/i
import logger from "@lib/logger"; import logger from "@lib/logger";
import prisma from "@lib/prisma"; import prisma from "@lib/prisma";
import { BookingConfirmBody } from "@lib/types/booking"; import { BookingConfirmBody } from "@lib/types/booking";
import sendPayload from "@lib/webhooks/sendPayload";
import getSubscribers from "@lib/webhooks/subscriptions";
import { getTranslation } from "@server/lib/i18n"; import { getTranslation } from "@server/lib/i18n";
@ -84,6 +86,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
id: bookingId, id: bookingId,
}, },
select: { select: {
userId: true,
title: true, title: true,
description: true, description: true,
startTime: true, startTime: true,
@ -153,6 +156,23 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
await sendScheduledEmails({ ...evt, additionInformation: metadata }); await sendScheduledEmails({ ...evt, additionInformation: metadata });
} }
// Hook up the webhook logic here
const eventTrigger = "BOOKING_CONFIRMED";
// Send Webhook call if hooked to BOOKING.CONFIRMED
const subscribers = await getSubscribers(booking.userId, eventTrigger);
const promises = subscribers.map((sub) =>
sendPayload(
eventTrigger,
new Date().toISOString(),
sub.subscriberUrl,
evt,
sub.payloadTemplate
).catch((e) => {
console.error(`Error executing webhook for event: ${eventTrigger}, URL: ${sub.subscriberUrl}`, e);
})
);
await Promise.all(promises);
await prisma.booking.update({ await prisma.booking.update({
where: { where: {
id: bookingId, id: bookingId,

View File

@ -1,6 +1,35 @@
import { expect, test } from "@playwright/test"; import { expect, test } from "@playwright/test";
import { createHttpServer, todo, waitFor } from "./lib/testUtils"; import { WEBHOOK_TRIGGER_EVENTS } from "../lib/webhooks/constants";
import { createHttpServer, Request, todo } from "./lib/testUtils";
function removeDynamicProps(body: any) {
// remove dynamic properties that differs depending on where you run the tests
const dynamic = "[redacted/dynamic]";
body.createdAt = dynamic;
body.payload.startTime = dynamic;
body.payload.endTime = dynamic;
body.payload.location = dynamic;
for (const attendee of body.payload.attendees) {
attendee.timeZone = dynamic;
if (attendee.id) attendee.id = dynamic;
if (attendee.bookingId) attendee.bookingId = dynamic;
}
body.payload.organizer.timeZone = dynamic;
body.payload.uid = dynamic;
body.payload.additionInformation = dynamic;
return body;
}
function findRequest(requestList: Request[], triggerEvent: typeof WEBHOOK_TRIGGER_EVENTS[number]) {
return requestList.find(
(r) =>
r.body &&
typeof r.body === "object" &&
"triggerEvent" in r.body &&
(r.body as any).triggerEvent === triggerEvent
);
}
test.describe("integrations", () => { test.describe("integrations", () => {
test.use({ storageState: "playwright/artifacts/proStorageState.json" }); test.use({ storageState: "playwright/artifacts/proStorageState.json" });
@ -18,10 +47,19 @@ test.describe("integrations", () => {
todo("Can add CalDav Calendar"); todo("Can add CalDav Calendar");
todo("Can add Apple Calendar"); todo("Can add Apple Calendar");
});
test("add webhook & test that creating an event triggers a webhook call", async ({ page }, testInfo) => { test.describe("Webhooks", () => {
const webhookReceiver = createHttpServer(); test.use({ storageState: "playwright/artifacts/proStorageState.json" });
let webhookReceiver: ReturnType<typeof createHttpServer>;
test.beforeAll(() => {
webhookReceiver = createHttpServer();
});
test("Can add webhooks", async ({ page }) => {
await page.goto("/integrations");
// --- add webhook // --- add webhook
await page.click('[data-testid="new_webhook"]'); await page.click('[data-testid="new_webhook"]');
expect(page.locator(`[data-testid='WebhookDialogForm']`)).toBeVisible(); expect(page.locator(`[data-testid='WebhookDialogForm']`)).toBeVisible();
@ -30,11 +68,16 @@ test.describe("integrations", () => {
await page.click("[type=submit]"); await page.click("[type=submit]");
await page.waitForSelector(`text=Webhook created successfully!`);
// dialog is closed // dialog is closed
expect(page.locator(`[data-testid='WebhookDialogForm']`)).not.toBeVisible(); expect(page.locator(`[data-testid='WebhookDialogForm']`)).not.toBeVisible();
// page contains the url // page contains the url
expect(page.locator(`text='${webhookReceiver.url}'`)).toBeDefined(); expect(page.locator(`text='${webhookReceiver.url}'`)).toBeDefined();
});
// test.describe.parallel("Booking events", () => {
test("BOOKING_CREATED is triggered", async ({ page }, testInfo) => {
// --- Book the first available day next month in the pro user's "30min"-event // --- Book the first available day next month in the pro user's "30min"-event
await page.goto(`/pro/30min`); await page.goto(`/pro/30min`);
await page.click('[data-testid="incrementMonth"]'); await page.click('[data-testid="incrementMonth"]');
@ -46,33 +89,64 @@ test.describe("integrations", () => {
await page.fill('[name="email"]', "test@example.com"); await page.fill('[name="email"]', "test@example.com");
await page.press('[name="email"]', "Enter"); await page.press('[name="email"]', "Enter");
// --- check that webhook was called await page.waitForNavigation({
await waitFor(() => { url(url) {
expect(webhookReceiver.requestList.length).toBe(1); return url.pathname.endsWith("/success");
},
}); });
const [request] = webhookReceiver.requestList; // --- check that webhook was called
const body = request.body as any; const request = findRequest(webhookReceiver.requestList, "BOOKING_CREATED");
if (!request) throw Error("No request found for 'BOOKING_CREATED'");
// remove dynamic properties that differs depending on where you run the tests
const dynamic = "[redacted/dynamic]";
body.createdAt = dynamic;
body.payload.startTime = dynamic;
body.payload.endTime = dynamic;
body.payload.location = dynamic;
for (const attendee of body.payload.attendees) {
attendee.timeZone = dynamic;
}
body.payload.organizer.timeZone = dynamic;
body.payload.uid = dynamic;
body.payload.additionInformation = dynamic;
const body = removeDynamicProps(request.body);
// if we change the shape of our webhooks, we can simply update this by clicking `u` // if we change the shape of our webhooks, we can simply update this by clicking `u`
// console.log("BODY", body); // console.log("BODY", body);
// Text files shouldn't have platform specific suffixes // Text files shouldn't have platform specific suffixes
testInfo.snapshotSuffix = ""; testInfo.snapshotSuffix = "";
expect(JSON.stringify(body)).toMatchSnapshot(`webhookResponse.txt`); expect(JSON.stringify(body)).toMatchSnapshot(`BOOKING_CREATED-webhook-payload.txt`);
});
test("BOOKING_CONFIRMED is triggered", async ({ page }, testInfo) => {
// --- Book the first available day next month in the pro user's "30min"-event
await page.goto(`/pro/opt-in`);
await page.click('[data-testid="incrementMonth"]');
await page.click('[data-testid="day"][data-disabled="false"]');
await page.click('[data-testid="time"]');
// --- fill form
await page.fill('[name="name"]', "Test Testson");
await page.fill('[name="email"]', "test@example.com");
await page.press('[name="email"]', "Enter");
await page.waitForNavigation({
url(url) {
return url.pathname.endsWith("/success");
},
});
// Go to bookings to confirm it
await page.goto(`/bookings`);
// Confirm the booking
await page.click('[data-testid="confirm"]');
// Wait for the success message
await page.waitForSelector(`text=Booking Confirmed`);
const request = findRequest(webhookReceiver.requestList, "BOOKING_CONFIRMED");
if (!request) throw Error("No request found for 'BOOKING_CONFIRMED'");
const body = removeDynamicProps(request.body);
// if we change the shape of our webhooks, we can simply update this by clicking `u`
// console.log("BODY", body);
// Text files shouldn't have platform specific suffixes
testInfo.snapshotSuffix = "";
expect(JSON.stringify(body)).toMatchSnapshot(`BOOKING_CONFIRMED-webhook-payload.txt`);
});
// });
test.afterAll(() => {
webhookReceiver.close(); webhookReceiver.close();
}); });
}); });

View File

@ -0,0 +1 @@
{"triggerEvent":"BOOKING_CONFIRMED","createdAt":"[redacted/dynamic]","payload":{"type":"opt-in between Pro Example and Test Testson","title":"opt-in between Pro Example and Test Testson","description":"","startTime":"[redacted/dynamic]","endTime":"[redacted/dynamic]","organizer":{"email":"pro@example.com","name":"Pro Example","timeZone":"[redacted/dynamic]"},"attendees":[{"id":"[redacted/dynamic]","email":"test@example.com","name":"Test Testson","timeZone":"[redacted/dynamic]","bookingId":"[redacted/dynamic]"}],"location":"[redacted/dynamic]","uid":"[redacted/dynamic]","additionInformation":"[redacted/dynamic]"}}

View File

@ -16,7 +16,7 @@ export function randomString(length = 12) {
return result; return result;
} }
type Request = IncomingMessage & { body?: unknown }; export type Request = IncomingMessage & { body?: unknown };
type RequestHandlerOptions = { req: Request; res: ServerResponse }; type RequestHandlerOptions = { req: Request; res: ServerResponse };
type RequestHandler = (opts: RequestHandlerOptions) => void; type RequestHandler = (opts: RequestHandlerOptions) => void;

View File

@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "WebhookTriggerEvents" ADD VALUE 'BOOKING_CONFIRMED';

View File

@ -315,6 +315,7 @@ enum WebhookTriggerEvents {
BOOKING_CREATED BOOKING_CREATED
BOOKING_RESCHEDULED BOOKING_RESCHEDULED
BOOKING_CANCELLED BOOKING_CANCELLED
BOOKING_CONFIRMED
} }
model Webhook { model Webhook {

View File

@ -229,6 +229,12 @@ async function main() {
length: 60, length: 60,
price: 50, price: 50,
}, },
{
title: "opt-in",
slug: "opt-in",
length: 60,
requiresConfirmation: true,
},
], ],
}); });