From 0391bfe887d1da27d037512973baecd30a875c08 Mon Sep 17 00:00:00 2001 From: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> Date: Thu, 2 May 2024 10:42:38 -0400 Subject: [PATCH 1/8] test: add unit tests for sms sending (#14737) * add tests for workflow sms * fix type error * improvements * add tests for team workflow * rename test * add emailsToReceive everywhere * fix type errors * code clean up * fix fresh-booking.test.ts * add destination email to emailsToReceive * remove unused webhooks --------- Co-authored-by: CarinaWolli Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> Co-authored-by: Hariom --- apps/web/test/fixtures/fixtures.ts | 11 + .../utils/bookingScenario/bookingScenario.ts | 5 +- .../web/test/utils/bookingScenario/expects.ts | 68 ++- .../getMockRequestDataForBooking.ts | 1 + apps/web/test/utils/bookingScenario/test.ts | 6 +- .../test/fresh-booking.test.ts | 31 +- .../handleNewBooking/test/reschedule.test.ts | 14 +- .../test/workflow-notifications.test.ts | 541 ++++++++++++++++++ .../lib/reminders/providers/twilioProvider.ts | 22 +- packages/lib/testSMS.ts | 21 + 10 files changed, 674 insertions(+), 46 deletions(-) create mode 100644 packages/features/bookings/lib/handleNewBooking/test/workflow-notifications.test.ts create mode 100644 packages/lib/testSMS.ts diff --git a/apps/web/test/fixtures/fixtures.ts b/apps/web/test/fixtures/fixtures.ts index 121c188bb3bd7e..438874624b48ed 100644 --- a/apps/web/test/fixtures/fixtures.ts +++ b/apps/web/test/fixtures/fixtures.ts @@ -2,15 +2,20 @@ import { test as base } from "vitest"; import { getTestEmails } from "@calcom/lib/testEmails"; +import { getTestSMS } from "@calcom/lib/testSMS"; export interface Fixtures { emails: ReturnType; + sms: ReturnType; } export const test = base.extend({ emails: async ({}, use) => { await use(getEmailsFixture()); }, + sms: async ({}, use) => { + await use(getSMSFixture()); + }, }); function getEmailsFixture() { @@ -18,3 +23,9 @@ function getEmailsFixture() { get: getTestEmails, }; } + +function getSMSFixture() { + return { + get: getTestSMS, + }; +} diff --git a/apps/web/test/utils/bookingScenario/bookingScenario.ts b/apps/web/test/utils/bookingScenario/bookingScenario.ts index de929ff1545504..e202773b13ad53 100644 --- a/apps/web/test/utils/bookingScenario/bookingScenario.ts +++ b/apps/web/test/utils/bookingScenario/bookingScenario.ts @@ -19,7 +19,7 @@ import logger from "@calcom/lib/logger"; import { safeStringify } from "@calcom/lib/safeStringify"; import { ProfileRepository } from "@calcom/lib/server/repository/profile"; import type { WorkflowActions, WorkflowTemplates, WorkflowTriggerEvents } from "@calcom/prisma/client"; -import type { SchedulingType } from "@calcom/prisma/enums"; +import type { SchedulingType, SMSLockState } from "@calcom/prisma/enums"; import type { BookingStatus } from "@calcom/prisma/enums"; import type { teamMetadataSchema } from "@calcom/prisma/zod-utils"; import type { userMetadataType } from "@calcom/prisma/zod-utils"; @@ -952,6 +952,7 @@ export function getOrganizer({ teams, organizationId, metadata, + smsLockState, }: { name: string; email: string; @@ -965,6 +966,7 @@ export function getOrganizer({ weekStart?: WeekDays; teams?: InputUser["teams"]; metadata?: userMetadataType; + smsLockState?: SMSLockState; }) { return { ...TestData.users.example, @@ -981,6 +983,7 @@ export function getOrganizer({ organizationId, profiles: [], metadata, + smsLockState, }; } diff --git a/apps/web/test/utils/bookingScenario/expects.ts b/apps/web/test/utils/bookingScenario/expects.ts index a018ebae9d03e7..c01bc1745ea5b8 100644 --- a/apps/web/test/utils/bookingScenario/expects.ts +++ b/apps/web/test/utils/bookingScenario/expects.ts @@ -307,38 +307,72 @@ export function expectWebhookToHaveBeenCalledWith( export function expectWorkflowToBeTriggered({ emails, - organizer, - destinationEmail, + emailsToReceive, }: { emails: Fixtures["emails"]; - organizer: { email: string; name: string; timeZone: string }; - destinationEmail?: string; + emailsToReceive: string[]; }) { const subjectPattern = /^Reminder: /i; - expect(emails.get()).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - subject: expect.stringMatching(subjectPattern), - to: destinationEmail ?? organizer.email, - }), - ]) - ); + emailsToReceive.forEach((email) => { + expect(emails.get()).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + subject: expect.stringMatching(subjectPattern), + to: email, + }), + ]) + ); + }); } export function expectWorkflowToBeNotTriggered({ emails, - organizer, + emailsToReceive, }: { emails: Fixtures["emails"]; - organizer: { email: string; name: string; timeZone: string }; + emailsToReceive: string[]; }) { const subjectPattern = /^Reminder: /i; - expect(emails.get()).not.toEqual( + emailsToReceive.forEach((email) => { + expect(emails.get()).not.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + subject: expect.stringMatching(subjectPattern), + to: email, + }), + ]) + ); + }); +} + +export function expectSMSWorkflowToBeTriggered({ + sms, + toNumber, +}: { + sms: Fixtures["sms"]; + toNumber: string; +}) { + expect(sms.get()).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + to: toNumber, + }), + ]) + ); +} + +export function expectSMSWorkflowToBeNotTriggered({ + sms, + toNumber, +}: { + sms: Fixtures["sms"]; + toNumber: string; +}) { + expect(sms.get()).not.toEqual( expect.arrayContaining([ expect.objectContaining({ - subject: expect.stringMatching(subjectPattern), - to: organizer.email, + to: toNumber, }), ]) ); diff --git a/apps/web/test/utils/bookingScenario/getMockRequestDataForBooking.ts b/apps/web/test/utils/bookingScenario/getMockRequestDataForBooking.ts index 00bddf9fefc11d..49298a98dffb4a 100644 --- a/apps/web/test/utils/bookingScenario/getMockRequestDataForBooking.ts +++ b/apps/web/test/utils/bookingScenario/getMockRequestDataForBooking.ts @@ -30,6 +30,7 @@ export function getMockRequestDataForBooking({ email: string; name: string; location: { optionValue: ""; value: string }; + smsReminderNumber?: string; }; }; }) { diff --git a/apps/web/test/utils/bookingScenario/test.ts b/apps/web/test/utils/bookingScenario/test.ts index 7a00f894fd8a3d..b3957f982f0615 100644 --- a/apps/web/test/utils/bookingScenario/test.ts +++ b/apps/web/test/utils/bookingScenario/test.ts @@ -15,7 +15,7 @@ const _testWithAndWithoutOrg = ( const t = mode === "only" ? test.only : mode === "skip" ? test.skip : test; t( `${description} - With org`, - async ({ emails, meta, task, onTestFailed, expect, skip }) => { + async ({ emails, sms, meta, task, onTestFailed, expect, skip }) => { const org = await createOrganization({ name: "Test Org", slug: "testorg", @@ -27,6 +27,7 @@ const _testWithAndWithoutOrg = ( onTestFailed, expect, emails, + sms, skip, org: { organization: org, @@ -39,9 +40,10 @@ const _testWithAndWithoutOrg = ( t( `${description}`, - async ({ emails, meta, task, onTestFailed, expect, skip }) => { + async ({ emails, sms, meta, task, onTestFailed, expect, skip }) => { await fn({ emails, + sms, meta, task, onTestFailed, diff --git a/packages/features/bookings/lib/handleNewBooking/test/fresh-booking.test.ts b/packages/features/bookings/lib/handleNewBooking/test/fresh-booking.test.ts index faba12aa50c41f..21e70e2169cbe6 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/fresh-booking.test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/fresh-booking.test.ts @@ -42,6 +42,7 @@ import { import { createMockNextJsRequest } from "@calcom/web/test/utils/bookingScenario/createMockNextJsRequest"; import { expectWorkflowToBeTriggered, + expectWorkflowToBeNotTriggered, expectSuccessfulBookingCreationEmails, expectBookingToBeInDatabase, expectAwaitingPaymentEmails, @@ -51,7 +52,6 @@ import { expectBookingPaymentIntiatedWebhookToHaveBeenFired, expectBrokenIntegrationEmails, expectSuccessfulCalendarEventCreationInCalendar, - expectWorkflowToBeNotTriggered, expectICalUIDAsString, } from "@calcom/web/test/utils/bookingScenario/expects"; import { getMockRequestDataForBooking } from "@calcom/web/test/utils/bookingScenario/getMockRequestDataForBooking"; @@ -215,9 +215,8 @@ describe("handleNewBooking", () => { }); expectWorkflowToBeTriggered({ - organizer, + emailsToReceive: [organizerDestinationCalendarEmailOnEventType], emails, - destinationEmail: organizerDestinationCalendarEmailOnEventType, }); expectSuccessfulCalendarEventCreationInCalendar(calendarMock, { calendarId: "event-type-1@google-calendar.com", @@ -379,7 +378,7 @@ describe("handleNewBooking", () => { iCalUID: createdBooking.iCalUID, }); - expectWorkflowToBeTriggered({ organizer, emails }); + expectWorkflowToBeTriggered({ emailsToReceive: [organizer.email], emails }); expectSuccessfulCalendarEventCreationInCalendar(calendarMock, { videoCallUrl: "http://mock-dailyvideo.example.com/meeting-1", // We won't be sending evt.destinationCalendar in this case. @@ -541,7 +540,7 @@ describe("handleNewBooking", () => { iCalUID: createdBooking.iCalUID, }); - expectWorkflowToBeTriggered({ organizer, emails }); + expectWorkflowToBeTriggered({ emailsToReceive: [organizer.email], emails }); expectSuccessfulCalendarEventCreationInCalendar(calendarMock, { calendarId: "organizer@google-calendar.com", videoCallUrl: "http://mock-dailyvideo.example.com/meeting-1", @@ -675,7 +674,7 @@ describe("handleNewBooking", () => { ], }); - expectWorkflowToBeTriggered({ organizer, emails }); + expectWorkflowToBeTriggered({ emailsToReceive: [organizer.email], emails }); // FIXME: We should send Broken Integration emails on calendar event creation failure // expectCalendarEventCreationFailureEmails({ booker, organizer, emails }); @@ -825,7 +824,7 @@ describe("handleNewBooking", () => { iCalUID: createdBooking.iCalUID, }); - expectWorkflowToBeTriggered({ organizer, emails }); + expectWorkflowToBeTriggered({ emailsToReceive: [organizer.email], emails }); expectSuccessfulCalendarEventCreationInCalendar(calendarMock, { calendarId: "organizer@google-calendar.com", @@ -982,7 +981,7 @@ describe("handleNewBooking", () => { ], }); - expectWorkflowToBeTriggered({ organizer, emails }); + expectWorkflowToBeTriggered({ emailsToReceive: [organizer.email], emails }); expectSuccessfulCalendarEventCreationInCalendar(calendarMock, { calendarId: "organizer@google-calendar.com", videoCallUrl: "http://mock-dailyvideo.example.com/meeting-1", @@ -1511,7 +1510,7 @@ describe("handleNewBooking", () => { status: BookingStatus.PENDING, }); - expectWorkflowToBeNotTriggered({ organizer, emails }); + expectWorkflowToBeNotTriggered({ emailsToReceive: [organizer.email], emails }); expectBookingRequestedEmails({ booker, @@ -1636,7 +1635,7 @@ describe("handleNewBooking", () => { }), }); - expectWorkflowToBeNotTriggered({ organizer, emails }); + expectWorkflowToBeNotTriggered({ emailsToReceive: [organizer.email], emails }); expectBookingRequestedEmails({ booker, @@ -1764,7 +1763,7 @@ describe("handleNewBooking", () => { iCalUID: createdBooking.iCalUID, }); - expectWorkflowToBeTriggered({ organizer, emails }); + expectWorkflowToBeTriggered({ emailsToReceive: [organizer.email], emails }); const iCalUID = expectICalUIDAsString(createdBooking.iCalUID); @@ -1897,7 +1896,7 @@ describe("handleNewBooking", () => { iCalUID: createdBooking.iCalUID, }); - expectWorkflowToBeNotTriggered({ organizer, emails }); + expectWorkflowToBeNotTriggered({ emailsToReceive: [organizer.email], emails }); expectBookingRequestedEmails({ booker, organizer, emails }); @@ -2076,7 +2075,7 @@ describe("handleNewBooking", () => { iCalUID: createdBooking.iCalUID, }); - expectWorkflowToBeTriggered({ organizer, emails }); + expectWorkflowToBeTriggered({ emailsToReceive: [organizer.email], emails }); const iCalUID = expectICalUIDAsString(createdBooking.iCalUID); @@ -2220,7 +2219,7 @@ describe("handleNewBooking", () => { }), }); - expectWorkflowToBeNotTriggered({ organizer, emails }); + expectWorkflowToBeNotTriggered({ emailsToReceive: [organizer.email], emails }); expectAwaitingPaymentEmails({ organizer, booker, emails }); @@ -2244,7 +2243,7 @@ describe("handleNewBooking", () => { status: BookingStatus.ACCEPTED, }); - expectWorkflowToBeTriggered({ organizer, emails }); + expectWorkflowToBeTriggered({ emailsToReceive: [organizer.email], emails }); expectBookingCreatedWebhookToHaveBeenFired({ booker, @@ -2374,7 +2373,7 @@ describe("handleNewBooking", () => { status: BookingStatus.PENDING, }); - expectWorkflowToBeNotTriggered({ organizer, emails }); + expectWorkflowToBeNotTriggered({ emailsToReceive: [organizer.email], emails }); expectAwaitingPaymentEmails({ organizer, booker, emails }); expectBookingPaymentIntiatedWebhookToHaveBeenFired({ diff --git a/packages/features/bookings/lib/handleNewBooking/test/reschedule.test.ts b/packages/features/bookings/lib/handleNewBooking/test/reschedule.test.ts index b942c118b02da4..f332a036824b79 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/reschedule.test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/reschedule.test.ts @@ -240,7 +240,7 @@ describe("handleNewBooking", () => { ], }, }); - expectWorkflowToBeTriggered({ emails, organizer }); + expectWorkflowToBeTriggered({ emailsToReceive: [organizer.email], emails }); expectSuccessfulVideoMeetingUpdationInCalendar(videoMock, { calEvent: { @@ -459,7 +459,7 @@ describe("handleNewBooking", () => { }, }); - expectWorkflowToBeTriggered({ emails, organizer }); + expectWorkflowToBeTriggered({ emailsToReceive: [organizer.email], emails }); expectSuccessfulVideoMeetingUpdationInCalendar(videoMock, { calEvent: { @@ -656,7 +656,7 @@ describe("handleNewBooking", () => { }, }); - expectWorkflowToBeTriggered({ emails, organizer }); + expectWorkflowToBeTriggered({ emailsToReceive: [organizer.email], emails }); // FIXME: We should send Broken Integration emails on calendar event updation failure // expectBrokenIntegrationEmails({ booker, organizer, emails }); @@ -850,7 +850,7 @@ describe("handleNewBooking", () => { }, }); - expectWorkflowToBeTriggered({ emails, organizer }); + expectWorkflowToBeTriggered({ emailsToReceive: [organizer.email], emails }); expectBookingRequestedEmails({ booker, @@ -1085,7 +1085,7 @@ describe("handleNewBooking", () => { }, }); - expectWorkflowToBeTriggered({ emails, organizer }); + expectWorkflowToBeTriggered({ emailsToReceive: [organizer.email], emails }); expectSuccessfulVideoMeetingUpdationInCalendar(videoMock, { calEvent: { @@ -1312,7 +1312,7 @@ describe("handleNewBooking", () => { }, }); - //expectWorkflowToBeTriggered({emails, organizer}); + //expectWorkflowToBeTriggered({ emailsToReceive: [organizer.email], emails }); expectBookingRequestedEmails({ booker, @@ -1559,7 +1559,7 @@ describe("handleNewBooking", () => { }, }); - expectWorkflowToBeTriggered({ emails, organizer }); + expectWorkflowToBeTriggered({ emailsToReceive: [organizer.email], emails }); expectSuccessfulVideoMeetingUpdationInCalendar(videoMock, { calEvent: { diff --git a/packages/features/bookings/lib/handleNewBooking/test/workflow-notifications.test.ts b/packages/features/bookings/lib/handleNewBooking/test/workflow-notifications.test.ts new file mode 100644 index 00000000000000..b11ca02128e4c8 --- /dev/null +++ b/packages/features/bookings/lib/handleNewBooking/test/workflow-notifications.test.ts @@ -0,0 +1,541 @@ +import { describe, beforeEach } from "vitest"; + +import { resetTestSMS } from "@calcom/lib/testSMS"; +import { SMSLockState, SchedulingType } from "@calcom/prisma/enums"; +import { test } from "@calcom/web/test/fixtures/fixtures"; +import { + createBookingScenario, + getGoogleCalendarCredential, + TestData, + getOrganizer, + getBooker, + getScenarioData, + mockSuccessfulVideoMeetingCreation, + mockCalendarToHaveNoBusySlots, + BookingLocations, + getDate, + Timezones, +} from "@calcom/web/test/utils/bookingScenario/bookingScenario"; +import { createMockNextJsRequest } from "@calcom/web/test/utils/bookingScenario/createMockNextJsRequest"; +import { + expectWorkflowToBeTriggered, + expectSMSWorkflowToBeTriggered, + expectSMSWorkflowToBeNotTriggered, +} from "@calcom/web/test/utils/bookingScenario/expects"; +import { getMockRequestDataForBooking } from "@calcom/web/test/utils/bookingScenario/getMockRequestDataForBooking"; +import { setupAndTeardown } from "@calcom/web/test/utils/bookingScenario/setupAndTeardown"; + +// Local test runs sometime gets too slow +const timeout = process.env.CI ? 5000 : 20000; + +describe("handleNewBooking", () => { + setupAndTeardown(); + + beforeEach(() => { + resetTestSMS(); + }); + + describe("User Workflows", () => { + test( + "should send workflow email and sms when booking is created", + async ({ emails, sms }) => { + const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default; + const booker = getBooker({ + email: "booker@example.com", + name: "Booker", + }); + + const organizerOtherEmail = "organizer2@example.com"; + const organizerDestinationCalendarEmailOnEventType = "organizerEventTypeEmail@example.com"; + + const organizer = getOrganizer({ + name: "Organizer", + email: "organizer@example.com", + id: 101, + schedules: [TestData.schedules.IstWorkHours], + credentials: [getGoogleCalendarCredential()], + selectedCalendars: [TestData.selectedCalendars.google], + destinationCalendar: { + integration: "google_calendar", + externalId: "organizer@google-calendar.com", + primaryEmail: organizerOtherEmail, + }, + }); + + await createBookingScenario( + getScenarioData({ + workflows: [ + { + userId: organizer.id, + trigger: "NEW_EVENT", + action: "EMAIL_HOST", + template: "REMINDER", + activeEventTypeId: 1, + }, + { + userId: organizer.id, + trigger: "NEW_EVENT", + action: "SMS_ATTENDEE", + template: "REMINDER", + activeEventTypeId: 1, + }, + ], + eventTypes: [ + { + id: 1, + slotInterval: 30, + length: 30, + useEventTypeDestinationCalendarEmail: true, + users: [ + { + id: 101, + }, + ], + destinationCalendar: { + integration: "google_calendar", + externalId: "event-type-1@google-calendar.com", + primaryEmail: organizerDestinationCalendarEmailOnEventType, + }, + }, + ], + organizer, + apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]], + }) + ); + + mockSuccessfulVideoMeetingCreation({ + metadataLookupKey: "dailyvideo", + videoMeetingData: { + id: "MOCK_ID", + password: "MOCK_PASS", + url: `http://mock-dailyvideo.example.com/meeting-1`, + }, + }); + + mockCalendarToHaveNoBusySlots("googlecalendar", { + create: { + id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", + }, + }); + + const mockBookingData = getMockRequestDataForBooking({ + data: { + user: organizer.username, + eventTypeId: 1, + responses: { + email: booker.email, + name: booker.name, + location: { optionValue: "", value: BookingLocations.CalVideo }, + smsReminderNumber: "000", + }, + }, + }); + + const { req } = createMockNextJsRequest({ + method: "POST", + body: mockBookingData, + }); + + await handleNewBooking(req); + + expectSMSWorkflowToBeTriggered({ + sms, + toNumber: "000", + }); + + expectWorkflowToBeTriggered({ + emailsToReceive: [organizerDestinationCalendarEmailOnEventType], + emails, + }); + }, + timeout + ); + test( + "should not send workflow sms when booking is created if the organizer is locked for sms sending", + async ({ sms }) => { + const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default; + const booker = getBooker({ + email: "booker@example.com", + name: "Booker", + }); + + const organizerOtherEmail = "organizer2@example.com"; + const organizerDestinationCalendarEmailOnEventType = "organizerEventTypeEmail@example.com"; + + const organizer = getOrganizer({ + name: "Organizer", + email: "organizer@example.com", + id: 101, + schedules: [TestData.schedules.IstWorkHours], + credentials: [getGoogleCalendarCredential()], + selectedCalendars: [TestData.selectedCalendars.google], + destinationCalendar: { + integration: "google_calendar", + externalId: "organizer@google-calendar.com", + primaryEmail: organizerOtherEmail, + }, + smsLockState: SMSLockState.LOCKED, + }); + + await createBookingScenario( + getScenarioData({ + workflows: [ + { + userId: organizer.id, + trigger: "NEW_EVENT", + action: "SMS_ATTENDEE", + template: "REMINDER", + activeEventTypeId: 1, + }, + ], + eventTypes: [ + { + id: 1, + slotInterval: 30, + length: 30, + useEventTypeDestinationCalendarEmail: true, + users: [ + { + id: 101, + }, + ], + destinationCalendar: { + integration: "google_calendar", + externalId: "event-type-1@google-calendar.com", + primaryEmail: organizerDestinationCalendarEmailOnEventType, + }, + }, + ], + organizer, + apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]], + }) + ); + + mockCalendarToHaveNoBusySlots("googlecalendar", { + create: { + id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", + }, + }); + + const mockBookingData = getMockRequestDataForBooking({ + data: { + user: organizer.username, + eventTypeId: 1, + responses: { + email: booker.email, + name: booker.name, + location: { optionValue: "", value: BookingLocations.CalVideo }, + smsReminderNumber: "000", + }, + }, + }); + + const { req } = createMockNextJsRequest({ + method: "POST", + body: mockBookingData, + }); + + await handleNewBooking(req); + + expectSMSWorkflowToBeNotTriggered({ + sms, + toNumber: "000", + }); + }, + timeout + ); + }); + describe("Team Workflows", () => { + test( + "should send workflow email and sms when booking is created", + async ({ emails, sms }) => { + const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default; + const booker = getBooker({ + email: "booker@example.com", + name: "Booker", + }); + + const organizerDestinationCalendarEmailOnEventType = "organizerEventTypeEmail@example.com"; + + const organizer = getOrganizer({ + name: "Organizer", + email: "organizer@example.com", + id: 101, + schedules: [TestData.schedules.IstWorkHours], + credentials: [getGoogleCalendarCredential()], + selectedCalendars: [TestData.selectedCalendars.google], + destinationCalendar: { + integration: "google_calendar", + externalId: "organizer@google-calendar.com", + primaryEmail: organizerDestinationCalendarEmailOnEventType, + }, + teams: [ + { + membership: { + accepted: true, + }, + team: { + id: 1, + name: "Team 1", + slug: "team-1", + }, + }, + ], + }); + + const otherTeamMembers = [ + { + name: "Other Team Member 1", + username: "other-team-member-1", + defaultScheduleId: null, + email: "other-team-member-1@example.com", + timeZone: Timezones["+0:00"], + id: 102, + schedules: [TestData.schedules.IstWorkHours], + credentials: [getGoogleCalendarCredential()], + selectedCalendars: [TestData.selectedCalendars.google], + destinationCalendar: { + integration: TestData.apps["google-calendar"].type, + externalId: "other-team-member-1@google-calendar.com", + }, + }, + ]; + + await createBookingScenario( + getScenarioData({ + workflows: [ + { + teamId: 1, + trigger: "NEW_EVENT", + action: "EMAIL_HOST", + template: "REMINDER", + activeEventTypeId: 1, + }, + { + teamId: 1, + trigger: "NEW_EVENT", + action: "SMS_ATTENDEE", + template: "REMINDER", + activeEventTypeId: 1, + }, + ], + eventTypes: [ + { + id: 1, + slotInterval: 15, + schedulingType: SchedulingType.COLLECTIVE, + length: 15, + users: [ + { + id: 101, + }, + { + id: 102, + }, + ], + teamId: 1, + destinationCalendar: { + integration: TestData.apps["google-calendar"].type, + externalId: "event-type-1@google-calendar.com", + }, + }, + ], + organizer, + usersApartFromOrganizer: otherTeamMembers, + apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]], + }) + ); + + mockSuccessfulVideoMeetingCreation({ + metadataLookupKey: "dailyvideo", + videoMeetingData: { + id: "MOCK_ID", + password: "MOCK_PASS", + url: `http://mock-dailyvideo.example.com/meeting-1`, + }, + }); + + mockCalendarToHaveNoBusySlots("googlecalendar", { + create: { + id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", + iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID", + }, + }); + + const mockBookingData = getMockRequestDataForBooking({ + data: { + start: `${getDate({ dateIncrement: 1 }).dateString}T09:00:00.000Z`, + end: `${getDate({ dateIncrement: 1 }).dateString}T09:15:00.000Z`, + user: organizer.username, + eventTypeId: 1, + responses: { + email: booker.email, + name: booker.name, + location: { optionValue: "", value: BookingLocations.CalVideo }, + smsReminderNumber: "000", + }, + }, + }); + + const { req } = createMockNextJsRequest({ + method: "POST", + body: mockBookingData, + }); + + await handleNewBooking(req); + + expectSMSWorkflowToBeTriggered({ + sms, + toNumber: "000", + }); + + expectWorkflowToBeTriggered({ + // emailsToReceive: [organizer.email].concat(otherTeamMembers.map(member => member.email)), + emailsToReceive: [organizer.email], + emails, + }); + }, + timeout + ); + + test( + "should not send workflow sms when booking is created if the team is locked for sms sending", + async ({ emails, sms }) => { + const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default; + const booker = getBooker({ + email: "booker@example.com", + name: "Booker", + }); + + const organizerDestinationCalendarEmailOnEventType = "organizerEventTypeEmail@example.com"; + + const organizer = getOrganizer({ + name: "Organizer", + email: "organizer@example.com", + id: 101, + schedules: [TestData.schedules.IstWorkHours], + credentials: [getGoogleCalendarCredential()], + selectedCalendars: [TestData.selectedCalendars.google], + destinationCalendar: { + integration: "google_calendar", + externalId: "organizer@google-calendar.com", + primaryEmail: organizerDestinationCalendarEmailOnEventType, + }, + teams: [ + { + membership: { + accepted: true, + }, + team: { + id: 1, + name: "Team 1", + slug: "team-1", + smsLockState: SMSLockState.LOCKED, + }, + }, + ], + }); + + const otherTeamMembers = [ + { + name: "Other Team Member 1", + username: "other-team-member-1", + defaultScheduleId: null, + email: "other-team-member-1@example.com", + timeZone: Timezones["+0:00"], + id: 102, + schedules: [TestData.schedules.IstWorkHours], + credentials: [getGoogleCalendarCredential()], + selectedCalendars: [TestData.selectedCalendars.google], + destinationCalendar: { + integration: TestData.apps["google-calendar"].type, + externalId: "other-team-member-1@google-calendar.com", + }, + }, + ]; + + await createBookingScenario( + getScenarioData({ + workflows: [ + { + teamId: 1, + trigger: "NEW_EVENT", + action: "SMS_ATTENDEE", + template: "REMINDER", + activeEventTypeId: 1, + }, + ], + eventTypes: [ + { + id: 1, + slotInterval: 15, + schedulingType: SchedulingType.COLLECTIVE, + length: 15, + users: [ + { + id: 101, + }, + { + id: 102, + }, + ], + teamId: 1, + destinationCalendar: { + integration: TestData.apps["google-calendar"].type, + externalId: "event-type-1@google-calendar.com", + }, + }, + ], + organizer, + usersApartFromOrganizer: otherTeamMembers, + apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]], + }) + ); + + mockSuccessfulVideoMeetingCreation({ + metadataLookupKey: "dailyvideo", + videoMeetingData: { + id: "MOCK_ID", + password: "MOCK_PASS", + url: `http://mock-dailyvideo.example.com/meeting-1`, + }, + }); + + mockCalendarToHaveNoBusySlots("googlecalendar", { + create: { + id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", + iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID", + }, + }); + + const mockBookingData = getMockRequestDataForBooking({ + data: { + start: `${getDate({ dateIncrement: 1 }).dateString}T09:00:00.000Z`, + end: `${getDate({ dateIncrement: 1 }).dateString}T09:15:00.000Z`, + user: organizer.username, + eventTypeId: 1, + responses: { + email: booker.email, + name: booker.name, + location: { optionValue: "", value: BookingLocations.CalVideo }, + smsReminderNumber: "000", + }, + }, + }); + + const { req } = createMockNextJsRequest({ + method: "POST", + body: mockBookingData, + }); + + await handleNewBooking(req); + + expectSMSWorkflowToBeNotTriggered({ + sms, + toNumber: "000", + }); + }, + timeout + ); + }); +}); diff --git a/packages/features/ee/workflows/lib/reminders/providers/twilioProvider.ts b/packages/features/ee/workflows/lib/reminders/providers/twilioProvider.ts index 1f136929dd2fe3..bcd466e4da6dc2 100644 --- a/packages/features/ee/workflows/lib/reminders/providers/twilioProvider.ts +++ b/packages/features/ee/workflows/lib/reminders/providers/twilioProvider.ts @@ -2,6 +2,7 @@ import TwilioClient from "twilio"; import { checkSMSRateLimit } from "@calcom/lib/checkRateLimitAndThrowError"; import logger from "@calcom/lib/logger"; +import { setTestSMS } from "@calcom/lib/testSMS"; import prisma from "@calcom/prisma"; import { SMSLockState } from "@calcom/prisma/enums"; @@ -31,7 +32,7 @@ function getDefaultSender(whatsapp = false) { if (whatsapp) { defaultSender = `whatsapp:+${process.env.TWILIO_WHATSAPP_PHONE_NUMBER}`; } - return defaultSender; + return defaultSender || ""; } function getSMSNumber(phone: string, whatsapp = false) { @@ -46,8 +47,6 @@ export const sendSMS = async ( teamId?: number | null, whatsapp = false ) => { - assertTwilio(twilio); - const isSMSSendingLocked = await isLockedForSMSSending(userId, teamId); if (isSMSSendingLocked) { @@ -55,6 +54,23 @@ export const sendSMS = async ( return; } + const testMode = process.env.NEXT_PUBLIC_IS_E2E || process.env.INTEGRATION_TEST_MODE; + + if (testMode) { + setTestSMS({ + to: getSMSNumber(phoneNumber, whatsapp), + from: whatsapp ? getDefaultSender(whatsapp) : sender ? sender : getDefaultSender(), + message: body, + }); + console.log( + "Skipped sending SMS because process.env.NEXT_PUBLIC_IS_E2E or process.env.INTEGRATION_TEST_MODE is set. SMS are available in globalThis.testSMS" + ); + + return; + } + + assertTwilio(twilio); + if (!teamId && userId) { await checkSMSRateLimit({ identifier: `sms:user:${userId}`, diff --git a/packages/lib/testSMS.ts b/packages/lib/testSMS.ts new file mode 100644 index 00000000000000..60952a606d40e7 --- /dev/null +++ b/packages/lib/testSMS.ts @@ -0,0 +1,21 @@ +declare global { + // eslint-disable-next-line no-var + var testSMS: { + to: string; + from: string; + message: string; + }[]; +} + +export const setTestSMS = (sms: (typeof globalThis.testSMS)[number]) => { + globalThis.testSMS = globalThis.testSMS || []; + globalThis.testSMS.push(sms); +}; + +export const getTestSMS = () => { + return globalThis.testSMS; +}; + +export const resetTestSMS = () => { + globalThis.testSMS = []; +}; From 5bafd1b2643f02a91f2a1429474763708575fe9d Mon Sep 17 00:00:00 2001 From: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> Date: Thu, 2 May 2024 10:44:08 -0400 Subject: [PATCH 2/8] chore: Allow disabling attendee emails if `SMS_ATTENDEE` is a part of the workflow (#14806) * Allow disabling confirmation emails to attendees if workflow contains SMS * Update copy --- apps/web/public/static/locales/en/common.json | 2 +- .../ee/workflows/lib/allowDisablingStandardEmails.ts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index c29411c7569c38..d03edc83551437 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -2022,7 +2022,7 @@ "invite_as": "Invite as", "form_updated_successfully": "Form updated successfully.", "disable_attendees_confirmation_emails": "Disable default confirmation emails for attendees", - "disable_attendees_confirmation_emails_description": "At least one workflow is active on this event type that sends an email to the attendees when the event is booked.", + "disable_attendees_confirmation_emails_description": "At least one workflow is active on this event type that sends a booking confirmation to the attendees.", "disable_host_confirmation_emails": "Disable default confirmation emails for host", "disable_host_confirmation_emails_description": "At least one workflow is active on this event type that sends an email to the host when the event is booked.", "add_an_override": "Add an override", diff --git a/packages/features/ee/workflows/lib/allowDisablingStandardEmails.ts b/packages/features/ee/workflows/lib/allowDisablingStandardEmails.ts index 5f9d8615d3fa78..a1a22779553b7a 100644 --- a/packages/features/ee/workflows/lib/allowDisablingStandardEmails.ts +++ b/packages/features/ee/workflows/lib/allowDisablingStandardEmails.ts @@ -22,6 +22,9 @@ export function allowDisablingAttendeeConfirmationEmails( return !!workflows.find( (workflow) => workflow.trigger === WorkflowTriggerEvents.NEW_EVENT && - !!workflow.steps.find((step) => step.action === WorkflowActions.EMAIL_ATTENDEE) + !!workflow.steps.find( + (step) => + step.action === WorkflowActions.EMAIL_ATTENDEE || step.action === WorkflowActions.SMS_ATTENDEE + ) ); } From 676eac357ff3c2f0335267dcafebee0a6d6f8ac1 Mon Sep 17 00:00:00 2001 From: Peer Richelsen Date: Thu, 2 May 2024 17:14:13 +0200 Subject: [PATCH 3/8] chore: fixed enterprise warning and description (#14791) * fixed enterprise warning and description * chore: remove fullstop --------- Co-authored-by: Udit Takkar --- apps/web/public/static/locales/ar/common.json | 2 -- apps/web/public/static/locales/cs/common.json | 2 -- apps/web/public/static/locales/de/common.json | 2 -- apps/web/public/static/locales/en/common.json | 4 +-- apps/web/public/static/locales/es/common.json | 2 -- apps/web/public/static/locales/fr/common.json | 2 -- apps/web/public/static/locales/he/common.json | 2 -- apps/web/public/static/locales/hu/common.json | 2 -- apps/web/public/static/locales/it/common.json | 2 -- apps/web/public/static/locales/ja/common.json | 2 -- apps/web/public/static/locales/ko/common.json | 2 -- apps/web/public/static/locales/nl/common.json | 2 -- apps/web/public/static/locales/pl/common.json | 2 -- .../public/static/locales/pt-BR/common.json | 2 -- apps/web/public/static/locales/pt/common.json | 2 -- apps/web/public/static/locales/ro/common.json | 2 -- apps/web/public/static/locales/ru/common.json | 2 -- apps/web/public/static/locales/sr/common.json | 2 -- apps/web/public/static/locales/sv/common.json | 2 -- apps/web/public/static/locales/tr/common.json | 2 -- apps/web/public/static/locales/uk/common.json | 2 -- apps/web/public/static/locales/vi/common.json | 2 -- .../public/static/locales/zh-CN/common.json | 2 -- .../public/static/locales/zh-TW/common.json | 2 -- .../ee/common/components/LicenseRequired.tsx | 33 +++++-------------- 25 files changed, 11 insertions(+), 72 deletions(-) diff --git a/apps/web/public/static/locales/ar/common.json b/apps/web/public/static/locales/ar/common.json index baaa0e288b64c0..2680e5332ee083 100644 --- a/apps/web/public/static/locales/ar/common.json +++ b/apps/web/public/static/locales/ar/common.json @@ -937,8 +937,6 @@ "verify_wallet": "تأكيد المحفظة", "create_events_on": "إنشاء أحداث في", "enterprise_license": "هذه هي ميزة للمؤسسات", - "enterprise_license_description": "لتمكين هذه الميزة، احصل على مفتاح نشر في وحدة التحكم {{consoleUrl}} وأضفه إلى وحدة التحكم الخاصة بك. nv باسم CALCOM_LICENSE_KEY. إذا كان لدى فريقك بالفعل ترخيص، يرجى الاتصال بـ {{supportMail}} للحصول على المساعدة.", - "enterprise_license_development": "يمكنك اختبار هذه الميزة في وضع التطوير. لاستخدام الإنتاج، يرجى مطالبة المسؤول بالذهاب إلى <2>/auth/setup لإدخال مفتاح الترخيص.", "missing_license": "الترخيص مفقود", "next_steps": "الخطوات التالية", "acquire_commercial_license": "الحصول على ترخيص تجاري", diff --git a/apps/web/public/static/locales/cs/common.json b/apps/web/public/static/locales/cs/common.json index 49e3a6ac267528..54660ce456524f 100644 --- a/apps/web/public/static/locales/cs/common.json +++ b/apps/web/public/static/locales/cs/common.json @@ -937,8 +937,6 @@ "verify_wallet": "Ověřit peněženku", "create_events_on": "Vytvořit události v:", "enterprise_license": "Jedná se o firemní funkci", - "enterprise_license_description": "Pokud chcete povolit tuto funkci, získejte klíč pro nasazení v konzoli {{consoleUrl}} a přidejte ho do souboru .env jako CALCOM_LICENSE_KEY. Pokud váš tým již licenci má, kontaktujte prosím {{supportMail}} a požádejte o pomoc.", - "enterprise_license_development": "Tuto funkci můžete vyzkoušet v režimu vývoje. Produkční použití vyžaduje od správce zadání licenčního klíče v <2>/auth/setup.", "missing_license": "Chybějící licence", "next_steps": "Další kroky", "acquire_commercial_license": "Získat komerční licenci", diff --git a/apps/web/public/static/locales/de/common.json b/apps/web/public/static/locales/de/common.json index 1469dc42d69129..7a50054c5fb9b2 100644 --- a/apps/web/public/static/locales/de/common.json +++ b/apps/web/public/static/locales/de/common.json @@ -980,8 +980,6 @@ "verify_wallet": "Wallet verifizieren", "create_events_on": "Erstelle Termine in:", "enterprise_license": "Das ist eine Enterprise-Funktion", - "enterprise_license_description": "Um diese Funktion zu aktivieren, holen Sie sich einen Deployment-Schlüssel von der {{consoleUrl}}-Konsole und fügen Sie ihn als CALCOM_LICENSE_KEY zu Ihrer .env hinzu. Wenn Ihr Team bereits eine Lizenz hat, wenden Sie sich bitte an {{supportMail}} für Hilfe.", - "enterprise_license_development": "Sie können diese Funktion im Entwicklungsmodus testen. Zur Nutzung im regulären Arbeitsumfeld, besuchen Sie bitte <2>/auth/setup, um einen Lizenzschlüssel einzugeben.", "missing_license": "Lizenz fehlt", "next_steps": "Nächste Schritte", "acquire_commercial_license": "Eine kommerzielle Lizenz erwerben", diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index d03edc83551437..0a52f6d9c53713 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -994,8 +994,8 @@ "verify_wallet": "Verify Wallet", "create_events_on": "Create events on", "enterprise_license": "This is an enterprise feature", - "enterprise_license_description": "To enable this feature, have an administrator go to <2>/auth/setup to enter a license key. If a license key is already in place, please contact <5>{{SUPPORT_MAIL_ADDRESS}} for help.", - "enterprise_license_development": "You can test this feature on development mode. For production usage please have an administrator go to <2>/auth/setup to enter a license key.", + "enterprise_license_locally": "You can test this feature locally but not on production.", + "enterprise_license_sales": "To upgrade to the enterprise edition, please reach out to our sales team. If a license key is already in place, please contact support@cal.com for help.", "missing_license": "Missing License", "next_steps": "Next Steps", "acquire_commercial_license": "Acquire a commercial license", diff --git a/apps/web/public/static/locales/es/common.json b/apps/web/public/static/locales/es/common.json index 434d8f3210b360..1edbc4bab5b2a2 100644 --- a/apps/web/public/static/locales/es/common.json +++ b/apps/web/public/static/locales/es/common.json @@ -936,8 +936,6 @@ "verify_wallet": "Verificar billetera", "create_events_on": "Crear eventos en", "enterprise_license": "Esta es una función empresarial", - "enterprise_license_description": "Para habilitar esta función, obtenga una clave de despliegue en la consola {{consoleUrl}} y añádala a su .env como CALCOM_LICENSE_KEY. Si su equipo ya tiene una licencia, póngase en contacto con {{supportMail}} para obtener ayuda.", - "enterprise_license_development": "Puede probar esta función en el modo de desarrollo. Para el uso de producción, haga que un administrador vaya a <2>/auth/setup para ingresar una clave de licencia.", "missing_license": "Falta la licencia", "next_steps": "Pasos siguientes", "acquire_commercial_license": "Adquirir una licencia comercial", diff --git a/apps/web/public/static/locales/fr/common.json b/apps/web/public/static/locales/fr/common.json index f0d55296e8b6bb..900ccf4627e44f 100644 --- a/apps/web/public/static/locales/fr/common.json +++ b/apps/web/public/static/locales/fr/common.json @@ -983,8 +983,6 @@ "verify_wallet": "Vérifier le portefeuille", "create_events_on": "Créer des événements dans :", "enterprise_license": "Il s'agit d'une fonctionnalité d'entreprise", - "enterprise_license_description": "Pour activer cette fonctionnalité, demandez à un administrateur d'accéder à <2>/auth/setup pour saisir une clé de licence. Si une clé de licence est déjà en place, veuillez contacter <5>{{SUPPORT_MAIL_ADDRESS}} pour obtenir de l'aide.", - "enterprise_license_development": "Vous pouvez tester cette fonctionnalité en mode développement. Pour une utilisation en production, veuillez demander à un administrateur d'aller à <2>/auth/setup pour entrer une clé de licence.", "missing_license": "Licence manquante", "next_steps": "Prochaines étapes", "acquire_commercial_license": "Obtenir une licence commerciale", diff --git a/apps/web/public/static/locales/he/common.json b/apps/web/public/static/locales/he/common.json index 34e36b2554bd56..63c61ed68e0ccd 100644 --- a/apps/web/public/static/locales/he/common.json +++ b/apps/web/public/static/locales/he/common.json @@ -988,8 +988,6 @@ "verify_wallet": "אימות הארנק", "create_events_on": "ליצור אירועים ב-", "enterprise_license": "תכונה זו זמינה רק במינוי Enterprise", - "enterprise_license_description": "כדי להפעיל את יכולת זו, בקש ממנהל לגשת אל הקישור {{setupUrl}} והקלדת הרישיון. אם כבר יש רישיון מוגדר, צור קשר עם {{supportMail}} לעזרה.", - "enterprise_license_development": "ניתן לבדוק את התכונה הזו במצב פיתוח. לשימוש מצב הייצור, מנהל/ת מערכת צריך/ה לעבור אל <2>/auth/setup ולהזין מפתח רישיון.", "missing_license": "חסר רישיון", "next_steps": "השלבים הבאים", "acquire_commercial_license": "רכישת רישיון לשימוש מסחרי", diff --git a/apps/web/public/static/locales/hu/common.json b/apps/web/public/static/locales/hu/common.json index 1ca6905192cc29..4f6abf04c6fb1a 100644 --- a/apps/web/public/static/locales/hu/common.json +++ b/apps/web/public/static/locales/hu/common.json @@ -989,8 +989,6 @@ "verify_wallet": "A Tárca ellenőrzése", "create_events_on": "Események létrehozása", "enterprise_license": "Ez egy vállalati szolgáltatás", - "enterprise_license_description": "A funkció engedélyezéséhez kérjen meg egy rendszergazdát, hogy lépjen a <2>/auth/setup oldalra, és adja meg a licenckulcsot. Ha már van licenckulcs, segítségért forduljon a következőhöz: <5>{{SUPPORT_MAIL_ADDRESS}}.", - "enterprise_license_development": "Ezt a funkciót fejlesztési módban tesztelheti. Éles használathoz kérjen meg egy rendszergazdát, hogy lépjen be a <2>/auth/setup oldalra, és adja meg a licenckulcsot.", "missing_license": "Hiányzó licenc", "next_steps": "Következő lépések", "acquire_commercial_license": "Szerezzen kereskedelmi licenc-et", diff --git a/apps/web/public/static/locales/it/common.json b/apps/web/public/static/locales/it/common.json index 2aa97abf2a963a..13321d2f07f0ea 100644 --- a/apps/web/public/static/locales/it/common.json +++ b/apps/web/public/static/locales/it/common.json @@ -980,8 +980,6 @@ "verify_wallet": "Verifica Wallet", "create_events_on": "Crea eventi su:", "enterprise_license": "Questa è una funzione Enterprise", - "enterprise_license_description": "Per abilitare questa funzione, ottenere una chiave di distribuzione presso la console {{consoleUrl}} e aggiungerla al proprio .env come CALCOM_LICENSE_KEY. Nel caso il team possieda già una licenza, contattare {{supportMail}} per assistenza.", - "enterprise_license_development": "Puoi testare questa funzionalità in modalità sviluppo. Per l'utilizzo in produzione, l'amministratore deve accedere a <2>/auth/setup e immettere la chiave di licenza.", "missing_license": "Licenza mancante", "next_steps": "Prossimi Passi", "acquire_commercial_license": "Acquista una licenza commerciale", diff --git a/apps/web/public/static/locales/ja/common.json b/apps/web/public/static/locales/ja/common.json index 3b33dba36929ab..3ec64761458f3f 100644 --- a/apps/web/public/static/locales/ja/common.json +++ b/apps/web/public/static/locales/ja/common.json @@ -937,8 +937,6 @@ "verify_wallet": "ウォレットを確認する", "create_events_on": "以下にイベントを作成する:", "enterprise_license": "これは企業向けの機能です", - "enterprise_license_description": "この機能を有効にするには、{{consoleUrl}} コンソールでデプロイメントキーを入手して .env に CALCOM_LICENSE_KEY として追加してください。既にチームがライセンスを持っている場合は {{supportMail}} にお問い合わせください。", - "enterprise_license_development": "この機能は開発モードでテストできます。本番環境で使用するには、管理者に <2>/auth/setup にアクセスするよう依頼し、ライセンスキーを入力してもらってください。", "missing_license": "ライセンスが見つかりません", "next_steps": "次のステップ", "acquire_commercial_license": "商用ライセンスを取得する", diff --git a/apps/web/public/static/locales/ko/common.json b/apps/web/public/static/locales/ko/common.json index 6bb5439ed7c8c3..6ae0211c1e927d 100644 --- a/apps/web/public/static/locales/ko/common.json +++ b/apps/web/public/static/locales/ko/common.json @@ -937,8 +937,6 @@ "verify_wallet": "지갑 인증", "create_events_on": "이벤트 생성일:", "enterprise_license": "엔터프라이즈 기능입니다", - "enterprise_license_description": "이 기능을 활성화하려면 {{consoleUrl}} 콘솔에서 배포 키를 가져와 .env에 CALCOM_LICENSE_KEY로 추가하세요. 팀에 이미 라이선스가 있는 경우 {{supportMail}}에 문의하여 도움을 받으세요.", - "enterprise_license_development": "개발 모드에서 이 기능을 테스트할 수 있습니다. 프로덕션 용도의 경우 관리자가 <2>/auth/setup으로 이동하여 라이선스 키를 입력하도록 하십시오.", "missing_license": "라이선스 없음", "next_steps": "다음 단계", "acquire_commercial_license": "상용 라이선스 취득하기", diff --git a/apps/web/public/static/locales/nl/common.json b/apps/web/public/static/locales/nl/common.json index 1fef5925ecf4c9..193511b4b9a1ba 100644 --- a/apps/web/public/static/locales/nl/common.json +++ b/apps/web/public/static/locales/nl/common.json @@ -937,8 +937,6 @@ "verify_wallet": "Wallet verifiëren", "create_events_on": "Maak gebeurtenissen aan in de", "enterprise_license": "Dit is een bedrijfsfunctie", - "enterprise_license_description": "Om deze functie in te schakelen, krijgt u een implementatiesleutel op de {{consoleUrl}}-console en voegt u deze toe aan uw .env als CALCOM_LICENSE_KEY. Als uw team al een licentie heeft, neem dan contact op met {{supportMail}} voor hulp.", - "enterprise_license_development": "U kunt deze functie testen in de in ontwikkelingsmodus. Voor productiegebruik moet een beheerder naar <2>/auth/setup gaan om een licentiecode in te voeren.", "missing_license": "Ontbrekende licentie", "next_steps": "Volgende stappen", "acquire_commercial_license": "Verkrijg een commerciële licentie", diff --git a/apps/web/public/static/locales/pl/common.json b/apps/web/public/static/locales/pl/common.json index ce7781022988aa..42d53fdf8b6c27 100644 --- a/apps/web/public/static/locales/pl/common.json +++ b/apps/web/public/static/locales/pl/common.json @@ -937,8 +937,6 @@ "verify_wallet": "Zweryfikuj portfel", "create_events_on": "Utwórz wydarzenia w", "enterprise_license": "To funkcja dla przedsiębiorstw", - "enterprise_license_description": "Aby włączyć tę funkcję, uzyskaj klucz wdrożenia na konsoli {{consoleUrl}} i dodaj go do swojego pliku .env jako CALCOM_LICENSE_KEY. Jeśli Twój zespół ma już licencję, napisz na adres {{supportMail}}, aby uzyskać pomoc.", - "enterprise_license_development": "Możesz przetestować tę funkcję w trybie deweloperskim. Aby wykorzystać ją w celach produkcyjnych, poproś administratora o wprowadzenie klucza licencyjnego w obszarze <2>/auth/setup.", "missing_license": "Brakująca licencja", "next_steps": "Następne kroki", "acquire_commercial_license": "Uzyskaj licencję komercyjną", diff --git a/apps/web/public/static/locales/pt-BR/common.json b/apps/web/public/static/locales/pt-BR/common.json index ca07ee86f0fa97..a377e2399a9320 100644 --- a/apps/web/public/static/locales/pt-BR/common.json +++ b/apps/web/public/static/locales/pt-BR/common.json @@ -981,8 +981,6 @@ "verify_wallet": "Verificar carteira", "create_events_on": "Criar eventos em", "enterprise_license": "Este não é um recurso corporativo", - "enterprise_license_description": "Para ativar este recurso, obtenha uma chave de desenvolvimento no console {{consoleUrl}} e adicione ao seu .env como CALCOM_LICENSE_KEY. Caso sua equipe já tenha uma licença, entre em contato com {{supportMail}} para obter ajuda.", - "enterprise_license_development": "Você pode testar este recurso no modo de desenvolvimento. Para uso em produção, solicite que um administrador acesse <2>/auth/setup e insira uma chave de licença.", "missing_license": "Falta a licença", "next_steps": "Próximos passos", "acquire_commercial_license": "Adquira uma licença comercial", diff --git a/apps/web/public/static/locales/pt/common.json b/apps/web/public/static/locales/pt/common.json index 50360ad11b011f..55dca37f91f187 100644 --- a/apps/web/public/static/locales/pt/common.json +++ b/apps/web/public/static/locales/pt/common.json @@ -980,8 +980,6 @@ "verify_wallet": "Verificar carteira", "create_events_on": "Criar eventos em:", "enterprise_license": "Esta é uma funcionalidade empresarial", - "enterprise_license_description": "Para ativar esta funcionalidade, obtenha uma chave de instalação na consola {{consoleUrl}} e adicione-a ao seu .env como CALCOM_LICENSE_KEY. Se a sua equipa já tem uma licença, entre em contacto com {{supportMail}} para obter ajuda.", - "enterprise_license_development": "Pode testar esta funcionalidade no modo de desenvolvimento. Para utilização em produção, um administrador deverá aceder a <2>/auth/setup para inserir a chave da licença.", "missing_license": "Licença em falta", "next_steps": "Passos seguintes", "acquire_commercial_license": "Adquira uma licença comercial", diff --git a/apps/web/public/static/locales/ro/common.json b/apps/web/public/static/locales/ro/common.json index f403e97dff0db9..cd24d6a025a04a 100644 --- a/apps/web/public/static/locales/ro/common.json +++ b/apps/web/public/static/locales/ro/common.json @@ -937,8 +937,6 @@ "verify_wallet": "Verificați portofelul", "create_events_on": "Creați evenimente în", "enterprise_license": "Aceasta este o caracteristică de întreprindere", - "enterprise_license_description": "Pentru a activa această caracteristică, obține o cheie de implementare la consola {{consoleUrl}} și adaug-o la .env ca CALCOM_LICENSE_KEY. Dacă echipa ta are deja o licență, te rugăm să contactezi {{supportMail}} pentru ajutor.", - "enterprise_license_development": "Puteți testa această caracteristică în modul de dezvoltare. Pentru utilizarea în mediul de producție, solicitați unui administrator să acceseze <2>/auth/setup pentru a introduce o cheie de licență.", "missing_license": "Licență lipsă", "next_steps": "Pașii următori", "acquire_commercial_license": "Obțineți o licență comercială", diff --git a/apps/web/public/static/locales/ru/common.json b/apps/web/public/static/locales/ru/common.json index 218b2136f7586d..6336783d635194 100644 --- a/apps/web/public/static/locales/ru/common.json +++ b/apps/web/public/static/locales/ru/common.json @@ -938,8 +938,6 @@ "verify_wallet": "Подтвердить кошелек", "create_events_on": "Создавать события в календаре:", "enterprise_license": "Это функция корпоративного тарифного плана", - "enterprise_license_description": "Чтобы включить эту функцию, получите ключ развертывания на консоли {{consoleUrl}} и добавьте его в .env как CALCOM_LICENSE_KEY. Если у вашей команды уже есть лицензия, пожалуйста, обратитесь за помощью в {{supportMail}}.", - "enterprise_license_development": "Протестируйте эту функцию в режиме разработки. Для использования в рабочем режиме администратор должен перейти по ссылке <2>/auth/setup и ввести лицензионный ключ.", "missing_license": "Отсутствует лицензия", "next_steps": "Следующие шаги", "acquire_commercial_license": "Приобрести коммерческую лицензию", diff --git a/apps/web/public/static/locales/sr/common.json b/apps/web/public/static/locales/sr/common.json index 8bed10507f07ff..4ed1a9e7f02a4f 100644 --- a/apps/web/public/static/locales/sr/common.json +++ b/apps/web/public/static/locales/sr/common.json @@ -937,8 +937,6 @@ "verify_wallet": "Verifikuj Wallet", "create_events_on": "Kreiraj događaje na", "enterprise_license": "Ovo je enterprise funkcija", - "enterprise_license_description": "Da biste omogućili ovu funkciju, nabavite ključ za primenu na {{consoleUrl}} konzoli i dodajte ga u svoj .env kao CALCOM_LICENCE_KEY. Ako vaš tim već ima licencu, obratite se na {{supportMail}} za pomoć.", - "enterprise_license_development": "Ovu funkciju možete testirati u razvojnom režimu. Za proizvodnu upotrebu neka administrator ode na <2>/auth/setup da unese ključ za licencu.", "missing_license": "Nedostaje Licenca", "next_steps": "Sledeći koraci", "acquire_commercial_license": "Steknite komercijalnu licencu", diff --git a/apps/web/public/static/locales/sv/common.json b/apps/web/public/static/locales/sv/common.json index 2f50d0ef5bb0f4..3a5560dfd851c7 100644 --- a/apps/web/public/static/locales/sv/common.json +++ b/apps/web/public/static/locales/sv/common.json @@ -937,8 +937,6 @@ "verify_wallet": "Verifiera plånbok", "create_events_on": "Skapa händelser i", "enterprise_license": "Det här är en företagsfunktion", - "enterprise_license_description": "För att aktivera den här funktionen hämtar du en distributionsnyckel i konsolen {{consoleUrl}} och lägger till den i din .env som CALCOM_LICENSE_KEY. Om ditt team redan har en licens kan du kontakta {{supportMail}} för att få hjälp.", - "enterprise_license_development": "Du kan testa den här funktionen i utvecklingsläge. För produktionsanvändning ska en administratör gå till <2>/auth/setup för att ange en licensnyckel.", "missing_license": "Licens saknas", "next_steps": "Kommande steg", "acquire_commercial_license": "Skaffa en kommersiell licens", diff --git a/apps/web/public/static/locales/tr/common.json b/apps/web/public/static/locales/tr/common.json index 58b65312ba1af3..a64c077f33b812 100644 --- a/apps/web/public/static/locales/tr/common.json +++ b/apps/web/public/static/locales/tr/common.json @@ -937,8 +937,6 @@ "verify_wallet": "Cüzdanı Doğrula", "create_events_on": "Şurada etkinlik oluşturun:", "enterprise_license": "Bu bir kurumsal özelliktir", - "enterprise_license_description": "Bu özelliği etkinleştirmek için {{consoleUrl}} konsolundan bir dağıtım anahtarı alın ve bunu .env dosyanıza CALCOM_LICENSE_KEY olarak ekleyin. Ekibinizin zaten bir lisansı varsa yardım için lütfen {{supportMail}} adresinden iletişime geçin.", - "enterprise_license_development": "Bu özelliği, geliştirme modunda deneyebilirsiniz. Üretim kullanımı için lütfen bir yöneticinin lisans anahtarını girmesi için <2>/auth/setup adımlarını izlemesini sağlayın.", "missing_license": "Eksik Lisans", "next_steps": "Sonraki Adımlar", "acquire_commercial_license": "Ticari bir lisans edinin", diff --git a/apps/web/public/static/locales/uk/common.json b/apps/web/public/static/locales/uk/common.json index e8b580c701c597..698ff57c35d91e 100644 --- a/apps/web/public/static/locales/uk/common.json +++ b/apps/web/public/static/locales/uk/common.json @@ -969,8 +969,6 @@ "verify_wallet": "Пройдіть перевірку гаманця", "create_events_on": "Створюйте заходи в календарі", "enterprise_license": "Це корпоративна функція", - "enterprise_license_description": "Щоб увімкнути цю функцію, отримайте ключ розгортання в консолі {{consoleUrl}} і додайте його у свій файл .env як CALCOM_LICENSE_KEY. Якщо у вашої команди вже є ліцензія, зверніться по допомогу за адресою {{supportMail}}.", - "enterprise_license_development": "Ви можете випробувати цю функцію в режимі розробки. Щоб скористатися нею, адміністратору потрібно перейти в розділ <2>«Перевірка»/«Налаштування» і ввести ключ ліцензії.", "missing_license": "Відсутня ліцензія", "next_steps": "Подальші кроки", "acquire_commercial_license": "Отримати комерційну ліцензію", diff --git a/apps/web/public/static/locales/vi/common.json b/apps/web/public/static/locales/vi/common.json index 47feec9fb6da5e..ec687a3a1c7d5e 100644 --- a/apps/web/public/static/locales/vi/common.json +++ b/apps/web/public/static/locales/vi/common.json @@ -940,8 +940,6 @@ "verify_wallet": "Xác minh Ví", "create_events_on": "Tạo sự kiện trên", "enterprise_license": "Đây là tính năng doanh nghiệp", - "enterprise_license_description": "Để bật tính năng này, nhận khoá triển khai tại console {{consoleUrl}} và thêm nó vào .env của bạn ở dạng CALCOM_LICENSE_KEY. Nếu nhóm của bạn đã có giấy phép, vui lòng liên hệ {{supportMail}} để được trợ giúp.", - "enterprise_license_development": "Bạn có thể thử nghiệm tính năng này ở chế độ phát triển. Để sử dụng cho sản xuất, hãy nhờ quản trị viên vào phần <2>/auth/setup để nhập một khoá giấy phép.", "missing_license": "Giấy phép bị thiếu", "next_steps": "Bước tiếp theo", "acquire_commercial_license": "Lấy giấy phép thương mại", diff --git a/apps/web/public/static/locales/zh-CN/common.json b/apps/web/public/static/locales/zh-CN/common.json index f12da771024058..e4aa6a92c60d33 100644 --- a/apps/web/public/static/locales/zh-CN/common.json +++ b/apps/web/public/static/locales/zh-CN/common.json @@ -973,8 +973,6 @@ "verify_wallet": "验证钱包", "create_events_on": "活动创建于:", "enterprise_license": "这是企业版功能", - "enterprise_license_description": "要启用此功能,请在 {{consoleUrl}} 控制台获取一个部署密钥,并将其添加到您的 .env 中作为 CALCOM_LICENSE_KEY。如果您的团队已经有许可证,请联系 {{supportMail}} 获取帮助。", - "enterprise_license_development": "您可以在开发模式下测试此功能。对于生产用途,请让管理员转到 <2>/auth/setup 输入许可证密钥。", "missing_license": "缺少许可证", "next_steps": "下一步", "acquire_commercial_license": "获取商业许可证", diff --git a/apps/web/public/static/locales/zh-TW/common.json b/apps/web/public/static/locales/zh-TW/common.json index b41b34d04386c1..91f6e8c1515acc 100644 --- a/apps/web/public/static/locales/zh-TW/common.json +++ b/apps/web/public/static/locales/zh-TW/common.json @@ -944,8 +944,6 @@ "verify_wallet": "驗證錢包", "create_events_on": "建立活動時間:", "enterprise_license": "此為企業版功能", - "enterprise_license_description": "若要啟用此功能,請在 {{consoleUrl}} 主控台取得部署金鑰,並在您的 .env 中新增為 CALCOM_LICENSE_KEY。如果您的團隊已經取得授權,請聯絡 {{supportMail}} 取得協助。", - "enterprise_license_development": "您可以在開發模式下測試此功能。若要用於生產環境,請由管理員前往 <2>/auth/setup 輸入授權金鑰。", "missing_license": "遺失授權", "next_steps": "下一步", "acquire_commercial_license": "取得商業授權", diff --git a/packages/features/ee/common/components/LicenseRequired.tsx b/packages/features/ee/common/components/LicenseRequired.tsx index 1f3422aa26db2b..98b8165adb8c4b 100644 --- a/packages/features/ee/common/components/LicenseRequired.tsx +++ b/packages/features/ee/common/components/LicenseRequired.tsx @@ -1,11 +1,10 @@ import { useSession } from "next-auth/react"; -import { Trans } from "next-i18next"; import type { AriaRole, ComponentType } from "react"; import React, { Fragment, useEffect } from "react"; -import { SUPPORT_MAIL_ADDRESS, WEBAPP_URL } from "@calcom/lib/constants"; +import { WEBAPP_URL } from "@calcom/lib/constants"; import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { EmptyScreen, Alert } from "@calcom/ui"; +import { EmptyScreen, Alert, Button } from "@calcom/ui"; type LicenseRequiredProps = { as?: keyof JSX.IntrinsicElements | ""; @@ -41,15 +40,8 @@ const LicenseRequired = ({ children, as = "", ...rest }: LicenseRequiredProps) = severity="warning" title={ <> - {t("enterprise_license")}.{" "} - - You can test this feature on development mode. For production usage please have an - administrator go to{" "} - - /auth/setup - {" "} - to enter a license key. - + {t("enterprise_license_locally")} {t("enterprise_license_sales")}{" "} + {t("contact_sales")} } /> @@ -59,19 +51,12 @@ const LicenseRequired = ({ children, as = "", ...rest }: LicenseRequiredProps) = - To enable this feature, have an administrator go to{" "} - - /auth/setup - - to enter a license key. If a license key is already in place, please contact{" "} - - {{ SUPPORT_MAIL_ADDRESS }} - - for help. - + buttonRaw={ + } + description={t("enterprise_license_sales")} /> )} From 77a98d96c8d2150da70d48eb8f56e7d1b918a441 Mon Sep 17 00:00:00 2001 From: Erik Date: Thu, 2 May 2024 12:24:24 -0300 Subject: [PATCH 4/8] chore: attach unique request ids (#14857) --- apps/api/v2/src/app.module.ts | 12 +++++++++++- apps/api/v2/src/filters/http-exception.filter.ts | 4 ++++ .../v2/src/filters/prisma-exception.filter.ts | 3 +++ .../v2/src/filters/sentry-exception.filter.ts | 2 ++ apps/api/v2/src/filters/trpc-exception.filter.ts | 3 +++ apps/api/v2/src/filters/zod-exception.filter.ts | 2 ++ .../request-ids/request-id.interceptor.ts | 16 ++++++++++++++++ .../request-ids/request-id.middleware.ts | 11 +++++++++++ 8 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 apps/api/v2/src/middleware/request-ids/request-id.interceptor.ts create mode 100644 apps/api/v2/src/middleware/request-ids/request-id.middleware.ts diff --git a/apps/api/v2/src/app.module.ts b/apps/api/v2/src/app.module.ts index 2c84b82f377b42..7b42702be6d742 100644 --- a/apps/api/v2/src/app.module.ts +++ b/apps/api/v2/src/app.module.ts @@ -3,6 +3,8 @@ import { AppLoggerMiddleware } from "@/middleware/app.logger.middleware"; import { RewriterMiddleware } from "@/middleware/app.rewrites.middleware"; import { JsonBodyMiddleware } from "@/middleware/body/json.body.middleware"; import { RawBodyMiddleware } from "@/middleware/body/raw.body.middleware"; +import { ResponseInterceptor } from "@/middleware/request-ids/request-id.interceptor"; +import { RequestIdMiddleware } from "@/middleware/request-ids/request-id.middleware"; import { AuthModule } from "@/modules/auth/auth.module"; import { EndpointsModule } from "@/modules/endpoints.module"; import { JwtModule } from "@/modules/jwt/jwt.module"; @@ -11,7 +13,7 @@ import { RedisModule } from "@/modules/redis/redis.module"; import { RedisService } from "@/modules/redis/redis.service"; import { MiddlewareConsumer, Module, NestModule, RequestMethod } from "@nestjs/common"; import { ConfigModule } from "@nestjs/config"; -import { RouterModule } from "@nestjs/core"; +import { APP_INTERCEPTOR, RouterModule } from "@nestjs/core"; import { seconds, ThrottlerModule } from "@nestjs/throttler"; import { ThrottlerStorageRedisService } from "nestjs-throttler-storage-redis"; @@ -52,6 +54,12 @@ import { AppController } from "./app.controller"; RouterModule.register([{ path: "/v2", module: EndpointsModule }]), ], controllers: [AppController], + providers: [ + { + provide: APP_INTERCEPTOR, + useClass: ResponseInterceptor, + }, + ], }) export class AppModule implements NestModule { configure(consumer: MiddlewareConsumer): void { @@ -63,6 +71,8 @@ export class AppModule implements NestModule { }) .apply(JsonBodyMiddleware) .forRoutes("*") + .apply(RequestIdMiddleware) + .forRoutes("*") .apply(AppLoggerMiddleware) .forRoutes("*") .apply(RewriterMiddleware) diff --git a/apps/api/v2/src/filters/http-exception.filter.ts b/apps/api/v2/src/filters/http-exception.filter.ts index c47e1ff936c9df..ef3eefd4dfb531 100644 --- a/apps/api/v2/src/filters/http-exception.filter.ts +++ b/apps/api/v2/src/filters/http-exception.filter.ts @@ -13,13 +13,17 @@ export class HttpExceptionFilter implements ExceptionFilter { const response = ctx.getResponse(); const request = ctx.getRequest(); const statusCode = exception.getStatus(); + const requestId = request.headers["X-Request-Id"]; + this.logger.error(`Http Exception Filter: ${exception?.message}`, { exception, body: request.body, headers: request.headers, url: request.url, method: request.method, + requestId, }); + response.status(statusCode).json({ status: ERROR_STATUS, timestamp: new Date().toISOString(), diff --git a/apps/api/v2/src/filters/prisma-exception.filter.ts b/apps/api/v2/src/filters/prisma-exception.filter.ts index 4490a0de8c8ac1..9bd1f82d10b23a 100644 --- a/apps/api/v2/src/filters/prisma-exception.filter.ts +++ b/apps/api/v2/src/filters/prisma-exception.filter.ts @@ -33,12 +33,15 @@ export class PrismaExceptionFilter implements ExceptionFilter { const ctx = host.switchToHttp(); const response = ctx.getResponse(); const request = ctx.getRequest(); + const requestId = request.headers["X-Request-Id"]; + this.logger.error(`PrismaError: ${error.message}`, { error, body: request.body, headers: request.headers, url: request.url, method: request.method, + requestId, }); response.status(HttpStatus.INTERNAL_SERVER_ERROR).json({ status: ERROR_STATUS, diff --git a/apps/api/v2/src/filters/sentry-exception.filter.ts b/apps/api/v2/src/filters/sentry-exception.filter.ts index 479525ecf47c20..996d9faa6dabc4 100644 --- a/apps/api/v2/src/filters/sentry-exception.filter.ts +++ b/apps/api/v2/src/filters/sentry-exception.filter.ts @@ -14,6 +14,7 @@ export class SentryFilter extends BaseExceptionFilter { const ctx = host.switchToHttp(); const response = ctx.getResponse(); const request = ctx.getRequest(); + const requestId = request.headers["X-Request-Id"]; this.logger.error(`Sentry Exception Filter: ${exception?.message}`, { exception, @@ -21,6 +22,7 @@ export class SentryFilter extends BaseExceptionFilter { headers: request.headers, url: request.url, method: request.method, + requestId, }); // capture if client has been init diff --git a/apps/api/v2/src/filters/trpc-exception.filter.ts b/apps/api/v2/src/filters/trpc-exception.filter.ts index e65e94af1628c7..47dbcd00b4c85a 100644 --- a/apps/api/v2/src/filters/trpc-exception.filter.ts +++ b/apps/api/v2/src/filters/trpc-exception.filter.ts @@ -41,12 +41,15 @@ export class TRPCExceptionFilter implements ExceptionFilter { break; } + const requestId = request.headers["X-Request-Id"]; + this.logger.error(`TRPC Exception Filter: ${exception?.message}`, { exception, body: request.body, headers: request.headers, url: request.url, method: request.method, + requestId, }); response.status(statusCode).json({ diff --git a/apps/api/v2/src/filters/zod-exception.filter.ts b/apps/api/v2/src/filters/zod-exception.filter.ts index eb962ff5fa001a..7a61b5c71b39ac 100644 --- a/apps/api/v2/src/filters/zod-exception.filter.ts +++ b/apps/api/v2/src/filters/zod-exception.filter.ts @@ -14,6 +14,7 @@ export class ZodExceptionFilter implements ExceptionFilter { const ctx = host.switchToHttp(); const response = ctx.getResponse(); const request = ctx.getRequest(); + const requestId = request.headers["X-Request-Id"]; this.logger.error(`ZodError: ${error.message}`, { error, @@ -21,6 +22,7 @@ export class ZodExceptionFilter implements ExceptionFilter { headers: request.headers, url: request.url, method: request.method, + requestId, }); response.status(HttpStatus.BAD_REQUEST).json({ diff --git a/apps/api/v2/src/middleware/request-ids/request-id.interceptor.ts b/apps/api/v2/src/middleware/request-ids/request-id.interceptor.ts new file mode 100644 index 00000000000000..b49d75d4564e88 --- /dev/null +++ b/apps/api/v2/src/middleware/request-ids/request-id.interceptor.ts @@ -0,0 +1,16 @@ +import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common"; +import { Request, Response } from "express"; + +@Injectable() +export class ResponseInterceptor implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler) { + const ctx = context.switchToHttp(); + const request = ctx.getRequest(); + const response = ctx.getResponse(); + + const requestId = request.headers["X-Request-Id"] ?? "unknown-request-id"; + response.setHeader("X-Request-Id", requestId.toString()); + + return next.handle(); + } +} diff --git a/apps/api/v2/src/middleware/request-ids/request-id.middleware.ts b/apps/api/v2/src/middleware/request-ids/request-id.middleware.ts new file mode 100644 index 00000000000000..2902dd8fb2b6fa --- /dev/null +++ b/apps/api/v2/src/middleware/request-ids/request-id.middleware.ts @@ -0,0 +1,11 @@ +import { Injectable, NestMiddleware } from "@nestjs/common"; +import { Request, Response, NextFunction } from "express"; +import { v4 as uuid } from "uuid"; + +@Injectable() +export class RequestIdMiddleware implements NestMiddleware { + use(req: Request, res: Response, next: NextFunction) { + req.headers["X-Request-Id"] = uuid(); + next(); + } +} From 52813b01e6db030477908972c1fb864965b95d24 Mon Sep 17 00:00:00 2001 From: Erik Date: Thu, 2 May 2024 12:24:42 -0300 Subject: [PATCH 5/8] chore: don't try to increase usage for legacy plans (#14856) --- .../billing/services/billing.service.ts | 44 ++++++++++++------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/apps/api/v2/src/modules/billing/services/billing.service.ts b/apps/api/v2/src/modules/billing/services/billing.service.ts index cdd44dddab9073..203d734c120642 100644 --- a/apps/api/v2/src/modules/billing/services/billing.service.ts +++ b/apps/api/v2/src/modules/billing/services/billing.service.ts @@ -99,22 +99,36 @@ export class BillingService { } async increaseUsageForTeam(teamId: number) { - // TODO - if we support multiple subscription items per team, we may need to track which plan they're - // subscribed to so we can do one less query. - const billingSubscription = await this.billingRepository.getBillingForTeam(teamId); - if (!billingSubscription || !billingSubscription?.subscriptionId) { - throw new Error(`Failed to increase usage for team ${teamId}`); - } + try { + const billingSubscription = await this.billingRepository.getBillingForTeam(teamId); + if (!billingSubscription || !billingSubscription?.subscriptionId) { + this.logger.error("Team did not have stripe subscription associated to it", { + teamId, + }); + return void 0; + } - const stripeSubscription = await this.stripeService.stripe.subscriptions.retrieve( - billingSubscription.subscriptionId - ); - const items = stripeSubscription.items.data[0]; // first (and only) subscription item. - await this.stripeService.stripe.subscriptionItems.createUsageRecord(items.id, { - action: "increment", - quantity: 1, - timestamp: "now", - }); + const stripeSubscription = await this.stripeService.stripe.subscriptions.retrieve( + billingSubscription.subscriptionId + ); + const item = stripeSubscription.items.data[0]; + // legacy plans are licensed, we cannot create usage records against them + if (item.price?.recurring?.usage_type === "licensed") { + return void 0; + } + + await this.stripeService.stripe.subscriptionItems.createUsageRecord(item.id, { + action: "increment", + quantity: 1, + timestamp: "now", + }); + } catch (error) { + // don't fail the request, log it. + this.logger.error("Failed to increase usage for team", { + teamId: teamId, + error, + }); + } } async increaseUsageByClientId(clientId: string) { From 6c8fced247ac393a259998eade4586edeb96fc95 Mon Sep 17 00:00:00 2001 From: Hariom Balhara Date: Thu, 2 May 2024 21:33:57 +0530 Subject: [PATCH 6/8] fix: Snippet path in preview (#14830) * fix: Snippet path in preview * Add test --- .../embed-core/playwright/tests/preview.e2e.ts | 18 +++++++++++++++++- packages/embeds/embed-core/src/preview.ts | 2 +- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/embeds/embed-core/playwright/tests/preview.e2e.ts b/packages/embeds/embed-core/playwright/tests/preview.e2e.ts index a55a0b97f44eec..28506adb07414b 100644 --- a/packages/embeds/embed-core/playwright/tests/preview.e2e.ts +++ b/packages/embeds/embed-core/playwright/tests/preview.e2e.ts @@ -3,7 +3,7 @@ import { expect } from "@playwright/test"; import { test } from "@calcom/web/playwright/lib/fixtures"; test.describe("Preview", () => { - test("Preview - embed-core should load", async ({ page }) => { + test("Preview - embed-core should load if correct embedLibUrl is provided", async ({ page }) => { await page.goto( "http://localhost:3000/embed/preview.html?embedLibUrl=http://localhost:3000/embed/embed.js&bookerUrl=http://localhost:3000&calLink=pro/30min" ); @@ -26,4 +26,20 @@ test.describe("Preview", () => { }); expect(libraryLoaded).toBe(true); }); + + test("Preview - embed-core should load from embedLibUrl", async ({ page }) => { + // Intentionally pass a URL that will not load to be able to easily test that the embed was loaded from there + page.goto( + "http://localhost:3000/embed/preview.html?embedLibUrl=http://wronglocalhost:3000/embed/embed.js&bookerUrl=http://localhost:3000&calLink=pro/30min" + ); + + const failedRequestUrl = await new Promise((resolve) => + page.on("requestfailed", (request) => { + console.log("request failed"); + resolve(request.url()); + }) + ); + + expect(failedRequestUrl).toBe("http://wronglocalhost:3000/embed/embed.js"); + }); }); diff --git a/packages/embeds/embed-core/src/preview.ts b/packages/embeds/embed-core/src/preview.ts index b4dcddc25329c6..d361342c76888d 100644 --- a/packages/embeds/embed-core/src/preview.ts +++ b/packages/embeds/embed-core/src/preview.ts @@ -47,7 +47,7 @@ if (!bookerUrl || !embedLibUrl) { } p(cal, ar); }; -})(window, "//localhost:3000/embed/embed.js", "init"); +})(window, embedLibUrl, "init"); const previewWindow = window; previewWindow.Cal.fingerprint = process.env.EMBED_PUBLIC_EMBED_FINGER_PRINT as string; From 00372b6b3bbbf0e2c7fd2a000dac6313e1e5d13f Mon Sep 17 00:00:00 2001 From: Erik Date: Thu, 2 May 2024 13:31:50 -0300 Subject: [PATCH 7/8] chore: Discard unused Platform stripe events (#14822) * chore: discard unused webhooks * chore: discard early --- .../billing/controllers/billing.controller.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/apps/api/v2/src/modules/billing/controllers/billing.controller.ts b/apps/api/v2/src/modules/billing/controllers/billing.controller.ts index 00bca4190ae180..00319098f3845a 100644 --- a/apps/api/v2/src/modules/billing/controllers/billing.controller.ts +++ b/apps/api/v2/src/modules/billing/controllers/billing.controller.ts @@ -104,10 +104,19 @@ export class BillingController { if (event.type === "customer.subscription.created" || event.type === "customer.subscription.updated") { const subscription = event.data.object as Stripe.Subscription; + if (!subscription.metadata?.teamId) { + return { + status: "success", + }; + } + const teamId = Number.parseInt(subscription.metadata.teamId); const plan = subscription.metadata.plan; if (!plan || !teamId) { - throw new Error("Invalid webhook received."); + this.logger.log("Webhook received but not pertaining to Platform, discarding."); + return { + status: "success", + }; } await this.billingService.setSubscriptionForTeam( @@ -121,6 +130,8 @@ export class BillingController { }; } - throw new BadRequestException(`Unhandled event type ${event.type}`); + return { + status: "success", + }; } } From 97b2816897a99da96f59cda2f0cce3b9f08c571b Mon Sep 17 00:00:00 2001 From: Keith Williams Date: Thu, 2 May 2024 19:23:46 -0300 Subject: [PATCH 8/8] v4.0.6 (#14865) --- apps/web/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/package.json b/apps/web/package.json index 9a616f10951331..7ed2d9fbd1e378 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@calcom/web", - "version": "4.0.5", + "version": "4.0.6", "private": true, "scripts": { "analyze": "ANALYZE=true next build",