Compare commits

...
298 Commits
Author SHA1 Message Date
GitHub Actions 214528ffa8 chore: release v5.1.7
Post release / release (push) Canceled after 0s
2025-03-25 00:23:18 +00:00
Keith WilliamsandAlex van Andel 7f79779bf8 chore: Upgrade jackson (#20107)
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2025-03-25 00:18:26 +00:00
GitHub Actions ed0f8fe0e7 chore: release v5.1.6
Post release / release (push) Canceled after 0s
2025-03-24 23:23:16 +00:00
Benny Joo 273fc544e7 feat: rewrite all routes starting with supported lang code (#20346)
* feat: rewrite all routes starting with matching locale code

* update e2e test
2025-03-24 23:15:39 +00:00
Joe Au-Yeung f24e8d12f0 feat: Salesforce - handle picklist field type (#20342)
* Add picklist to `SalesforceFieldType` enum

* Add picklist value to Salesforce settings

* Add `getPicklistFieldValue` method

* Find picklist option based on active and value
2025-03-24 23:05:30 +00:00
Omar López 19712f62e3 feat: improved observability on api v1 and webapp (#20302)
* feat: associate errors to users on api v1

* Added webapp

* Update layout.tsx

* Update captureUserId.ts
2025-03-24 23:01:20 +00:00
Benny JooandAnik Dhabal Babu 0ec701f9e6 fix: rewrite /en/* paths to /* (#20331)
* fix: add back i18n config to next.config.js

* add back localeDetection

* rewrite /en/* paths to /

* add e2e test

* add comment

* add test

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-03-24 16:50:55 -03:00
sean-brydon e4cfaa56f0 no throw 202 (#20339) 2025-03-24 16:48:38 +00:00
c7641f852f feat: Added org username to admin users view (#20254)
* feat : Added org username to admin users view

* styling tweaks (#20215)

* minor-changes

---------

Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
Co-authored-by: Tushar <tusharbhatt0135@gmail.com>
2025-03-24 15:03:58 +00:00
Ayanava Karmakar e2597fbcd2 docs: fix invalid JSX in code snippets in platform/guides/booking-redirects (#20321) 2025-03-24 14:32:02 +00:00
Zayd Krunz ce88018ecc Update aws.mdx (#20330) 2025-03-24 14:31:01 +00:00
Vijay 0eb4e6c7d4 explicitly specify default visibility value till initializes (#20312) 2025-03-24 14:25:56 +00:00
Keith Williams 10c9c63ad8 revert: "fix: lint (#20325)" (#20337)
This reverts commit 1dc63e0df7.
2025-03-24 11:02:04 -03:00
Omar López a18defaa36 fix: increase memory requirement for building atoms (#20306) 2025-03-24 10:50:35 -03:00
GitHub Actions f9ee907245 chore: release v5.1.5
Post release / release (push) Canceled after 0s
2025-03-24 00:23:17 +00:00
Calcom BotandLingo.dev eac6b12e91 feat: update translations via @lingodotdev (#20320)
Co-authored-by: Lingo.dev <support@lingo.dev>
2025-03-23 23:07:36 +00:00
Anik Dhabal Babu 1dc63e0df7 fix: lint (#20325) 2025-03-23 23:03:25 +00:00
Keith Williams 96cd3f7b2f chore: Upgrade @sentry/nextjs to 9.8.0 (#20324)
* chore: Upgrade Sentry

* Use ^9.8.0
2025-03-23 15:24:59 -03:00
Keith Williams 7943a83e7d fix: lint errors (#20323) 2025-03-23 14:38:53 +00:00
Anik Dhabal Babu 01c16ad150 fix: calender setting skeleton (#20317) 2025-03-23 08:23:28 +00:00
Ayanava Karmakar 42807481c1 fix(docs/platform/guides) - use correct variable in reschedule booker code snippet (#20316) 2025-03-23 04:51:05 +00:00
Keith Williams 032c41fefd chore: Upgrade Next to 15.2.3 (#20315) 2025-03-22 15:07:57 +00:00
Vijay 41041226b4 wait in advance and avoid race condition (#20311) 2025-03-22 14:56:01 +05:30
Hariom Balhara 56ab4e8415 Add salesforce query validation in integration tests (#20209) 2025-03-21 23:06:41 +00:00
Calcom BotandLingo.dev 8ee6d2befa feat: update translations via @lingodotdev (#20307)
Co-authored-by: Lingo.dev <support@lingo.dev>
2025-03-21 20:10:34 +00:00
Omar López 9cd133ff10 Auto builds icons before dev and build scripts (#20304) 2025-03-21 19:59:00 +00:00
GitHub Actions 6df473c5fb chore: release v5.1.4
Post release / release (push) Canceled after 0s
2025-03-21 19:14:23 +00:00
Benny Joo 4606fc6ace chore: Remove HeadSeo everywhere (#20272) 2025-03-21 14:24:53 -04:00
78ddf71823 perf: Move OOO-related tRPC procedures to new routers (#20136)
* perf: Move OOO-related tRPC procedures to new routers

* Moved test file

* push fix for import path in tests

* fix trpc response url that we wait for

---------

Co-authored-by: sean-brydon <sean@cal.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2025-03-21 09:03:10 -07:00
Morgan 15cb687307 chore: ensure default calendars for delegated users (#20300)
* chore: ensure default calendars for delegated users

* fixup! chore: ensure default calendars for delegated users
2025-03-21 15:41:04 +00:00
Lauris Skraucis c0ecfcd910 docs: v2 team event schedulingType (#20289)
* refactor: input scheduling type

* refactor: output scheduling type
2025-03-21 13:25:59 +00:00
Lauris Skraucis 2e86f8404f fix: booker atom booking with split name (#20298) 2025-03-21 13:25:32 +00:00
Morgan b94b16316f fix: i18n failed to fetch translations results in api v2 crash (#20294)
* fix: i18n failed to fetch translations

* fixup! fix: i18n failed to fetch translations
2025-03-21 14:22:38 +02:00
Tushar Bhatt f80b780aae fix: removing unnecessary styles (#20284)
* fix: removing unnecessary styles

* add padding

* padding

* padding
2025-03-21 16:36:21 +05:30
Pranav Patil 00dc467f25 fix: Padding issue in text fields (#20287) 2025-03-21 09:03:54 +00:00
Morgan d644c09bac fix: missing Prisma type in libraries (#20285) 2025-03-21 08:50:14 +00:00
Vijay 5d9c7bfde9 place correct actions before await (#20266) 2025-03-21 09:39:21 +05:30
Benny Joo cfd1c5dfe3 show-skeleton-only-if-cached-data-do-not-exist (#20275) 2025-03-21 09:38:58 +05:30
amrit d7e5a8e588 fix: update SkeletonLoaderTeamList component layout (#20263) 2025-03-21 02:21:54 +00:00
Benny Joo 3b992fa9bd chore: missing awaits for params/search params in two pages (#20276) 2025-03-21 02:19:15 +00:00
8ad442f2be feat: Upgrade to Next 15 (#18834)
* wip

* update layoutHOC

* wip

* remove app router related code no longer used

* remove more

* await cookies, headers, params, serachparams

* update yarn.lock

* await cookies, headers, params, serachparams more

* update yarn lock again

* downgrade next-auth to 4.22.1

* update yarn lock

* fix

* update yarn lock

* fix type checks

* update yarn lock

* await headers and cookies

* restore pages folder

* restore yarn.lock

* update yarn.lock

* await headers and cookies

* remove

* await params in API routes

* updates

* restore next.config.js

* remove i18n from next.config.js

* Fixed tests

* Fixed types

* Removed duplicate favicon.ico

* Fixing more types

* ImageResponse moved to next/og

* Fixed prisma import issues

* dynamic import for @ewsjs/xhr

* remove deasync dep

* dynamic import for @tryvital/vital-node

* fix type checks

* add back turbopack command

* Type fix

* Removed unneeded file

* fix turbopack relative path errors

* add comments

* remove unneeded code

* Fixed build errors

* await apis

* use Promise<Params> type in defaultResponderForAppDir util

* refactor scim api route

* fix type checks

* separate app-routing.config into client-config and server-config

* wip

* refactor routing forms components

* revert unneeded changes for easier review

* fix

* fix

* use CustomTrans

* fix type

* fix unit tests

* fix type error

* fix build error

* fix build error

* fix build error

* fix warnings

* fix warnings

* upgrade @tremor/react and tailwindcss

* numCols -> numItems

* fix forgot-password e2e test

* fix 1 more e2e test

* fix login e2e test

* fix e2e tests

* fix e2e tests

* clean up

* remove error

* use tremor/react 2.11.0

* fix

* rename CustomTrans to ServerTrans

* address comment

* fix test

* fix ServerTrans

* fix

* fix type

* fix ServerTrans usages 1

* fix translations

* fix type checks

* fix type checks

* link styling

* fix

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-03-20 21:30:51 -03:00
GitHub Actions 695a4d7a59 chore: release v5.1.3
Post release / release (push) Canceled after 0s
2025-03-20 22:05:36 +00:00
Anik Dhabal BabuandTushar Bhatt 9bcea0fa01 replyTo includes the correct email (#20235)
Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
2025-03-20 17:16:12 +00:00
Rajiv Sahal 9a4c8c147b feat: improve trpc exception handling for api v2 (#20158)
* improve trpc exception handling for api v2

* update error messages

* fixup
2025-03-20 16:03:29 +00:00
Gaurav Verma a48ad2219f feat: add aria-labels for accessibility in AddVariablesDropdown and ToolbarPlugin components (#20260) 2025-03-20 15:59:47 +00:00
Udit Takkarandsean-brydon 55a97b71c8 perf: fix circular dependencies (#20121)
* perf: fix circular dependencies

* fix: types

* fix: types

* fix: types

---------

Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2025-03-20 15:30:26 +00:00
Udit Takkar b1b8b0135e refactor: event type builder (#19459) 2025-03-20 07:49:14 -07:00
Rajiv SahalandMorgan 08d4c800e9 feat: allow timezones selection in Booker atom via prop (#20044)
* expose props to pass in timezones and booking dry run values from outside

* fix merge conflicts

* update typings

* add props values to test

* fixup: PR feedback

* PR feedback

* fix wrong prop name

* fix merge conflicts

* fixup

* update docs

* update docs examples

* fix props naming

* fix timezone component logic and typo

* extract typings of timeZones

* update typings for Timezone

* update docs

* resolve merge conflicts

* fix typings

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2025-03-20 14:05:45 +00:00
Vijay efeb0a8f41 fix: repeatedly occurring flaky tests (#20238)
* fix flaky tests

* minor tweak in ooo tests

* chore

* update: fill description at end
2025-03-20 18:46:41 +05:30
Hariom Balhara 207147f5ff fix: Errors seen in some cases when organization is created through webhook (#20249) 2025-03-20 13:02:32 +00:00
Anik Dhabal Babu 6bb8a7c6a9 fix: e2e flake (#20252) 2025-03-20 12:14:51 +00:00
Hariom Balhara aa7f9719c9 Must slugify the slug - in UI and before we use it (#20242) 2025-03-20 11:23:46 +00:00
Carina WollendorferandCarinaWolli e045dcff11 fix: add response id to webhook payload (#20247)
* add responseId to webhook payload

* fix test

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2025-03-20 11:21:30 +00:00
Eunjae Lee 80eb6daf1f fix: case insensitive search on organization member list (#20240) 2025-03-20 10:47:57 +00:00
Benny Joo b0bf274896 perf: Remove packages/ui/index.ts barrel file (#20243)
* remove packages/ui/index.ts

* Update tests
2025-03-20 07:26:12 -03:00
Morgan 4762d475cb chore: get oauth client parans from event-type hosts (#20236) 2025-03-20 08:02:25 +00:00
Peer Richelsen c7d9c63390 added padding bottom so plain chat doenst overlay anything (#20224) 2025-03-20 03:56:16 +00:00
Benny JooandKeith Williams 853f9bc436 perf: do not import from @calcom/ui barrel file (#20184)
* Icon and IconName

* Button and ButtonGroup

* UserAvatar

* AvatarGroup

* Avatar

* WizardLayout

* Dialogs

* EmptyScreen

* showToast and TextField

* Editor

* Skeleton

* Skeleton

* TopBanner and showToast

* Button again

* more

* perf: Remove app-store reference from @calcom/ui

* more

* Fixing types

* Icon

* Fixed casing

* dropdown

* more

* Select

* more

* Badge

* List

* more

* Divider

* more

* fix

* fix type check

* refactor

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix type check

* fix

* fix

* fix

* fix

* more

* more

* more

* more

* add index file to components/command

* fix

* fix

* fix

* fix imports

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix build errors

* fix build errors

* fix

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2025-03-19 19:00:55 -03:00
GitHub Actions ca19aa00c1 chore: release v5.1.2
Post release / release (push) Canceled after 0s
2025-03-19 21:59:27 +00:00
Anik Dhabal Babu 467c979aa7 fix: updating the user does not invalidate/refresh the table (#20228) 2025-03-19 20:28:18 +00:00
Hariom Balhara 9f9bf10040 fix: A user exists with that username (#20217) 2025-03-19 14:34:33 +00:00
Lauris Skraucis 8102192987 refactor: v2 booking responses schema (#20030)
* refactor: when rescheduling fallback on attendee if responses invalid

* chore: submit auto generated docs

* fix: slots event type metadata parsing

* fix: booking responses without email with attendee phone

* refactor

* refactor: test

* remove unused import

* refactor

* test: slots variable duration
2025-03-19 16:32:00 +02:00
Pranav PatilandTushar Bhatt 232f9d288c fix: Loading indicator alignment in settings page (#20219)
* Fix: Loading indicator alignment in profile page

* Added animate spin class

---------

Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
2025-03-19 14:31:05 +00:00
Anik Dhabal Babu 7dab100382 fix: unskip confirm step if there are multiple location (#20051) 2025-03-19 12:17:49 +00:00
Lauris Skraucis 91d4f1c219 feat: booker atom confirm and back button styling (#20162)
* feat: submitButtonDisabled Booker atom prop

* feat: confirmStep.confirmButton & backButton booker styles

* refactor: rename submitButtonDisabled to confirmButtonDisabled

* feat: implement this feature for skip confirm step

* refactor: naming
2025-03-19 11:30:48 +00:00
MorganandSomay Chauhan a64e7f9c2a fix: set default managed users calendars (#20208)
* fix: set default managed users calendars

* fix: module imports

---------

Co-authored-by: Somay Chauhan <somaychauhan98@gmail.com>
2025-03-19 09:22:28 +00:00
sean-brydonandKeith Williams 3c1297aa72 fix: calcom trpc sessio circle dep (#19893)
* fix trpc session circle dep

* fixes trpc session cirlce dep

* fix relative imports

* fix more imports to use types and not trpc

* fix exports

* Fixed types

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2025-03-19 05:50:22 -03:00
Vijay 96f7a01c5f fix: few flaky tests in ooo and bookings-list (#20170)
* fix flaky tests

* chore
2025-03-19 08:32:47 +00:00
Udit TakkarandTushar Bhatt a4a05878ef fix: use correct skeleton loader (#20197)
Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
2025-03-19 08:07:22 +00:00
Keith WilliamsandhbjORbj 352d68ae1c perf: Remove app-store reference from @calcom/ui (#20188)
* perf: Remove app-store reference from @calcom/ui

* Fixing types

* Fixed casing

* fix type check

* refactor

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: hbjORbj <sldisek783@gmail.com>
2025-03-19 05:04:02 -03:00
Tushar Bhatt ffd034679c fix: team-update-on-eventTypes-page (#20152) 2025-03-19 07:28:18 +00:00
Morgan d64ae3c901 fix: missing booking reminder reference in cron (#20200) 2025-03-19 07:27:52 +00:00
Hariom Balhara 7250e22257 fix salesforce query (#20196) 2025-03-19 09:23:12 +02:00
Udit Takkar f459e5d9c4 fix: use correct logo (#20194) 2025-03-19 06:29:07 +00:00
Tushar Bhatt 30bb0f1383 fix: logo-sync-issue (#20190) 2025-03-19 04:44:55 +00:00
Abdullah Arif 24c993a3d2 fix(createButton): add scrolling to empty webhook dropdown (#20191)
Added a scroll-bar class and set a max height on DropdownMenuContent to ensure large dropdowns are scrollable, improving the user experience by making all items accessible without expanding beyond the viewport.
2025-03-19 09:54:09 +05:30
GitHub Actions aa4276be5b chore: release v5.1.1
Post release / release (push) Canceled after 0s
2025-03-18 20:45:23 +00:00
Somay ChauhanandKeith Williams 413aba6e52 fix: add documentation to the possible values of :app in conferencing controller (#20164)
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2025-03-18 17:41:35 -03:00
sean-brydon c2547acb3d fix eslint error (#20127) 2025-03-18 20:21:08 +00:00
Benny Joo cc1b6bac4a perf: Remove QueryCell from availability view (#20176) 2025-03-18 17:05:02 -03:00
Udit Takkar d5d90a8b87 fix: sms formatting (#18647) 2025-03-18 16:59:13 -03:00
Joe Au-YeungandKeith Williams 40333c81c5 chore: Add logging to Salesforce (#20167)
* Remove unused param

* Add log when no contacts are created

* Replace console.log with logger

* Add logging around `buildRecordUpdatePayload`

* Add details if field isn't being caught

* Revert change

* Type fixes

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2025-03-18 19:51:02 +00:00
Tushar Bhatt ff92979311 fix: spacing (#20172) 2025-03-18 17:37:24 +00:00
Anik Dhabal Babu f63edef48d fix: flaky e2e tests (#20168)
* fix: flaky e2e tests

* update

* fix
2025-03-18 22:30:00 +05:30
Rajiv Sahal 37e56ebbf9 dont auto accept user into an org when creating platform org (#20125) 2025-03-18 16:12:26 +00:00
07c38f1f33 test: Add tests for Booker component (#19051)
* feat: Don't allow unavailable slot's booking form to be visible - Send back to slots listing

* Improvements

* feat: Add env variables and strategies to prevent booking failures

- Added new environment variables for configuring slot reservation and availability checks
- Implemented strategies to handle concurrent bookings and slot availability
- Updated Booker component and related files to support new reservation and slot checking mechanisms
- Added README with detailed explanation of booking prevention strategies
- Configured dynamic intervals for slot and reservation queries

* docs: Update Booker README with detailed slot reservation and availability explanation

- Clarified slot reservation behavior when multiple users access the same booking page
- Added details about `getSchedule` refetching strategies and timing
- Explained how slot availability is detected and communicated to users

* Support for skip confirmation form flow

* feat: Enhance slot availability and reservation mechanism

- Refactored slot availability checking to support multiple slots
- Added tentative selected timeslots to improve booking UX
- Updated trpc handler to check availability for multiple slots
- Introduced new store methods for managing tentative slot selections
- Improved slot reservation and availability status tracking

* refactor: Improve slot availability quick check mechanism

- Renamed and restructured slot availability check parameters
- Extracted quick availability checks into a separate hook
- Updated type definitions for slot status and availability checks
- Simplified slot availability logic in Booker and related components
- Maintained cached state for quick availability checks

* Add tests booker

* Support minimu booking check too

* change data-testid

* Rneame

* Fix race condition with uid cookie not set

* fix tests

* Remove unsded variable

* fixup! Merge branch 'main' into dont-allow-booking-form-with-unavaialble-slot

* Fix the unavailability bug when only seconds differ

* Handle column view that doesnt have selected date and also add tests for isTimeSlotAavailable

* Fi ts and unit tests

---------

Co-authored-by: Hariom Balhara <hariombalhara@gmgmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Morgan Vernay <morgan@cal.com>
2025-03-18 13:02:20 +01:00
Gene Parmesan Thomas 192238f4bf fix: UID cookie isn't accessible/created when in embed as it is third party context. (#19859)
* [CAL-5240] Embeds: UID cookie isn't accessible/created when in embed as it is third party context.

* comments
2025-03-18 16:22:55 +05:30
Lauris SkraucisandMorgan 94a26031a8 feat: v2 api phone based organization team event types (#20134)
* refactor: input event types service remove unused code

* feat: toggle if email field is hidden

* chore: ensure event type has email or attendee phone field required

* chore: ensure non-org team cant have phone only event type

* chore: ensure users cant have phone only event type

* chore: ensure org team can have phone only event type

* chore: bump libraries

* docs

* refactor: confirm failing test response message

* fix: unit test

* chore: default organizer booking location docs

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2025-03-18 11:34:35 +02:00
Joe Au-Yeung e9f20b1a0b fix: Salesforce - fallback to creating lead (#20137)
* Fallback creating lead

* Add tests

* Type fix

* Type fixes

* Type fix

* Fix tests
2025-03-18 14:29:09 +05:30
Morgan 7cb9e2b99b chore: create org membership should be an upsert apiv2 (#20129) 2025-03-17 21:07:32 +00:00
Morgan dd45d8cdbc fix: don't remind platform users in reminder cron (#20056) 2025-03-17 23:00:25 +02:00
Hariom Balhara 3a181f1dac fix: Salesforce Stricter Website querying to prevent wrong account match during contact owner check (#20133)
* Make website check stricter in contact ownership flow as well

* Add test
2025-03-17 17:34:36 -03:00
Morgan c3abfa51ba chore: stringify body for request interceptor/middleware (#20131)
* chore: stringify body for request interceptor/middleware

* rename jsonBody to jsonBodyString
2025-03-17 19:45:37 +00:00
Alex van Andel 6498af167a refactor: Delete dead code that is not safe to use (#20141) 2025-03-17 17:23:39 +00:00
Keith WilliamsandhbjORbj 30dce7bacd perf: Move apps procedures off of loggedInViewer (#20112)
* perf: Slim down loggedInViewer tRPC router

* Fixed trpc client calls for new routes

* Moved bookingUnconfirmedCount

* Moved getUserTopBanners to me router

* Moved shouldVerifyEmail to me router

* Moved i18n from public to its own router

* fix ssrInit

* fix ssrInit usage

* fix type check

* better naming

* fix

* fix

* Fix types

* Removed used of importHandler

* perf: Move apps procedures off of loggedInViewer

* Moved 2 more apps-related procedures

---------

Co-authored-by: hbjORbj <sldisek783@gmail.com>
2025-03-17 17:18:19 +00:00
Keith Williams 3be9e1bd38 perf: Remove slots router from publicViewer (#20128) 2025-03-17 09:28:40 -07:00
Lauris Skraucis db52f44293 fix: atoms making requests to customers' domains (#20084)
* refactor: centralize useSchedule hook with isInit

* refactor: centralize useSchedules hook with isInit

* refactor: useConnectedCalendars enabled if isInit

* chore: some type fixes

* chore: type fix

* fix: import paths

* driveby: oauth clients docs
2025-03-17 10:43:39 +02:00
Keith WilliamsandhbjORbj 20d6eb5200 perf: Slim down loggedInViewer tRPC router (#20111)
* perf: Slim down loggedInViewer tRPC router

* Fixed trpc client calls for new routes

* Moved bookingUnconfirmedCount

* Moved getUserTopBanners to me router

* Moved shouldVerifyEmail to me router

* Moved i18n from public to its own router

* fix ssrInit

* fix ssrInit usage

* fix type check

* better naming

* fix

* fix

* Fix types

* Removed used of importHandler

---------

Co-authored-by: hbjORbj <sldisek783@gmail.com>
2025-03-16 18:22:48 -07:00
Keith Williams 424d94f16d feat: Use Watchlist on sending email verifications (#20114) 2025-03-16 14:20:20 +00:00
GitHub Actions 73e7d6f986 chore: release v5.1.0
Post release / release (push) Canceled after 0s
2025-03-15 11:11:37 +00:00
Alex van Andel 145001d26e chore: Bump typescript version in @calcom/ui (#19766)
* chore: Bump typescript version in @calcom/ui

* Reduce required types from @calcom/types to just used ones
2025-03-15 06:43:05 -03:00
5d2d7ce979 fix: German translation for seats_availability text (#20086)
* Fix German translation for seats_availability text

* enhancements

---------

Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
Co-authored-by: Tushar <tusharbhatt0135@gmail.com>
2025-03-15 08:12:04 +00:00
GitHub Actions 4dc21ab2b0 chore: release v5.0.20
Post release / release (push) Canceled after 0s
2025-03-14 19:07:01 +00:00
Anik Dhabal Babu df6ee73e05 fix: schedule.get unneccsary called from avaiablity view (#20094) 2025-03-14 18:31:35 +00:00
Keith Williams 587af33179 perf: Use built types for appRouter (#20083)
* perf: Use built types for appRouter

* Got it working just for server

* Added dependency to turbo.json
2025-03-14 10:42:45 -07:00
Morgan 3b381b037c fix: incorrect managed user username format on update (#20090) 2025-03-14 15:36:24 +00:00
0968fc56eb feat: add utm tracking on booking (#19965)
* add tracking to booking + pass to handleNewBooking

* save tracking to booking

* return undefined if no utm params

* add test for handleNewBooking

* fix type error in test

* refactor code

* code clean up

* code clean up

* add missing import

* fix type import

* add utm params to insights (#20063)

* add utm params to insights

* remove duplicates

* remove duplicate columns

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
2025-03-14 08:21:55 -07:00
Lauris Skraucis 13044cb5b0 feat: v2 bookings createdAt filter (#20054)
* feat: add before and after updated filter to core handler

* feat: afterCreatedAt and beforeCreatedAt v2 bookings filters

* wip

* finish

* chore: bump libraries

* fix: tests

* refactor: implement feedback
2025-03-14 15:49:57 +02:00
Morgan 580b03f2b9 chore: handle conferencing callback missing state error (#20081) 2025-03-14 15:49:04 +02:00
amrit 543d4bb6b3 fix: invitati instead of inviati (#20085) 2025-03-14 17:57:19 +05:30
Sarvesh Bajaj 21d87d5a80 fix: No type errors in build (#20075) 2025-03-14 07:39:10 +00:00
Lauris Skraucis 194719e7ba docs: v2 set cal-api-version header automatically (#20042) 2025-03-14 07:37:14 +00:00
Benny JooandKeith Williams dc666fc770 perf: fix paths in tailwind preset + ignore node modules in Platform tailwind config (#20067)
* fix paths in packages/config/tailwind preset

* ignore node_modules in platform tailwind preset

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2025-03-14 04:18:16 -03:00
Morgan 50a76e6b27 fix: delegation credential not found (#20055) 2025-03-14 06:13:41 +00:00
Parteek malikandamrit be4d3170db fix: synced no-show state after mutation (#20015)
Co-authored-by: amrit <iamamrit27@gmail.com>
2025-03-14 05:49:59 +00:00
Anik Dhabal Babu 7b97002230 fix: flaky test in ooo e2e (#20073)
* fix: flaky test in ooo e2e

* tweak
2025-03-14 08:58:13 +05:30
Benny Joo 6319d63e1f fix: client-export warning for enterprise page (#20077) 2025-03-13 21:48:40 +00:00
Calcom BotandLingo.dev 2e18478b5c feat: update translations via @lingodotdev (#20074)
Co-authored-by: Lingo.dev <support@lingo.dev>
2025-03-13 20:08:24 +00:00
GitHub Actions 0ef93edd56 chore: release v5.0.19
Post release / release (push) Canceled after 0s
2025-03-13 19:08:48 +00:00
Eunjae LeeandBenny Joo d6a3661e58 fix: support more className-related props at DataTable (#19953)
* fix: support more className-related props at DataTable

* update style

* fix type error

* use relative import

---------

Co-authored-by: Benny Joo <sldisek783@gmail.com>
2025-03-13 14:25:57 -04:00
Alex van Andel be404436d7 fix: Do not self import @calcom/ui (#20050)
* fix: Do not self import @calcom/ui

* Make translations optional

* Fix mocking implementation of Button (never worked)

* Ensure other libraries can resolve AppListCard
2025-03-13 18:17:42 +00:00
Somay Chauhan b1ebefb854 feat: don't update query params for booker atom (#20000)
* feat: Add `omitUpdatingParams` prop to Booker store for query param control

* remove console

* Update booking.tsx

* fix: type check failing

* undo changes

* feat: Expose `onBookerStateChange` callback to track `booker-store` updates

* add `onBookerStateChange` docs

* Update store.ts

* added `allowUpdatingUrlParams` for backwards compatibility
2025-03-13 23:47:24 +05:30
f8aa2701b6 fix: add booking uid and link to insights routing downloads (#20007)
* fix: add booking uid and link to insights routing downloads

* minor change

---------

Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
Co-authored-by: Benny Joo <sldisek783@gmail.com>
2025-03-13 14:11:02 -04:00
Keith Williams 034dfc8e6b perf: Ignore node_modules in Tailwind preset (#20058) 2025-03-13 18:05:58 +00:00
sean-brydon a6b6898697 add conditional (#20057) 2025-03-13 15:58:19 +00:00
Udit Takkar 0e5a0bfa7b feat: store cal video recording on s3 (#17050)
* feat: store cal video recording on s3

* fix: type error

* chore: update name and add condition
2025-03-13 12:46:10 -03:00
Eunjae Lee 6c01f5eace fix: reset page on filter change and remove client-side filtering (#20043)
* fix: reset page on filter change

* remove unused filterFn
2025-03-13 15:25:10 +00:00
VachmaraandTushar Bhatt ef2c34c55f fix: Intercom app - regex valid url (#20033)
* fix: regex valid url

* fix: only https urls

---------

Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
2025-03-13 13:46:23 +00:00
Udit Takkar 7c1582c06f fix: circular dependency in handleNewBooking (#19890)
* fix: circular dependency

* fix: get orginial scheduled booking

* fix: create logger

* fix: type err
2025-03-13 13:45:00 +00:00
367e2666c6 fix: apply standard pagination to /bookings (#19973)
* fix: apply standard pagination to /bookings

* rename params

* fix type error

---------

Co-authored-by: Benny Joo <sldisek783@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2025-03-13 12:32:07 +00:00
Hariom Balhara 209a401113 fix: Enable feature for embeds when 100 pc is set (#20003)
* Enable feature for embeds when 100 pc is set

* Dont consider reserved status for quick availability check
2025-03-13 17:16:43 +05:30
837349e3d3 feat: Organization onboarding improvement - Payment before creation of org but onboarding doesn't require payment (#18990)
* create seed for org upgrade

* migrate about/new pages

* pages refactor + use persistant zustand store

* invited members view + confirm button

* add check to org slug

* check reserved subdomain

* remove quotes from subdomain env var

* intro to creation + billing

* create price

* fix types

* createWithPaymentIntent + permission check on teams

* open stripe link in popup

* intro to tests

* organization price and seat override tests

* move permissions to a new permission service

* update env and permission check

* intro to paid invoice trigger

* dont use subId

* wip

* Get e2e working with migration of teams and members

* fix ts errors

* Get flow working again

* Fix various issues and refactor

* Some more fixes

* Fix wrong page route link

* Platform onboarding fix and moving members of team to org rix

* Platform onboarding fix and moving members of team to org rix

* Fix tests, found a bug

* Get custom price flow working

* Get admin impersonation flow working for a non-existent user

* Fix unit test

* Admin onboarding handover

* fix admin onboarding tests

* fix ts error

* Get updateQuantity working

* More fixes

* fix test

* fix schema name

* Add tests

* Add missing file

* More tests

* Add more tests

* fix mt-2 moving down input into overflow

* fix handover layout removing HOC

* Update PR_TODO.md

* fix ts error due to merge conflict

---------

Co-authored-by: Hariom Balhara <hariombalhara@gmgmail.com>
Co-authored-by: Hariom <hariombalhara@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
2025-03-13 11:17:43 +00:00
Hariom Balhara e66602aa5c Disable connected forms - Temporarily (#20040) 2025-03-13 11:14:23 +00:00
nizzyandUdit Takkar 082f8e1b04 fix: disable plain for self hosters (#19853)
* fix: disable plain for self hosters

* fix type error

* test update

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2025-03-13 08:50:16 +00:00
07716474d3 feat: Fetch previous ooo records using DateRangeFilter (#19596)
* teamMates OOO is visible for teamMember

* Update packages/features/settings/outOfOffice/CreateOrEditOutOfOfficeModal.tsx

Co-authored-by: Omar López <zomars@me.com>

* nit

* fixed tests, admin chk reqd for forMembers only

* check reqd in case of teamOOO only

* datefilter with default and improved search filter

* fix preset options

* fix ooo tests, due to default date filter

* ensure ooo test's wont fail when run on different dates of the month, due to default filter

* improvement: instead of 'Date to Year' safer is customRange

* added test for non-admin team ooo

* show disabled icons instead of notshowing

* tests for dateRangeFilters

* some more daterangefilter tests

* nit

* include only fetch prev records in this pr

* nit

* chore

* chore

* review comments

* chore

* update to fix tests

* nit

* nit

* update pagination mode

* removed unrelated changes

* remove unrelated changes

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
2025-03-13 12:54:20 +05:30
Udit Takkar f335cc4269 fix: hide slot unavailable message for instant meetings (#20002)
* fix: hide slot unavailable message for instant meetings

* fix:
2025-03-13 07:22:02 +00:00
Benny Joo f19750cfaf chore: remove campsite from app store (#20032) 2025-03-12 23:23:19 -04:00
Benny Joo 46810f69ca fix: no-meeting-found is exporting client component from RSC (#20027) 2025-03-12 17:33:05 +00:00
amrit a7247c594b fix: round robin locale err (#20024)
* fix: round robin locale err

* Update bookings-single-view.tsx
2025-03-12 17:01:17 +00:00
Morgan 8de7876f72 chore: add CalendarService Exception Filter api v2 (#20021) 2025-03-12 16:24:50 +00:00
Morgan 49f75f5070 fix: lbraries/app-store missing tsconfig (#20006) 2025-03-12 14:55:18 +00:00
Lauris Skraucis 4ff543efc7 docs: v2 reschedule and cancel booking (#20011) 2025-03-12 16:36:32 +02:00
sean-brydon 8af5145b56 fixes extra span causing issues (#20004) 2025-03-12 12:12:13 +00:00
Lauris Skraucis a31242cdc5 refactor: v2 booking locations (#19613)
* refactor: v2 booking locations

* regenerate docs

* remove describe only

* update docs

* tests

* tests

* remove as any

* fix tests

* log

* remove unused dependency

* fix: bookings module dependency

* fix test

* fix: allow all integrations as booking location

* refactor: add organizersDefaultApp

* refactor: handle all integrations
2025-03-12 13:03:01 +01:00
Udit Takkar e20df1e7aa fix: remove Trans component (#20008) 2025-03-12 11:24:20 +00:00
sean-brydon 960a368175 fix sentry errors (#20005) 2025-03-12 10:53:17 +00:00
sean-brydonandTushar Bhatt 49cb86dfe9 fix: add improved logging and decrease ttl (#19979)
* add improved logging and decrease ttl

* fix test TTL time

---------

Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
2025-03-12 10:26:07 +00:00
Udit Takkar b644ec58af fix: push notification page (#19999) 2025-03-12 10:09:55 +00:00
GitHub Actions bd92b89bf0 chore: release v5.0.18
Post release / release (push) Canceled after 0s
2025-03-12 07:42:40 +00:00
Benny Joo 495ac74255 fix: importing from @calcom/emails exposed daily-video api route to client code (#19994)
* create a new file

* use from new file

* add comment
2025-03-12 06:05:53 +00:00
Calcom BotandLingo.dev c3d35fd8c8 feat: update translations via @lingodotdev (#19992)
Co-authored-by: Lingo.dev <support@lingo.dev>
2025-03-12 04:48:31 +00:00
Benny Joo 87bc8d649a feat: add sentry report wrapper to every api route in appDir (#19966)
* update oauth tests

* fix parseRequestData

* add logging

* add fallback

* use ??

* updates

* use parseUrlFormData in saml/callback

* update tests

* fix

* remove log

* update

* any -> unknown

* use HttpErrors

* addressed

* unknown -> any

* feat: Add entry report wrapper to every api route in appDir

* fix

* fix unit test

* fix
2025-03-11 23:38:49 -04:00
GitHub Actions 1e9f3a8060 chore: release v5.0.17
Post release / release (push) Canceled after 0s
2025-03-11 22:22:13 +00:00
Morgan bc1d73e9fe fix: broken metadata import from platform/types (#19986)
* fix: broken metadata import from platform/types

* fix: OAuthClientUsersService instead of this in oauth-clients-users.service.ts
2025-03-11 22:05:40 +00:00
GitHub Actions eca01be58c chore: release v5.0.16 2025-03-11 21:51:10 +00:00
4ad1127184 refactor: extract schedules from libraries barrel file (#19838)
* refactor: extract schedules from libraries barrel file

* cleanup: remove functions not being used by schedules

* fix tsconfig ]

* bump platform libs

* bump platform libs

---------

Co-authored-by: Morgan Vernay <morgan@cal.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2025-03-11 19:07:06 +00:00
Lauris SkraucisandMorgan Vernay 76b9b9b0fe fix: v2 rescheduled booking with confirmation (#19833)
* fix: old bookings controller

* fix: new bookings controller

* fix: BookerPlatformWrapper pass rescheduledBy to BookerComponent

* rescheduleBy email

* fix test

* remove log

* centralize getOAuthUserEmail, examples app rescheduledBy

* revert

* finalize

* fix: old bookings rescheduling

* unify email validation

* update input description

* chore: move booker platform wrapper types

---------

Co-authored-by: Morgan Vernay <morgan@cal.com>
2025-03-11 20:04:59 +01:00
Benny Joo ff5c6de5da chore: remove code not needed in defaultResponder (#19976)
* 1 line change

* fix test
2025-03-11 17:47:01 +00:00
Eunjae Lee eafb61f515 fix: apply standard pagination to /insights/routing (#19950)
* fix: apply standard pagination to /insights/routing

* fix type error

* max limit
2025-03-11 16:51:11 +00:00
Benny Joo d0ca0dab53 fix: do not export client components directly from RSC (#19971) 2025-03-11 16:11:30 +00:00
Lauris SkraucisandMorgan e6f561c3d7 fix: v2 team metadata (#19939)
* refactor: allow metadata to also receive numbers and booleans

* refactor: Team metadata

* refactor: Managed orgs metadata

* refactor: Team metadata

* test: organizations teams create team with metadata

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2025-03-11 15:03:30 +00:00
Lauris Skraucis b7c5cfcf02 feat: v2 get managed users by email (#19526)
* feat: v2 get managed users by email

* docs

* fix: offset query param not showing up in docs

* finish merge main

* fix: check if each email in emails query param is email
2025-03-11 15:01:43 +00:00
Hariom BalharaandMorgan Vernay b1bcbe9dfb feat: calendar-links API (#19777)
* refactor: Remove intervalLimits from @calcom/lib and export directly (#19710)
* refactor: Remove intervalLimits from @calcom/lib and export directly

* Tackle other places that use parseBookingLimit/parseDurationLimit

* More type fixups that were hidden by previous fails

* Fixed up booking-limits file

* Remove server-only

## What does this PR do?

<!-- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. -->

- Fixes #XXXX (GitHub issue number)
- Fixes CAL-XXXX (Linear issue number - should be visible at the bottom of the GitHub issue description)

## Visual Demo (For contributors especially)

A visual demonstration is strongly recommended, for both the original and new change **(video / image - any one)**.

#### Video Demo (if applicable):

- Show screen recordings of the issue or feature.
- Demonstrate how to reproduce the issue, the behavior before and after the change.

#### Image Demo (if applicable):

- Add side-by-side screenshots of the original and updated change.
- Highlight any significant change(s).

## Mandatory Tasks (DO NOT REMOVE)

- [ ] I have self-reviewed the code (A decent size PR without self-review might be rejected).
- [ ] I have updated the developer docs in /docs if this PR makes changes that would require a [documentation change](https://cal.com/docs). If N/A, write N/A here and check the checkbox.
- [ ] I confirm automated tests are in place that prove my fix is effective or that my feature works.

## How should this be tested?

<!-- Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration. Write details that help to start the tests -->

- Are there environment variables that should be set?
- What are the minimal test data to have?
- What is expected (happy path) to have (input and output)?
- Any other important info that could help to test that PR

## Checklist

<!-- Remove bullet points below that don't apply to you -->

- I haven't read the [contributing guide](https://github.com/calcom/cal.com/blob/main/CONTRIBUTING.md)
- My code doesn't follow the style guidelines of this project
- I haven't commented my code, particularly in hard-to-understand areas
- I haven't checked if my changes generate no new warnings

* Add unit tests and e2e

* fix ts errors

* Add comment

* bump platform libs

* fix: yarn-lock

* fix yarn.lock

* doc update

* Add isString

---------

Co-authored-by: Morgan Vernay <morgan@cal.com>
2025-03-11 17:01:32 +02:00
Steven Tey 19e1714794 Upgrade dub to latest version for dual-sided incentives (#19885) 2025-03-11 14:03:54 +00:00
Benny Joo e3e624464d fix: update wrong oauth tests and parseRequestData util (#19958)
* update oauth tests

* fix parseRequestData

* add logging

* add fallback

* use ??

* updates

* use parseUrlFormData in saml/callback

* update tests

* fix

* remove log

* update

* any -> unknown

* use HttpErrors

* addressed

* unknown -> any
2025-03-11 14:01:48 +00:00
amrit dc3c7b43bf fix: correct translation key for host and attendee emails (#19962) 2025-03-11 13:35:59 +00:00
Morgan 83736785b7 fix: catch error for getOwnerId with invalid token (#19949) 2025-03-11 13:27:20 +02:00
Rajiv Sahal 83b6b7caf9 chore: update hooks sections for platform docs (#19726)
* export custom hooks to create event types

* update hooks page

* separate hooks into their own files
2025-03-11 16:57:05 +05:30
GitHub Actions 6fc3ca539b chore: release v5.0.15
Post release / release (push) Canceled after 0s
2025-03-11 10:43:26 +00:00
Keith Williams 9d58d21e85 perf: Remove dayjs import from main constants.ts file (#19644) 2025-03-11 10:37:51 +00:00
Lauris Skraucis 43f6db194e test: managed org creator profiles (#19944) 2025-03-11 10:13:50 +00:00
Morgan 43156fa4d7 refactor: extract emails from libraries barrel file (#19836)
* refactor: extract emails from libraries barrel file

* fixup! Merge branch 'main' into refactor-libraries-barrel-emails

* fix: add missing configs

* add sendSignupToOrganizationEmail to barrel email file

* bump libs
2025-03-11 11:36:21 +02:00
Eunjae LeeandBenny Joo 7eee2728a2 feat: implement standard pagination for org member list (#19802)
* feat: implement standard pagination for org member list

* fix type error

* i18n for pagination

* add paginationMode

* apply the change to PlatformManagedUsersTable

* replace useInfiniteQuery with useQuery

* fix type error

* fix type error

* fix type error

* fix type error

* update comment

* remove optimistic update

* minor changes

* update usage on nuqs

---------

Co-authored-by: Benny Joo <sldisek783@gmail.com>
2025-03-11 09:55:41 +01:00
Somay Chauhan 861e3aa24f fix: extracted app-store and DWD platform-libraries imports to a /app-store barrel file (#19938)
* fix: platform libraries barrel file extract app-store

* extracted DWD to app-store barrel file

* removed getCalendar as not used in api/v2

* bump platform-libraries
2025-03-11 08:30:37 +00:00
Benny Joo 517d509321 fix: req.formData() should be used in some API routes (#19930) 2025-03-11 07:51:33 +00:00
Udit Takkar 2f9a853023 chore: pass metadata (#19712)
* chore: pass metadata

* fix: type error

* chore: pass videoCallUrl
2025-03-11 04:34:14 -03:00
Udit Takkar b0125e1a05 feat: sound on browser push notification (#18548)
* feat: sound on notification

* refactor: add load audio

* chore: remove

* fix: type error

* fix: notification

* fix: prevent multiple initialization
=

* feat: create notification page

* chore: update the mp3 file
2025-03-11 11:49:51 +05:30
Tushar Bhatt 4daad2ac08 fix/view (#19928) 2025-03-11 03:04:20 +00:00
Alex van Andel 9d28f5574d fix: Remove hardcoded Node version from engines 2025-03-10 23:35:00 +00:00
GitHub Actions be525a27df chore: release v5.0.14
Post release / release (push) Canceled after 0s
2025-03-10 22:44:00 +00:00
Calcom BotandLingo.dev df037b008f feat: update translations via @lingodotdev (#19927)
Co-authored-by: Lingo.dev <support@lingo.dev>
2025-03-10 21:12:15 +00:00
amritandUdit Takkar 8fb4df4b8c fix: duplicate value found in common.json error (#19693)
* fix: duplicate value found in common.json error

* Remove subteam invitation translation from locales

* verify_email_email_button to verify_email_button

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2025-03-10 19:19:32 +00:00
6758654796 feat: round robin reset interval for teams (#19441)
* add component to set interval

* fix variable namings

* fetch data for given interval

* migration

* fix type errors in test

* test correct filtering for interval

* fix start end end times to interval

* fix type error

* pass rrResetInterval to getOrderedListOfLuckyUsers

* fix imports

* add translations

* add missing translation

* code clean up

* fix type errors

* fix type error

* fix import

* add translation

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2025-03-10 17:34:47 +00:00
Alex van AndelandMorgan Vernay 1e86f1046b chore: Fix build failure due to trpc inference depth (#19884)
* chore: Fix build failure due to trpc inference depth

* Also bump platform libraries typescript version

* fix: e2e test using unassigned variable

* chore: bump libraries to 0.0.142

---------

Co-authored-by: Morgan Vernay <morgan@cal.com>
2025-03-10 17:06:05 +00:00
Eunjae LeeandAlex van Andel d3775cd920 fix: update filter styles on org member list (#19918)
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2025-03-10 17:01:37 +00:00
Benny JooandAnik Dhabal Babu 3ab053bf17 chore: migrate oauth APIs to app router (#19824)
* migrate api/auth/oauth/me

* migrate the rest

* fix

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-03-10 17:56:53 +01:00
Benny Joo 4a4bee7140 chore: Remove typeform app (#19911)
* remove typeform app

* update yarn.lock
2025-03-10 16:54:20 +00:00
Benny Joo 9ba85ea0b2 chore: missing pieces to migrated apis (#19915)
* fix

* refactor
2025-03-10 15:24:03 +00:00
avnish100 d1d1472f3e fix: removed rounded top corners on webhook form (#19905) 2025-03-10 14:54:43 +00:00
Benny Jooandsean-brydon 8d18b3da38 chore: migrate saml/oidc APIs to App Router (#19515)
* migrate oidc

* migrate saml apis

* remove comment

* add force-dynamic

* fix

* fix

* refactor

---------

Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2025-03-10 14:36:11 +00:00
Benny Joo 631c5f7c53 migrate webhookTriggers and workflows apis (#19875) 2025-03-10 14:35:11 +00:00
f2ca2bdc83 fix: add badge for disabled workflow actions and templates in trial mode (#19675)
* add badge for trial mode

* fix tooltip issue

* fix typo

* add inactive team plan badge

* change to one salesforce request

* clean up

* fix type error

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
2025-03-10 15:34:29 +01:00
Rajiv Sahal bb36807689 feat: update platform onboarding docs (#17941)
* update images

* restructure getting started for platform

* add steps on setting up a platform account

* PR feedback

* fixup

* add setup page

* move over setup to a separate page

* update images
2025-03-10 20:02:24 +05:30
Keith Williams 7ed32d3c77 perf: Reduce imports by moving bookings zod out of utils (#19869) 2025-03-10 13:54:05 +00:00
Morgan 5d84179345 refactor: circular dependencies platform packages (#19903)
* refactor: circular dependencies platform packages

* fixup! refactor: circular dependencies platform packages

* fixup! fixup! refactor: circular dependencies platform packages

* fix: circular deps in BaseCalProvider
2025-03-10 13:25:01 +00:00
cb437231ad fix: platform libraries barrel file extract event-types (#19846)
* fix: event-types

* chore: added "+ group link" tip (#19809)

* added + link tip

* nit

---------

Co-authored-by: amrit <iamamrit27@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>

* Added fix to solve the id TypeError (#19810)

Co-authored-by: amrit <iamamrit27@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>

* Revert "fix: prevent background scroll on opening timezone select menu (#19789)" (#19814)

This reverts commit 1a96149ce3.

* fix: Calendar events attached to workflow emails are missing meeting link (#19790)

* fixup! fix: Calendar events attached to workflow emails are missing meeting link (#19790)

* chore: fix type errors

* fix: wrong import in e2e file

* fix: tsconfig api v2 for /event-types

* fix: import and refactor

* fixed remaning type issues

* undo extra changes

* bump platform libs

* Update event-types.controller.e2e-spec.ts

* fix: test with seats enabled

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: amrit <iamamrit27@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
Co-authored-by: Harshit Singh <73997189+harshit078@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Morgan Vernay <morgan@cal.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2025-03-10 12:34:53 +00:00
Somay Chauhan d47a5be659 fix: add documentaion to blockUnconfirmedBookingsInBooker (#19894)
* fix: add documentaion to `blockUnconfirmedBookingsInBooker`

* fix documentation of ConfirmationPolicy.type

* Update confirmation-policy.input.ts
2025-03-10 09:31:49 -03:00
sean-brydon e89211782c fix: calcom trpc circle deps slots handlers (#19896)
* remove validator file and include inside handler

* fixes slots and handler circle dep

* fix type in tests
2025-03-10 11:29:51 +00:00
sean-brydon fd454061e7 fix profile circle dep (#19892) 2025-03-10 11:05:14 +00:00
Rajiv SahalandMorgan Vernay 277ce1c244 refactor: extract slots from libraries barrel file (#19895)
* refactor: extract slots fn from libraries barrel file

* bump platform libs

---------

Co-authored-by: Morgan Vernay <morgan@cal.com>
2025-03-10 10:33:29 +00:00
luzpaz 1848cd55b1 fix: fix playwright typos (#19794)
Found via `codespell -q 3 -S "*.svg,./apps/web/public/static/locales,./packages/app-store/stripepayment/lib/currencyOptions.ts,./packages/lib/freeEmailDomainCheck/freeEmailDomains.ts" -L afterall,atleast,datea,fo,incase,ist,nam,noone,notin,optionel,perview,pris,reccuring`
2025-03-10 07:39:58 +00:00
Anik Dhabal Babu b0e3cc4dd2 chore: add logs for debugging (#19851) 2025-03-10 03:34:52 +00:00
Morgan b3e344d226 chore: decouple calcom/ui/Tooltip from atoms (#19798)
* chore: decouple calcom/ui from atoms #1

* recover input from main

* fix: vite config dialog
2025-03-10 01:43:01 +00:00
Suyash Srivastava 810f3b834c fix:small typo with email (#19879) 2025-03-10 01:22:13 +00:00
Hariom Balhara bf1eacfd9b Ignore faulty org (#19825) 2025-03-10 00:58:48 +00:00
c5c654c879 feat: no-show confirmation does not pop up for single attendee (#19806)
* feat: no-show marking  confirmation does not pop up for single attendee bookings

* fix: Add toggle for no-show status

* simplify logic

* feat: Centralize no-show handling logic in BookingListItem

* Update BookingListItem.tsx

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-03-09 17:06:03 +00:00
sean-brydonandBenny Joo d1ce04edc1 chore: move miscellaneous small api pages to app dir api (#19699)
* version and verify-booking-token

* username route

* api/me

* logo route

* render email in dev

* link route

* future

* nope and geo location

* referal link and daily

* intercom route and dynamic variables

* intercom route and dynamic variables

* scim 2.0 and helpscout route

* app credentials

* api/book events

* fix daily import path in tests

* fix buildLegacyRequest generation

* fix type errors

* migrate the /teams/ routes

* move cron jobs

* fix daily import path in tests

* fix buildLegacyRequest generation

* fix type errors

* migrate the /teams/ routes

* move cron jobs

* Revert "api/book events"

This reverts commit 607a32fb5b754cad090c2d0cbf64a68f990e220e.

* mock next server NextResponse to fix daily video

* add default responder to teams api

* fix search params

* uses nextUrl.searchParams instead of new url

* uses nextUrl.searchParams instead of new url

* remove outdated api config

* remove app dir version of inbound dynamic variables

* restore pages version of inbound variables

* fix missing code from stupid cursor

---------

Co-authored-by: Benny Joo <sldisek783@gmail.com>
2025-03-09 11:56:56 -04:00
Benny JooandAnik Dhabal Babu 1f1862e526 chore: migrate two-factor APIs to app router (#19826)
* wip

* remove comment

* clean up

* clean up

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-03-09 15:40:27 +00:00
Benny Joo 63dfc3376c chore: remove empty path from middleware matcher (#19860) 2025-03-09 15:38:04 +00:00
Adam BashaandKeith Williams 3166db9bcf chore: update robots.txt to disallow /settings and /my-account (#19867)
* chore: update robots.txt to disallow /settings and /my-account

* Update robots.txt

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2025-03-09 08:53:34 +00:00
Vijay f8af9dc17e fix: failing tests during DST transition (#19862)
* make the test dst transition proof

* fix other test similarly
2025-03-09 00:43:42 +00:00
Benny Joo ae659b5176 chore: migrate forgot-password, reset-password, auth/setup apis to App Router (#19512)
* chore: migrate /api/auth/changepw

* migrate forgot-password and reset-password

* remove unneeded export

* rename request to req for easier review

* migrate auth/setup

* remove setup

* refactor

* remove redundant code

* fix

* remove changepw api and unused ui
2025-03-07 14:43:36 -07:00
Eunjae Lee 33226f268d fix: date range on /bookings (#19785) 2025-03-07 21:54:49 +01:00
sean-brydonandAnik Dhabal Babu 7e2a0d1a1b fix: calcom UI circle dep (#19839)
* editor circle dep fix

* circle dep fix in select

* fix barrel export types

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-03-07 09:26:26 -07:00
a385bd4d5f feat: team ooo read-only for non-admin (#19700)
* feat team ooo read-only for non-admin

* Update apps/web/playwright/out-of-office.e2e.ts

Co-authored-by: Younes Henni <youneshenniwrites@gmail.com>

* updated error msgs with t

* nit

* nit

---------

Co-authored-by: Younes Henni <youneshenniwrites@gmail.com>
Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
2025-03-07 16:19:30 +00:00
Anik Dhabal Babu 4b4bc28f1d fix: Calendar events attached to workflow emails are missing meeting link (#19790) 2025-03-07 14:41:51 +00:00
Anik Dhabal Babu 316fba908b Revert "fix: prevent background scroll on opening timezone select menu (#19789)" (#19814)
This reverts commit 1a96149ce3.
2025-03-07 14:39:09 +00:00
d40d282912 Added fix to solve the id TypeError (#19810)
Co-authored-by: amrit <iamamrit27@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
2025-03-07 14:26:36 +00:00
e81df6fdeb chore: added "+ group link" tip (#19809)
* added + link tip

* nit

---------

Co-authored-by: amrit <iamamrit27@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
2025-03-07 14:23:51 +00:00
Hariom Balhara 76420411b9 fix: fixes the crash when empty users or hosts array is passed to _getDelegationCredentialsMapPerUser (#19834)
* fix crash when no users are there

* typo fix
2025-03-07 13:11:07 +00:00
Keith Williams 7ec1617dc2 chore: Update CODEOWNERS for package.json files (#19831) 2025-03-07 11:02:33 +00:00
Rajiv Sahal 870cadd81f fix: dry run improvements for v2 (#19196)
* add dry run param for v2 getSlots

* fixup

* revert changes

* dont reserve a slot for booking dry run

* accept dry run prop as a parameter instead of computing

* fix merge conflicts

* fixup

* dont reserve slots for booking dry run
2025-03-07 14:49:54 +05:30
amrit 04c581a25e docs: Update README (#19815) 2025-03-07 10:32:33 +05:30
Syed Ali Shahbaz 5ef131333d update SAML guide (#19778) 2025-03-07 09:00:53 +05:30
GitHub Actions c262220f0f chore: release v5.0.13
Post release / release (push) Canceled after 0s
2025-03-07 00:38:07 +00:00
Morgan b5e08ea80f fix: api v2 create event-type with delegation credentials (#19797) 2025-03-06 19:46:46 +02:00
Joe Au-Yeung 00ee1ef441 refactor: Salesforce - account query based on website (#19799)
* Refactor account lookup to include website prefixes

* Type fix
2025-03-06 21:53:49 +05:30
Rajiv Sahal 6cdc9c9f59 fix invalid access token bug (#19793) 2025-03-06 14:51:39 +00:00
Pranav Rajveerandamrit 56d1998f0f chore: rename virtual-queue to router-position (#19774)
Co-authored-by: amrit <iamamrit27@gmail.com>
2025-03-06 19:32:30 +05:30
Udit Takkar d59fdf21fd feat: display country code from IP (#19788)
* feat: display country code from IP

* chore: remove

* fix: unit test
2025-03-06 19:16:41 +05:30
suraj 1a96149ce3 fix: prevent background scroll on opening timezone select menu (#19789) 2025-03-06 13:29:50 +00:00
Eunjae Lee d2bc81758c feat: add booking id & booking link columns to /insights/routing (#19697)
* feat: add booking id & booking link columns to /insights/routing

* update style

* update i18n

* update i18n keys

* update host

* useCopy instead of raw navigator.clipboard
2025-03-06 10:24:50 +00:00
Alex van Andel 0565e23818 refactor: Remove intervalLimits from @calcom/lib and export directly (#19710)
* refactor: Remove intervalLimits from @calcom/lib and export directly

* Tackle other places that use parseBookingLimit/parseDurationLimit

* More type fixups that were hidden by previous fails

* Fixed up booking-limits file

* Remove server-only
2025-03-05 19:32:53 +00:00
GitHub Actions 34cff214dd chore: release v5.0.12
Post release / release (push) Canceled after 0s
2025-03-05 18:40:35 +00:00
Alex van Andel f7af19b7cb chore: Bump node to 20 in APIv2 Dockerfile (#19770) 2025-03-05 18:19:29 +00:00
Bailey Pumfleetandsnyk-bot 6849638b1f fix: apps/api/v2/package.json & apps/api/v2/.snyk to reduce vulnerabilities (#19755)
The following vulnerabilities are fixed with a Snyk patch:
- https://snyk.io/vuln/npm:semver:20150403

Co-authored-by: snyk-bot <snyk-bot@snyk.io>
2025-03-05 18:07:55 +00:00
Joe Au-Yeung 07969f5287 perf: Add CrmServiceMap (#19718)
* Generate crm apps map

* Remove .tsx file

* Use `CrmServiceMap` in `getCrm`

* Remove unused types

* Type fixes

* Type fixes
2025-03-05 14:58:02 -03:00
GitHub Actions 44a6ceff3f chore: release v5.0.12 2025-03-05 17:52:37 +00:00
Vijay 0e4309777b fix: make strict bottom position of DateRangePicker optional (#19768)
* make strictbottom optional

* chore
2025-03-05 17:26:10 +00:00
Keith Williams b8f0741923 chore: Update CODEOWNERS to have @calcom/Foundation on Platform line items (#19769) 2025-03-05 10:01:34 -07:00
nizzy ba1ede2e31 fix: plain z index (#19767) 2025-03-05 16:15:01 +00:00
Udit TakkarandBailey Pumfleet b74a49f31d fix: use impersonatedById (#19682)
Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
2025-03-05 15:43:25 +00:00
Tushar Bhatt 31cd77345d all changes (#19720) 2025-03-05 15:41:54 +00:00
Anik Dhabal Babu 2be9ff7622 fix: hubspot not working for team events (#19761) 2025-03-05 15:16:49 +00:00
Alex van Andel 96a5abe6c3 chore: Fix types of app-store to prevent type drilling (#19732) 2025-03-05 12:06:01 -03:00
sean-brydonandKeith Williams 4cdb8f3bae feat: make calcom UI dumb again (#19658)
* remove create button for teams from calcom ui

* migrate timezone select to features

* remove trpc from phone and storybook provider

* new-yarn.lock

* fix imports + test file for select

* fix platform timezone select to use Raw timezone select

* fix timezone type import

* use correct select in timezone-select.tsx - needs trpc version

* fix lock and remove test-utils

* fix log file

* Updated yarn.lock

* Fixed types

* Fixed tests

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2025-03-05 11:58:52 -03:00
Shivansh KumarandAnik Dhabal Babu 1b111c8add fix(payments): add return_url for Alipay PaymentIntent confirmation; fixes #19138 (#19153)
* Update README.md

* fix(payments): add return_url for Alipay PaymentIntent confirmation; fixes #19138

* Fix: update confirmPayment to use confirmParams and resolve TS errors

* Update Payment.tsx

* Update

* fix build error

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-03-05 11:57:43 -03:00
luzpazandTushar Bhatt be1377e9af fix: typos in apps/api (#19762)
Found via codespell

Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
2025-03-05 14:29:11 +00:00
Alex van Andel 7226370427 chore: Bump .nvmrc node version to node 20 (#19760) 2025-03-05 13:50:06 +00:00
Morgan 524c0d81a7 chore: rename DWD to DelegationCredential (#19744)
* Revert "Revert "chore: rename DWD to DelegationCredential (#19703)" (#19734)"

This reverts commit 340b5ab061.

* chore: fix schema and types for now

* fix: domainWideDelegationCredentialId error type
2025-03-05 10:42:20 -03:00
Udit TakkarandPeer Richelsen f008a28499 fix: open cal video url on new page when ready (#19683)
* fix: open cal video url on new page when ready

* fix: open on new tab

* chore: change logic

* chore: add condition for embed

* chore: simplify the code

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2025-03-05 18:27:06 +05:30
Keith Williams 381f2781a3 perf: remove atoms monorepo imports (#19688)
* perf: Remove atoms/monorepo barrel exports

* Removed more exports

* completely removed the monorepo file

* fixed types

* fixing tests

* fixing package errors

* Pushing latest

* Fixed atoms build

* hack to fix type

* Fixed web build
2025-03-05 09:49:13 -03:00
Alex van Andel 6f62c6ad5d refactor: Typescript fixes and updates for bumping ts version to 5.8.2 (#19690)
* Typescript fixes and updates for bumping ts version to 5.8.2

* Different TS versions do see an error, so @ts-ignore'ing

* Attempt this fix for ignoring createDecipher/createCipher fns

* Remove @ts-expect-error
2025-03-05 09:17:08 -03:00
Alex van Andel 1545a8ec82 chore: Upgrade to node v20 as v18 is EOL next month (#19733)
* chore: Upgrade to node v20 as v18 is EOL next month

* Run tests using node 20
2025-03-05 13:34:17 +02:00
Muhammad Aiman Sulaiman 58d7e59b38 feat: Updates to HitPay Payment App (#19737)
* feat: add hitpay embed feature for cal embed

feat: update hitpay app description

fix: fixed customers being able to exceed bookings in the same timeslot when using HitPay

fix: fixed callback issue in cal.com using hitpay app when not receiving payment responses

* fix: fixed type-check error
2025-03-05 11:29:38 +00:00
Eunjae Lee d79a5c3adf fix: search on team availiability (for non-org users) (#19738) 2025-03-05 10:56:03 +00:00
Morgan 340b5ab061 Revert "chore: rename DWD to DelegationCredential (#19703)" (#19734)
This reverts commit 7a9ddf2194.
2025-03-05 10:30:23 +00:00
Anik Dhabal Babu fa58fe6563 fix: tab item is not translated (#19735) 2025-03-05 09:28:45 +00:00
Benny Joo 251121a76f chore: write defaultResponder for App Router APIs (#19727)
* create defaultResponderForAppDir

* rename all usages

* finish

* fix
2025-03-05 09:12:07 +00:00
Rajiv Sahal 75627cb097 fix: booking fields not saving properly for team events (#19702) 2025-03-05 05:38:07 +00:00
amrit f9780fbbd2 fix: auto submission upon uploading photo (#19715)
* fix: auto submission upon uploading photo

* set image source from server response

* get rid of extra network request by removing refetch
2025-03-05 05:34:02 +00:00
luzpaz 13649d3bf3 fix: typos in packages directory (#19574)
Found via codespell
2025-03-05 05:12:40 +00:00
Benny Joo 65898f29eb perf: dynamic import for sentry CaptureException (#19730) 2025-03-04 23:07:20 +00:00
Calcom BotandLingo.dev 11267e9359 feat: update translations via @lingodotdev (#19729)
Co-authored-by: Lingo.dev <support@lingo.dev>
2025-03-04 21:40:29 +00:00
sean-brydonandBailey Pumfleet d38b35b626 chore: remove storybook (#19659)
* remove storybook

* remove storybook trpc provider

* add user-event

* fix lockk file

---------

Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
2025-03-04 14:14:56 -07:00
Lauris Skraucis fff521e171 fix: v2 managed user attendee bookings (#19716)
* Revert "Revert "fix: v2 managed user attendee bookings (#19612)" (#19635)"

This reverts commit 2600a6c3d5.

* fix: make guests optional
2025-03-04 18:08:21 +01:00
Vijay fd6666ebad feat: improved ooo search (#19704)
* improved ooo search

* chore
2025-03-04 13:45:00 +00:00
Anik Dhabal Babu f2fa1248eb fix (#19692) 2025-03-04 18:21:39 +05:30
Morgan 7a9ddf2194 chore: rename DWD to DelegationCredential (#19703)
* chore: rename DWD to DelegationCredential

* fixup! chore: rename DWD to DelegationCredential

* fixup! fixup! chore: rename DWD to DelegationCredential

* fixup! Merge branch 'main' into rename-domain-wide-delegation

* fixup! fixup! Merge branch 'main' into rename-domain-wide-delegation

* fixup! fixup! fixup! Merge branch 'main' into rename-domain-wide-delegation
2025-03-04 09:45:39 -03:00
Anik Dhabal Babu a39b5e47a7 fix: member filter fix (#19707) 2025-03-04 12:45:27 +00:00
Morgan ae8180e7c2 chore: improve validate hosts api v2 team event-types (#19696)
* chore: improve validate hosts api v2 team event-types

* fixup! chore: improve validate hosts api v2 team event-types
2025-03-04 08:55:40 +00:00
Morgan 72832354e6 fix: api v2 axiom logs exceed column size (#19636) 2025-03-04 05:40:36 +00:00
Vachmara df598bf281 feat: improve Intercom app (#19687)
* fix: don't POST to cal.com/[user]/[type]

* fix: return html with cal calendar embed

* fix: intercom app team events types

* feat: improve Intercom UX integration

* Merge branch 'main' of https://github.com/calcom/cal.com
2025-03-04 10:48:31 +05:30
GitHub Actions a3ce957f00 chore: release v5.0.11
Post release / release (push) Canceled after 0s
2025-03-03 23:30:41 +00:00
Keith Williams 7701edb0bb chore: TS config - ignore node_modules in subdirectories (#19680)
* chore: TS config - ignore node_modules in subdirectories

* Update tsconfig.json

* Applied the pattern to the entire monorepo
2025-03-03 15:53:28 -03:00
Benny Joo efd427831d chore: early return for next.js static assets (#19678) 2025-03-03 15:35:42 -03:00
bec83b2342 chore: remove TRPC barrel exports (#19598)
* remove barrel export for client

* fix more imports

* remove server barrel

* remove more barrel exporst and fix tssconfig

* fix types

* fix imports for removed barrels

* fig ssg createserverside helpers

* restore lock

* new yarn lock

* remove barel exports

* remove barrel

* replace client with react in tsconfig

* fix yarn lock

* Update exports for trpc to not be types

* add ENDPOINTS export to the correct location

* other exports fixes lost due to barrel

* fix client error export

* more imports

---------

Co-authored-by: hbjORbj <sldisek783@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2025-03-03 15:33:32 -03:00
Ash DavisandAnik Dhabal Babu 5a6a13b954 fix: allow cancelledBy query param to work on api v1 (#19268)
* fix: cancelledby to work on api v1

* fix: scheduleId to work with hosts in eventtype api v1

* Update event-type.ts

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2025-03-03 18:19:09 +00:00
Ash Davisandamrit 340f15fea5 fix: scheduleId to work on event type host assignment via patch api v1 (#19546)
Co-authored-by: amrit <iamamrit27@gmail.com>
2025-03-03 18:18:06 +00:00
Anik Dhabal Babu f572a2b9c2 fix: some flaky e2e tests (#19648)
* fix: some flaky e2e tests

* update

* update
2025-03-03 21:55:21 +05:30
MorganandSomay Chauhan ee9c73476b chore: add docs to /v2/organizations/:orgId/organizations endpoints (#19668)
Co-authored-by: Somay Chauhan <somaychauhan98@gmail.com>
2025-03-03 16:03:32 +00:00
Keith Williams 78d06c6d01 refactor: Move classNames from @calcom/lib to @calcom/ui (#19674)
* refactor: Move classNames from @calcom/lib to @calcom/ui

* Import fix

* Removed extra import of classNames

* Removed tailwind-merge from @calcom/lib
2025-03-03 15:54:33 +00:00
Eunjae Lee 3d3a9c0dfb fix: use portal for DataTableSelectionBar (#19672) 2025-03-03 15:42:38 +00:00
Tushar Bhatt ed06050fda fix UI on mobile (#19673) 2025-03-03 15:24:37 +00:00
amrit fd6effa6ff feat: booking redirect page url is now has uid in it's parameters (#19522)
* feat: booking redirect page is now w UID

* refactor: use getBookingRedirectExtraParams

* refactor: remove uncessary const result

* uid isnt the first param anymore
2025-03-03 09:47:32 -05:00
Morgan 2889893804 refactor: import UseIsPlatform directly instead of barrel file (#19670)
* refactor: import useIsPlatform directly

* fixup! refactor: import useIsPlatform directly

* fixup! fixup! refactor: import useIsPlatform directly

* fixup! fixup! fixup! refactor: import useIsPlatform directly
2025-03-03 14:26:50 +00:00
Rajiv Sahal 3c035ab636 chore: add hooks section to v2 docs (#19651)
* add custom hooks page
2025-03-03 14:18:39 +00:00
sean-brydon c1ccd62f98 use profileUsername (#19667) 2025-03-03 12:28:40 +00:00
Eunjae Lee 0e5775a087 fix: adjust table height on /bookings (#19662) 2025-03-03 11:00:38 +00:00
amritandUdit Takkar 7e83f1a59a fix locale (#19665)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2025-03-03 10:52:31 +00:00
Eunjae LeeandUdit Takkar 12724ef894 fix: show selected filter values first in single- & multi-select filters (#19620)
* fix: show selected filter values first in single- & multi-select filters

* fix type errors

* fix type error

* fix type error

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2025-03-03 10:28:45 +00:00
Somay ChauhanandPeer Richelsen 6244b5a2c0 feat: added Ability to set organizer's default app as a valid input location in api/v2 (#19549)
* feat: added Ability to set organizer's default app as a valid input location in api/v2

* only allow organizers default for team events

* fixed for atoms and added unit and e2e tests

* Update organizations-event-types.e2e-spec.ts

* Update transformApiEventTypeForAtom.ts

* published platform-libraries@0.0.124

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2025-03-03 09:01:24 +00:00
Anik Dhabal Babu 615ef93572 fix: remove ‘add guests’ from the dropdown if it’s disabled in the booking questions (#19649)
* fix: remove ‘add guests’ from the dropdown if it’s disabled in the booking questions

* fix: remove dd guests' from the dropdown if it's
disabled in the booking questions
2025-03-03 05:06:51 +00:00
Anik Dhabal Babu 22f1060ed6 fix: fix: Incorrect Attendee Shown When Cancelling a Seated Event (#19647) 2025-03-03 05:05:36 +00:00
amrit 51a39571aa fix height (#19656) 2025-03-03 04:38:56 +00:00
Keith Williams 7180eb067a refactor: Move @calcom/core to @calcom/lib (#19655)
* refactor: Move @calcom/core to @calcom/lib

* Merging imports

* Clean up

* Ignoreing type error for now
2025-03-02 23:02:35 -03:00
Alex van AndelandOmar López 1cfd94d293 fix: Re-instate new slot logic now it's A/B tested in production (#19605)
* fix: Re-instate new slot logic now it's well tested

* Fixup getAggregatedAvailability

* Changes to code-owners to reflect removed/moved code

* Move more files around.

* Fix failed type

---------

Co-authored-by: Omar López <zomars@me.com>
2025-03-02 23:32:31 +00:00
Calcom BotandLingo.dev 4bab8c2c32 feat: update translations via @lingodotdev (#19654)
Co-authored-by: Lingo.dev <support@lingo.dev>
2025-03-02 17:53:42 -05:00
2170 changed files with 37320 additions and 32562 deletions
+10 -8
View File
@@ -17,6 +17,7 @@
#
# To enable enterprise-only features please add your environment variable to the .env file then make your way to /auth/setup to select your license and follow instructions.
CALCOM_LICENSE_KEY=
# Signature token for the Cal.com License API (used for self-hosted integrations)
# We will give you a token when we provide you with a license key this ensure you and only you can communicate with the Cal.com License API for your license key
@@ -192,7 +193,10 @@ NEXT_PUBLIC_IS_PREMIUM_NEW_PLAN=0
NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE=
STRIPE_TEAM_MONTHLY_PRICE_ID=
STRIPE_TEAM_PRODUCT_ID=
# It is a price ID in the product with id STRIPE_ORG_PRODUCT_ID
STRIPE_ORG_MONTHLY_PRICE_ID=
STRIPE_ORG_PRODUCT_ID=
STRIPE_WEBHOOK_SECRET=
STRIPE_WEBHOOK_SECRET_APPS=
STRIPE_PRIVATE_KEY=
@@ -285,7 +289,7 @@ NEXT_PUBLIC_AVAILABILITY_SCHEDULE_INTERVAL=
# use organizations
ORGANIZATIONS_ENABLED=
NEXT_PUBLIC_ORGANIZATIONS_MIN_SELF_SERVE_SEATS=30
NEXT_PUBLIC_ORGANIZATIONS_SELF_SERVE_PRICE=3700 # $37.00 per seat
NEXT_PUBLIC_ORGANIZATIONS_SELF_SERVE_PRICE_NEW=37 # $37.00 per seat
# This variable should only be set to 1 or true if you want to autolink external provider sign-ups with
# existing organizations based on email domain address
@@ -353,13 +357,6 @@ E2E_TEST_OIDC_USER_PASSWORD=
# ***********************************************************************************************************
# provide a value between 0 and 100 to ensure the percentage of traffic
# redirected from the legacy to the future pages
AB_TEST_BUCKET_PROBABILITY=50
APP_ROUTER_APPS_SLUG_SETUP_ENABLED=0
APP_ROUTER_APPS_ENABLED=0
APP_ROUTER_TEAM_ENABLED=0
# disable setry server source maps
SENTRY_DISABLE_SERVER_WEBPACK_PLUGIN=1
@@ -402,6 +399,11 @@ NEXT_PUBLIC_WEBSITE_TERMS_URL=
# [0: silly & upwards, 1: trace & upwards, 2: debug & upwards, 3: info & upwards, 4: warn & upwards, 5: error & fatal, 6: fatal]
NEXT_PUBLIC_LOGGER_LEVEL=
# For storing Daily Video recordings on S3 Bucket
CAL_VIDEO_BUCKET_NAME=
CAL_VIDEO_BUCKET_REGION=
CAL_VIDEO_ASSUME_ROLE_ARN=
# Used to use Replexica SDK, a tool for real-time AI-powered localization
REPLEXICA_API_KEY=
+5 -5
View File
@@ -1,4 +1,5 @@
/apps/api/**/* @calcom/Platform
/**/package.json @calcom/Foundation
/apps/api/**/* @calcom/Platform @calcom/Foundation
/deploy/**/* @calcom/Foundation
/infra/**/* @calcom/Foundation
/packages/app-store/applecalendar/**/* @calcom/Foundation
@@ -12,12 +13,11 @@
/packages/app-store/larkcalendar/**/* @calcom/Foundation
/packages/app-store/office365calendar/**/* @calcom/Foundation
/packages/app-store/zohocalendar/**/* @calcom/Foundation
/packages/core/getAggregatedAvailability/index.ts @calcom/Foundation
/packages/core/getAggregateWorkingHours.ts @calcom/Foundation
/packages/core/getUserAvailability.ts @calcom/Foundation
/packages/features/bookings/lib/**/* @calcom/Foundation
/packages/lib/getAggregatedAvailability.ts @calcom/Foundation
/packages/lib/getUserAvailability.ts @calcom/Foundation
/packages/lib/server/getLuckyUser.ts @calcom/Foundation
/packages/lib/slots.ts @calcom/Foundation
/packages/platform/**/* @calcom/Platform
/packages/platform/**/* @calcom/Platform @calcom/Foundation
/packages/prisma/**/* @calcom/Foundation
/packages/trpc/server/routers/viewer/slots/**/* @calcom/Foundation
+1 -1
View File
@@ -14,7 +14,7 @@ description: "Run yarn install with node_modules linker and cache enabled"
inputs:
node_version:
required: false
default: v18.x
default: v20.x
runs:
using: "composite"
+1 -1
View File
@@ -1 +1 @@
v18
20
+8
View File
@@ -0,0 +1,8 @@
undecided:
- calcom-monorepo
- "@calcom/app-store-cli"
- "@calcom/platform-constants"
- "@calcom/platform-enums"
- "@calcom/platform-types"
- "@calcom/platform-utils"
- "@calcom/prisma"
+2
View File
@@ -0,0 +1,2 @@
undecided:
- "@calcom/prisma"
View File
View File
View File
+6
View File
@@ -0,0 +1,6 @@
undecided:
- "@calcom/app-store-cli"
- "@calcom/platform-constants"
- "@calcom/platform-enums"
- "@calcom/platform-types"
- "@calcom/platform-utils"
View File
+8
View File
@@ -0,0 +1,8 @@
undecided:
- calcom-monorepo
- "@calcom/app-store-cli"
- "@calcom/platform-constants"
- "@calcom/platform-enums"
- "@calcom/platform-types"
- "@calcom/platform-utils"
- "@calcom/prisma"
+2
View File
@@ -0,0 +1,2 @@
undecided:
- "@calcom/prisma"
+8
View File
@@ -0,0 +1,8 @@
undecided:
- calcom-monorepo
- "@calcom/app-store-cli"
- "@calcom/platform-constants"
- "@calcom/platform-enums"
- "@calcom/platform-types"
- "@calcom/platform-utils"
- "@calcom/prisma"
+3
View File
@@ -0,0 +1,3 @@
undecided:
- calcom-monorepo
- "@calcom/prisma"
+2
View File
@@ -0,0 +1,2 @@
undecided:
- "@calcom/prisma"
+3
View File
@@ -0,0 +1,3 @@
undecided:
- calcom-monorepo
- "@calcom/prisma"
View File
+3
View File
@@ -0,0 +1,3 @@
undecided:
- calcom-monorepo
- "@calcom/prisma"
+8
View File
@@ -0,0 +1,8 @@
undecided:
- calcom-monorepo
- "@calcom/app-store-cli"
- "@calcom/platform-constants"
- "@calcom/platform-enums"
- "@calcom/platform-types"
- "@calcom/platform-utils"
- "@calcom/prisma"
+8
View File
@@ -0,0 +1,8 @@
undecided:
- calcom-monorepo
- "@calcom/app-store-cli"
- "@calcom/platform-constants"
- "@calcom/platform-enums"
- "@calcom/platform-types"
- "@calcom/platform-utils"
- "@calcom/prisma"
+9
View File
@@ -0,0 +1,9 @@
undecided:
- calcom-monorepo
- "@calcom/api-v2"
- "@calcom/app-store-cli"
- "@calcom/platform-constants"
- "@calcom/platform-enums"
- "@calcom/platform-types"
- "@calcom/platform-utils"
- "@calcom/prisma"
+2
View File
@@ -0,0 +1,2 @@
undecided:
- "@calcom/prisma"
+14 -13
View File
@@ -172,24 +172,25 @@ yarn dx
```
#### Development tip
1. Add `export NODE_OPTIONS=“--max-old-space-size=16384”` to your shell script to increase the memory limit for the node process. Alternatively, you can run this in your terminal before running the app. Replace 16384 with the amount of RAM you want to allocate to the node process.
Add `NEXT_PUBLIC_LOGGER_LEVEL={level}` to your .env file to control the logging verbosity for all tRPC queries and mutations.\
Where {level} can be one of the following:
2. Add `NEXT_PUBLIC_LOGGER_LEVEL={level}` to your .env file to control the logging verbosity for all tRPC queries and mutations.\
Where {level} can be one of the following:
`0` for silly \
`1` for trace \
`2` for debug \
`3` for info \
`4` for warn \
`5` for error \
`6` for fatal
`0` for silly \
`1` for trace \
`2` for debug \
`3` for info \
`4` for warn \
`5` for error \
`6` for fatal
When you set `NEXT_PUBLIC_LOGGER_LEVEL={level}` in your .env file, it enables logging at that level and higher. Here's how it works:
When you set `NEXT_PUBLIC_LOGGER_LEVEL={level}` in your .env file, it enables logging at that level and higher. Here's how it works:
The logger will include all logs that are at the specified level or higher. For example: \
The logger will include all logs that are at the specified level or higher. For example: \
- If you set `NEXT_PUBLIC_LOGGER_LEVEL=2`, it will log from level 2 (debug) upwards, meaning levels 2 (debug), 3 (info), 4 (warn), 5 (error), and (fatal) will be logged. \
- If you set `NEXT_PUBLIC_LOGGER_LEVEL=3`, it will log from level 3 (info) upwards, meaning levels 3 (info), 4 (warn), 5 (error), and 6 (fatal) will be logged, but level 2 (debug) and level 1 (trace) will be ignored. \
- If you set `NEXT_PUBLIC_LOGGER_LEVEL=2`, it will log from level 2 (debug) upwards, meaning levels 2 (debug), 3 (info), 4 (warn), 5 (error), and (fatal) will be logged. \
- If you set `NEXT_PUBLIC_LOGGER_LEVEL=3`, it will log from level 3 (info) upwards, meaning levels 3 (info), 4 (warn), 5 (error), and 6 (fatal) will be logged, but level 2 (debug) and level 1 (trace) will be ignored. \
```sh
echo 'NEXT_PUBLIC_LOGGER_LEVEL=3' >> .env
+2 -2
View File
@@ -113,7 +113,7 @@ We use it in several ways, but mainly, we first import the auto-generated schema
We have some shared validations which several resources require, like baseApiParams which parses apiKey in all requests, or querIdAsString or TransformParseInt which deal with the id's coming from req.query.
- **[*]BaseBodyParams** that omits any values from the model that are too sensitive or we don't want to pick when creating a new resource like id, userId, etc.. (those are gotten from context or elswhere)
- **[*]BaseBodyParams** that omits any values from the model that are too sensitive or we don't want to pick when creating a new resource like id, userId, etc.. (those are gotten from context or elsewhere)
- **[*]Public** that also omits any values that we don't want to expose when returning the model as a response, which we parse against before returning all resources.
@@ -208,7 +208,7 @@ Add this command: `./scripts/vercel-deploy.sh`
See `scripts/vercel-deploy.sh` for more info on how the deployment is done.
> _❗ IMORTANT: If you're forking the API repo you will need to update the URLs in both the main repo [`.gitmodules`](https://github.com/calcom/cal.com/blob/main/.gitmodules#L7) and this repo [`./scripts/vercel-deploy.sh`](https://github.com/calcom/api/blob/main/scripts/vercel-deploy.sh#L3) ❗_
> _❗ IMPORTANT: If you're forking the API repo you will need to update the URLs in both the main repo [`.gitmodules`](https://github.com/calcom/cal.com/blob/main/.gitmodules#L7) and this repo [`./scripts/vercel-deploy.sh`](https://github.com/calcom/api/blob/main/scripts/vercel-deploy.sh#L3) ❗_
## Environment variables
+8
View File
@@ -0,0 +1,8 @@
import { setUser as SentrySetUser } from "@sentry/nextjs";
import type { NextMiddleware } from "next-api-middleware";
export const captureUserId: NextMiddleware = async (req, res, next) => {
if (req.userId) SentrySetUser({ id: req.userId });
await next();
};
+7 -4
View File
@@ -2,15 +2,16 @@ import { label } from "next-api-middleware";
import { addRequestId } from "./addRequestid";
import { captureErrors } from "./captureErrors";
import { captureUserId } from "./captureUserId";
import { checkIsInMaintenanceMode } from "./checkIsInMaintenanceMode";
import { extendRequest } from "./extendRequest";
import {
HTTP_POST,
HTTP_DELETE,
HTTP_PATCH,
HTTP_GET,
HTTP_GET_OR_POST,
HTTP_GET_DELETE_PATCH,
HTTP_GET_OR_POST,
HTTP_PATCH,
HTTP_POST,
} from "./httpMethods";
import { rateLimitApiKey } from "./rateLimitApiKey";
import { verifyApiKey } from "./verifyApiKey";
@@ -31,6 +32,7 @@ const middleware = {
extendRequest,
pagination: withPagination,
captureErrors,
captureUserId,
verifyCredentialSyncEnabled,
};
@@ -44,8 +46,9 @@ const middlewareOrder = [
"verifyApiKey",
"rateLimitApiKey",
"addRequestId",
"captureUserId",
] as Middleware[]; // <-- Provide a list of middleware to call automatically
const withMiddleware = label(middleware, middlewareOrder);
export { withMiddleware, middleware, middlewareOrder };
export { middleware, middlewareOrder, withMiddleware };
+2 -1
View File
@@ -8,7 +8,8 @@ import {
_TeamModel,
_UserModel,
} from "@calcom/prisma/zod";
import { extendedBookingCreateBody, iso8601 } from "@calcom/prisma/zod-utils";
import { iso8601 } from "@calcom/prisma/zod-utils";
import { extendedBookingCreateBody } from "@calcom/prisma/zod/custom/booking";
import { schemaQueryUserId } from "./shared/queryUserId";
@@ -22,6 +22,7 @@ const recurringEventInputSchema = z.object({
const hostSchema = _HostModel.pick({
isFixed: true,
userId: true,
scheduleId: true,
});
export const childrenSchema = z.object({
+2 -2
View File
@@ -113,7 +113,7 @@ const schemaUserEditParams = z.object({
});
// @note: These are the values that are editable via PATCH method on the user Model,
// merging both BaseBodyParams with RequiredParams, and omiting whatever we want at the end.
// merging both BaseBodyParams with RequiredParams, and omitting whatever we want at the end.
const schemaUserCreateParams = z.object({
email: emailSchema.toLowerCase(),
@@ -137,7 +137,7 @@ const schemaUserCreateParams = z.object({
});
// @note: These are the values that are editable via PATCH method on the user Model,
// merging both BaseBodyParams with RequiredParams, and omiting whatever we want at the end.
// merging both BaseBodyParams with RequiredParams, and omitting whatever we want at the end.
export const schemaUserEditBodyParams = schemaUserBaseBodyParams
.merge(schemaUserEditParams)
.omit({})
+1 -2
View File
@@ -10,7 +10,6 @@ const nextConfig = {
},
transpilePackages: [
"@calcom/app-store",
"@calcom/core",
"@calcom/dayjs",
"@calcom/emails",
"@calcom/features",
@@ -47,7 +46,7 @@ const nextConfig = {
async rewrites() {
return {
afterFiles: [
// This redirects requests recieved at / the root to the /api/ folder.
// This redirects requests received at / the root to the /api/ folder.
{
source: "/v:version/:rest*",
destination: "/api/v:version/:rest*",
-1
View File
@@ -24,7 +24,6 @@
},
"dependencies": {
"@calcom/app-store": "*",
"@calcom/core": "*",
"@calcom/dayjs": "*",
"@calcom/emails": "*",
"@calcom/features": "*",
+1 -1
View File
@@ -1,7 +1,7 @@
import type { NextApiRequest } from "next";
import { z } from "zod";
import { getUserAvailability } from "@calcom/core/getUserAvailability";
import { getUserAvailability } from "@calcom/lib/getUserAvailability";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
import prisma from "@calcom/prisma";
+11 -3
View File
@@ -38,6 +38,12 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
* schema:
* type: string
* description: The reason for cancellation of the booking
* - in: query
* name: cancelledBy
* required: false
* schema:
* type: string
* description: The email of who cancelled the booking
* tags:
* - bookings
* responses:
@@ -64,15 +70,17 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
* description: User not found
*/
async function handler(req: NextApiRequest) {
const { id, allRemainingBookings, cancellationReason } = schemaQueryIdParseInt
.merge(bookingCancelSchema.pick({ allRemainingBookings: true, cancellationReason: true }))
const { id, allRemainingBookings, cancellationReason, cancelledBy } = schemaQueryIdParseInt
.merge(
bookingCancelSchema.pick({ allRemainingBookings: true, cancellationReason: true, cancelledBy: true })
)
.parse({
...req.query,
allRemainingBookings: req.query.allRemainingBookings === "true",
});
// Normalizing for universal handler
req.body = { id, allRemainingBookings, cancellationReason };
req.body = { id, allRemainingBookings, cancellationReason, cancelledBy };
return await handleCancelBooking(req);
}
@@ -112,7 +112,7 @@ export async function patchHandler(req: NextApiRequest) {
async function checkPermissions(req: NextApiRequest, body: z.infer<typeof schemaBookingEditBodyParams>) {
const { userId, isSystemWideAdmin, isOrganizationOwnerOrAdmin } = req;
if (body.userId && !isSystemWideAdmin && !isOrganizationOwnerOrAdmin) {
// Organizer has to be a cal user and we can't allow a booking to be transfered to some other cal user's name
// Organizer has to be a cal user and we can't allow a booking to be transferred to some other cal user's name
throw new HttpError({
statusCode: 403,
message: "Only admin can change the organizer of a booking",
@@ -1,9 +1,11 @@
import type { NextApiRequest } from "next";
import { getRecordingsOfCalVideoByRoomName } from "@calcom/core/videoClient";
import { getDownloadLinkOfCalVideoByRecordingId } from "@calcom/core/videoClient";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
import {
getRecordingsOfCalVideoByRoomName,
getDownloadLinkOfCalVideoByRecordingId,
} from "@calcom/lib/videoClient";
import prisma from "@calcom/prisma";
import type { RecordingItemSchema } from "@calcom/prisma/zod-utils";
import type { PartialReference } from "@calcom/types/EventManager";
@@ -1,11 +1,11 @@
import type { NextApiRequest } from "next";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
import {
getTranscriptsAccessLinkFromRecordingId,
checkIfRoomNameMatchesInRecording,
} from "@calcom/core/videoClient";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
} from "@calcom/lib/videoClient";
import prisma from "@calcom/prisma";
import type { PartialReference } from "@calcom/types/EventManager";
@@ -1,8 +1,8 @@
import type { NextApiRequest } from "next";
import { getAllTranscriptsAccessLinkFromRoomName } from "@calcom/core/videoClient";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
import { getAllTranscriptsAccessLinkFromRoomName } from "@calcom/lib/videoClient";
import prisma from "@calcom/prisma";
import type { PartialReference } from "@calcom/types/EventManager";
@@ -2,7 +2,7 @@ import type { Prisma } from "@prisma/client";
import type { NextApiRequest } from "next";
import type { z } from "zod";
import { getCalendarCredentialsWithoutDwd, getConnectedCalendars } from "@calcom/core/CalendarManager";
import { getCalendarCredentialsWithoutDelegation, getConnectedCalendars } from "@calcom/lib/CalendarManager";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
import prisma from "@calcom/prisma";
@@ -185,7 +185,7 @@ async function verifyCredentialsAndGetId({
currentCredentialId: number | null;
}) {
if (parsedBody.integration && parsedBody.externalId) {
const calendarCredentials = getCalendarCredentialsWithoutDwd(userCredentials);
const calendarCredentials = getCalendarCredentialsWithoutDelegation(userCredentials);
const { connectedCalendars } = await getConnectedCalendars(
calendarCredentials,
@@ -1,6 +1,6 @@
import type { NextApiRequest } from "next";
import { getCalendarCredentialsWithoutDwd, getConnectedCalendars } from "@calcom/core/CalendarManager";
import { getCalendarCredentialsWithoutDelegation, getConnectedCalendars } from "@calcom/lib/CalendarManager";
import { HttpError } from "@calcom/lib/http-error";
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
import prisma from "@calcom/prisma";
@@ -82,7 +82,7 @@ async function postHandler(req: NextApiRequest) {
message: "Bad request, credential id invalid",
});
const calendarCredentials = getCalendarCredentialsWithoutDwd(userCredentials);
const calendarCredentials = getCalendarCredentialsWithoutDelegation(userCredentials);
const { connectedCalendars } = await getConnectedCalendars(calendarCredentials, [], parsedBody.externalId);
+1 -1
View File
@@ -124,7 +124,7 @@ async function defaultScheduleId<T extends DefaultScheduleIdEventTypeBase>({
}, {} as { [x: number]: number | null });
return eventTypes.map((eventType) => {
// realistically never happens, userId should't be null on personal event types.
// realistically never happens, userId shouldn't be null on personal event types.
if (!eventType.userId) return eventType;
return {
...eventType,
@@ -28,7 +28,7 @@ import { membershipIdSchema } from "~/lib/validations/membership";
* - memberships
* responses:
* 201:
* description: OK, membership removed successfuly
* description: OK, membership removed successfully
* 400:
* description: Bad request. Membership id is invalid.
* 401:
@@ -31,7 +31,7 @@ import { schemaQueryUserId } from "~/lib/validations/shared/queryUserId";
* - users
* responses:
* 201:
* description: OK, user removed successfuly
* description: OK, user removed successfully
* 400:
* description: Bad request. User id is invalid.
* 401:
@@ -5,11 +5,11 @@ import type { NextApiRequest, NextApiResponse } from "next";
import { createMocks } from "node-mocks-http";
import { describe, expect, test, vi, afterEach } from "vitest";
import { buildBooking } from "@calcom/lib/test/builder";
import {
getRecordingsOfCalVideoByRoomName,
getDownloadLinkOfCalVideoByRecordingId,
} from "@calcom/core/videoClient";
import { buildBooking } from "@calcom/lib/test/builder";
} from "@calcom/lib/videoClient";
import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
@@ -22,7 +22,7 @@ type CustomNextApiResponse = NextApiResponse & Response;
const adminUserId = 1;
const memberUserId = 10;
vi.mock("@calcom/core/videoClient", () => {
vi.mock("@calcom/lib/videoClient", () => {
return {
getRecordingsOfCalVideoByRoomName: vi.fn(),
getDownloadLinkOfCalVideoByRecordingId: vi.fn(),
@@ -5,11 +5,11 @@ import type { NextApiRequest, NextApiResponse } from "next";
import { createMocks } from "node-mocks-http";
import { describe, expect, test, vi, afterEach } from "vitest";
import { buildBooking } from "@calcom/lib/test/builder";
import {
getTranscriptsAccessLinkFromRecordingId,
checkIfRoomNameMatchesInRecording,
} from "@calcom/core/videoClient";
import { buildBooking } from "@calcom/lib/test/builder";
} from "@calcom/lib/videoClient";
import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
@@ -19,7 +19,7 @@ import handler from "../../../../../../pages/api/bookings/[id]/transcripts/[reco
type CustomNextApiRequest = NextApiRequest & Request;
type CustomNextApiResponse = NextApiResponse & Response;
vi.mock("@calcom/core/videoClient", () => {
vi.mock("@calcom/lib/videoClient", () => {
return {
getTranscriptsAccessLinkFromRecordingId: vi.fn(),
checkIfRoomNameMatchesInRecording: vi.fn(),
@@ -5,8 +5,8 @@ import type { NextApiRequest, NextApiResponse } from "next";
import { createMocks } from "node-mocks-http";
import { describe, expect, test, vi, afterEach } from "vitest";
import { getAllTranscriptsAccessLinkFromRoomName } from "@calcom/core/videoClient";
import { buildBooking } from "@calcom/lib/test/builder";
import { getAllTranscriptsAccessLinkFromRoomName } from "@calcom/lib/videoClient";
import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
@@ -16,7 +16,7 @@ import handler from "../../../../../pages/api/bookings/[id]/transcripts/_get";
type CustomNextApiRequest = NextApiRequest & Request;
type CustomNextApiResponse = NextApiResponse & Response;
vi.mock("@calcom/core/videoClient", () => {
vi.mock("@calcom/lib/videoClient", () => {
return {
getAllTranscriptsAccessLinkFromRoomName: vi.fn(),
};
@@ -81,6 +81,7 @@ describe("POST /api/selected-calendars", () => {
userId: 444444,
id: "xxx-xxx",
eventTypeId: null,
delegationCredentialId: null,
domainWideDelegationCredentialId: null,
googleChannelId: null,
googleChannelKind: null,
@@ -123,6 +124,7 @@ describe("POST /api/selected-calendars", () => {
googleChannelResourceId: null,
googleChannelResourceUri: null,
googleChannelExpiration: null,
delegationCredentialId: null,
domainWideDelegationCredentialId: null,
eventTypeId: null,
error: null,
+1 -1
View File
@@ -18,5 +18,5 @@
"../../../packages/types/*.d.ts",
"../../../packages/types/next-auth.d.ts"
],
"exclude": ["node_modules", "templates", "auth"]
"exclude": ["**/node_modules/**", "templates", "auth"]
}
+12
View File
@@ -0,0 +1,12 @@
# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.
version: v1.25.1
ignore: {}
# patches apply the minimum changes required to fix a vulnerability
patch:
'npm:semver:20150403':
- '@calcom/platform-libraries-0.0.2 > @calcom/features > @calcom/atoms > rollup-plugin-node-builtins > browserify-fs > levelup > semver':
patched: '2025-03-05T12:50:30.654Z'
id: 'npm:semver:20150403'
path: >-
@calcom/platform-libraries-0.0.2 > @calcom/features > @calcom/atoms >
rollup-plugin-node-builtins > browserify-fs > levelup > semver
+1 -1
View File
@@ -1,4 +1,4 @@
FROM node:18-alpine as build
FROM node:20-alpine AS build
ARG DATABASE_DIRECT_URL
ARG DATABASE_URL
+30 -1
View File
@@ -54,9 +54,29 @@ CALCOM_LICENSE_KEY=c4234812-12ab-42s6-a1e3-55bedd4a5bb
## Running the app
# Development
```bash
# development
$ yarn run start
```
OR if you don't want to use docker, you can run following command.
```bash
$ yarn dev:no-docker
```
Additionally you can run following command(in different terminal) to ensure that any change in any of the dependencies is rebuilt and detected. It watches platform-libraries, platform-constants, platform-enums, platform-utils, platform-types.
```bash
$ yarn run dev:build:watch
```
If you are making changes in packages/platform/libraries, you should run the following command too that would connect your local packages/platform/libraries to the api/v2
```bash
$ yarn local
```
# watch mode
$ yarn run start:dev
@@ -65,6 +85,9 @@ $ yarn run start:dev
$ yarn run start:prod
```
## Test
```bash
@@ -74,6 +97,12 @@ $ yarn run test
# e2e tests
$ yarn run test:e2e
# e2e tests in watch mode
$ yarn test:e2e:watch
# run specific e2e test file in watch mode
$ yarn test:e2e:watch --testPathPattern=filePath
# test coverage
$ yarn run test:cov
```
+17 -7
View File
@@ -9,8 +9,14 @@
"build": "yarn dev:build && nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"dev:build:watch": "yarn workspace @calcom/platform-constants build:watch & yarn workspace @calcom/platform-utils build:watch & yarn workspace @calcom/platform-types build:watch",
"dev:build": "yarn workspace @calcom/platform-constants build && yarn workspace @calcom/platform-enums build && yarn workspace @calcom/platform-utils build && yarn workspace @calcom/platform-types build ",
"dev:build:watch": "concurrently --names \"libraries,lru-fix,constants,enums,utils,types\" \"yarn _dev:build:watch:libraries\" \"yarn _dev:build:watch:libraries:lru-fix\" \"yarn _dev:build:watch:constants\" \"yarn _dev:build:watch:enums\" \"yarn _dev:build:watch:utils\" \"yarn _dev:build:watch:types\"",
"_dev:build:watch:libraries": "yarn workspace @calcom/platform-libraries build:watch",
"_dev:build:watch:libraries:lru-fix": "yarn workspace @calcom/platform-libraries watch-lru-fix",
"_dev:build:watch:constants": "yarn workspace @calcom/platform-constants build:watch",
"_dev:build:watch:enums": "yarn workspace @calcom/platform-enums build:watch",
"_dev:build:watch:utils": "yarn workspace @calcom/platform-utils build:watch",
"_dev:build:watch:types": "yarn workspace @calcom/platform-types build:watch",
"dev:build": "yarn workspace @calcom/platform-constants build && yarn workspace @calcom/platform-enums build && yarn workspace @calcom/platform-utils build && yarn workspace @calcom/platform-types build",
"dev": "yarn dev:build && ts-node scripts/docker-start.ts && yarn copy-swagger-module && yarn start --watch",
"dev:no-docker": "yarn dev:build && yarn copy-swagger-module && yarn start --watch",
"start:debug": "nest start --debug --watch",
@@ -24,13 +30,15 @@
"test:e2e:watch": "yarn dev:build && jest --runInBand --detectOpenHandles --forceExit --config ./jest-e2e.json --watch",
"prisma": "yarn workspace @calcom/prisma prisma",
"generate-schemas": "yarn prisma generate && yarn prisma format",
"copy-swagger-module": "ts-node -r tsconfig-paths/register swagger/copy-swagger-module.ts"
"copy-swagger-module": "ts-node -r tsconfig-paths/register swagger/copy-swagger-module.ts",
"prepare": "yarn run snyk-protect",
"snyk-protect": "snyk-protect"
},
"dependencies": {
"@axiomhq/winston": "^1.2.0",
"@calcom/platform-constants": "*",
"@calcom/platform-enums": "*",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.123",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.152",
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
"@calcom/platform-types": "*",
"@calcom/platform-utils": "*",
@@ -50,10 +58,12 @@
"@sentry/nestjs": "^8.37.1",
"@sentry/node": "^8.8.0",
"@sentry/profiling-node": "^8.37.1",
"@snyk/protect": "latest",
"body-parser": "^1.20.2",
"bull": "^4.12.4",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"concurrently": "^9.1.2",
"cookie-parser": "^1.4.6",
"dotenv": "^16.3.1",
"fs-extra": "^11.2.0",
@@ -86,7 +96,6 @@
"@types/fs-extra": "^11.0.4",
"@types/jest": "^29.5.10",
"@types/luxon": "^3.3.7",
"@types/node": "^20.3.1",
"@types/passport-jwt": "^3.0.13",
"@types/supertest": "^2.0.12",
"jest": "^29.7.0",
@@ -99,9 +108,10 @@
"ts-loader": "^9.4.3",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.1.0",
"typescript": "^4.9.4"
"typescript": "^5.8.2"
},
"prisma": {
"schema": "../../../packages/prisma/schema.prisma"
}
},
"snyk": true
}
+2
View File
@@ -21,6 +21,7 @@ import {
X_CAL_PLATFORM_EMBED,
} from "@calcom/platform-constants";
import { CalendarServiceExceptionFilter } from "./filters/calendar-service-exception.filter";
import { TRPCExceptionFilter } from "./filters/trpc-exception.filter";
export const bootstrap = (app: NestExpressApplication): NestExpressApplication => {
@@ -76,6 +77,7 @@ export const bootstrap = (app: NestExpressApplication): NestExpressApplication =
app.useGlobalFilters(new ZodExceptionFilter());
app.useGlobalFilters(new HttpExceptionFilter());
app.useGlobalFilters(new TRPCExceptionFilter());
app.useGlobalFilters(new CalendarServiceExceptionFilter());
app.use(cookieParser());
@@ -1,17 +1,49 @@
import { BookingsController_2024_04_15 } from "@/ee/bookings/2024-04-15/controllers/bookings.controller";
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module";
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
import { AppsRepository } from "@/modules/apps/apps.repository";
import { BillingModule } from "@/modules/billing/billing.module";
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { RedisModule } from "@/modules/redis/redis.module";
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { TokensRepository } from "@/modules/tokens/tokens.repository";
import { UsersModule } from "@/modules/users/users.module";
import { Module } from "@nestjs/common";
@Module({
imports: [PrismaModule, RedisModule, TokensModule, BillingModule],
providers: [TokensRepository, OAuthFlowService, OAuthClientRepository, ApiKeysRepository],
imports: [
PrismaModule,
RedisModule,
TokensModule,
BillingModule,
UsersModule,
EventTypesModule_2024_04_15,
SchedulesModule_2024_04_15,
EventTypesModule_2024_06_14,
],
providers: [
TokensRepository,
OAuthFlowService,
OAuthClientRepository,
ApiKeysRepository,
OAuthClientUsersService,
PlatformBookingsService,
CalendarsService,
CredentialsRepository,
AppsRepository,
CalendarsRepository,
SelectedCalendarsRepository,
],
controllers: [BookingsController_2024_04_15],
})
export class BookingsModule_2024_04_15 {}
@@ -114,7 +114,6 @@ describe("Bookings Endpoints 2024-04-15", () => {
optionValue: "",
},
notes: "test",
guests: [],
};
const body: CreateBookingInput_2024_04_15 = {
@@ -145,6 +144,7 @@ describe("Bookings Endpoints 2024-04-15", () => {
expect(responseBody.data.eventTypeId).toEqual(bookingEventTypeId);
expect(responseBody.data.user.timeZone).toEqual(bookingTimeZone);
expect(responseBody.data.metadata).toEqual(bookingMetadata);
expect(responseBody.data.responses).toEqual(bookingResponses);
createdBooking = responseBody.data;
});
@@ -244,6 +244,7 @@ describe("Bookings Endpoints 2024-04-15", () => {
expect(responseBody.data.eventTypeId).toEqual(bookingEventTypeId);
expect(responseBody.data.user.timeZone).toEqual(bookingTimeZone);
expect(responseBody.data.metadata).toEqual(bookingMetadata);
expect(responseBody.data.responses).toEqual(bookingResponses);
createdBooking = responseBody.data;
});
@@ -269,7 +270,7 @@ describe("Bookings Endpoints 2024-04-15", () => {
optionValue: "",
},
notes: "test",
guests: [],
guests: ["someone@example.com"],
};
const body: CreateBookingInput_2024_04_15 = {
@@ -301,6 +302,7 @@ describe("Bookings Endpoints 2024-04-15", () => {
expect(responseBody.data.eventTypeId).toEqual(bookingEventTypeId);
expect(responseBody.data.user.timeZone).toEqual(bookingTimeZone);
expect(responseBody.data.metadata).toEqual(newBookingMetadata);
expect(responseBody.data.responses).toEqual(bookingResponses);
createdBooking = responseBody.data;
});
@@ -349,6 +351,64 @@ describe("Bookings Endpoints 2024-04-15", () => {
});
});
it("should create a booking with split name", async () => {
const bookingStart = "2040-05-21T11:30:00.000Z";
const bookingEnd = "2040-05-21T12:30:00.000Z";
const bookingEventTypeId = eventTypeId;
const bookingTimeZone = "Europe/London";
const bookingLanguage = "en";
const bookingHashedLink = "";
const bookingMetadata = {
timeFormat: "12",
meetingType: "organizer-phone",
};
const bookingResponses = {
name: { firstName: "John", lastName: "Doe" },
email: "tester@example.com",
location: {
value: "link",
optionValue: "",
},
notes: "test",
};
const body: CreateBookingInput_2024_04_15 = {
start: bookingStart,
end: bookingEnd,
eventTypeId: bookingEventTypeId,
timeZone: bookingTimeZone,
language: bookingLanguage,
metadata: bookingMetadata,
hashedLink: bookingHashedLink,
responses: bookingResponses,
};
return request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.expect(201)
.then(async (response) => {
const responseBody: ApiSuccessResponse<Awaited<ReturnType<typeof handleNewBooking>>> =
response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data.userPrimaryEmail).toBeDefined();
expect(responseBody.data.userPrimaryEmail).toEqual(userEmail);
expect(responseBody.data.id).toBeDefined();
expect(responseBody.data.uid).toBeDefined();
expect(responseBody.data.startTime).toEqual(bookingStart);
expect(responseBody.data.eventTypeId).toEqual(bookingEventTypeId);
expect(responseBody.data.user.timeZone).toEqual(bookingTimeZone);
expect(responseBody.data.metadata).toEqual(bookingMetadata);
expect(responseBody.data.responses).toEqual({
...bookingResponses,
name: `${bookingResponses.name.firstName} ${bookingResponses.name.lastName}`,
});
createdBooking = responseBody.data;
});
});
afterAll(async () => {
await userRepositoryFixture.deleteByEmail(user.email);
await bookingsRepositoryFixture.deleteAllBookings(user.id, user.email);
@@ -4,6 +4,7 @@ import { MarkNoShowInput_2024_04_15 } from "@/ee/bookings/2024-04-15/inputs/mark
import { GetBookingOutput_2024_04_15 } from "@/ee/bookings/2024-04-15/outputs/get-booking.output";
import { GetBookingsOutput_2024_04_15 } from "@/ee/bookings/2024-04-15/outputs/get-bookings.output";
import { MarkNoShowOutput_2024_04_15 } from "@/ee/bookings/2024-04-15/outputs/mark-no-show.output";
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
import { hashAPIKey, isApiKey, stripApiKey } from "@/lib/api-key";
import { VERSION_2024_04_15, VERSION_2024_06_11, VERSION_2024_06_14 } from "@/lib/api-versions";
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
@@ -13,8 +14,10 @@ import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { BillingService } from "@/modules/billing/services/billing.service";
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { UsersRepository } from "@/modules/users/users.repository";
import {
Controller,
Post,
@@ -99,7 +102,9 @@ export class BookingsController_2024_04_15 {
private readonly oAuthClientRepository: OAuthClientRepository,
private readonly billingService: BillingService,
private readonly config: ConfigService,
private readonly apiKeyRepository: ApiKeysRepository
private readonly apiKeyRepository: ApiKeysRepository,
private readonly platformBookingsService: PlatformBookingsService,
private readonly usersRepository: UsersRepository
) {}
@Get("/")
@@ -166,7 +171,8 @@ export class BookingsController_2024_04_15 {
@Headers(X_CAL_CLIENT_ID) clientId?: string,
@Headers(X_CAL_PLATFORM_EMBED) isEmbed?: string
): Promise<ApiResponse<Partial<BookingResponse>>> {
const oAuthClientId = clientId?.toString();
const oAuthClientId =
clientId?.toString() || (await this.getOAuthClientIdFromEventType(body.eventTypeId));
const { orgSlug, locationUrl } = body;
req.headers["x-cal-force-slug"] = orgSlug;
try {
@@ -194,7 +200,7 @@ export class BookingsController_2024_04_15 {
async cancelBooking(
@Req() req: BookingRequest,
@Param("bookingUid") bookingUid: string,
@Body() _: CancelBookingInput_2024_04_15,
@Body() body: CancelBookingInput_2024_04_15,
@Headers(X_CAL_CLIENT_ID) clientId?: string,
@Headers(X_CAL_PLATFORM_EMBED) isEmbed?: string
): Promise<ApiResponse<{ bookingId: number; bookingUid: string; onlyRemovedAttendee: boolean }>> {
@@ -257,11 +263,12 @@ export class BookingsController_2024_04_15 {
@Post("/recurring")
async createRecurringBooking(
@Req() req: BookingRequest,
@Body() _: CreateRecurringBookingInput_2024_04_15[],
@Body() body: CreateRecurringBookingInput_2024_04_15[],
@Headers(X_CAL_CLIENT_ID) clientId?: string,
@Headers(X_CAL_PLATFORM_EMBED) isEmbed?: string
): Promise<ApiResponse<BookingResponse[]>> {
const oAuthClientId = clientId?.toString();
const oAuthClientId =
clientId?.toString() || (await this.getOAuthClientIdFromEventType(body[0]?.eventTypeId));
try {
const recurringEventId = uuidv4();
for (const recurringEvent of req.body) {
@@ -296,11 +303,12 @@ export class BookingsController_2024_04_15 {
@Post("/instant")
async createInstantBooking(
@Req() req: BookingRequest,
@Body() _: CreateBookingInput_2024_04_15,
@Body() body: CreateBookingInput_2024_04_15,
@Headers(X_CAL_CLIENT_ID) clientId?: string,
@Headers(X_CAL_PLATFORM_EMBED) isEmbed?: string
): Promise<ApiResponse<Awaited<ReturnType<typeof handleInstantMeeting>>>> {
const oAuthClientId = clientId?.toString();
const oAuthClientId =
clientId?.toString() || (await this.getOAuthClientIdFromEventType(body.eventTypeId));
req.userId = (await this.getOwnerId(req)) ?? -1;
try {
const instantMeeting = await handleInstantMeeting(
@@ -338,7 +346,8 @@ export class BookingsController_2024_04_15 {
return keyData?.userId;
} else {
// Access Token
return this.oAuthFlowService.getOwnerId(bearerToken);
const ownerId = await this.oAuthFlowService.getOwnerId(bearerToken);
return ownerId;
}
}
} catch (err) {
@@ -346,6 +355,41 @@ export class BookingsController_2024_04_15 {
}
}
private async getOwnerIdRescheduledBooking(
request: Request,
platformClientId?: string
): Promise<number | undefined> {
if (
platformClientId &&
request.body.rescheduledBy &&
!request.body.rescheduledBy.includes(platformClientId)
) {
request.body.rescheduledBy = OAuthClientUsersService.getOAuthUserEmail(
platformClientId,
request.body.rescheduledBy
);
}
if (request.body.rescheduledBy) {
if (request.body.rescheduledBy !== request.body.responses.email) {
return (await this.usersRepository.findByEmail(request.body.rescheduledBy))?.id;
}
}
return undefined;
}
private async getOAuthClientIdFromEventType(eventTypeId: number): Promise<string | undefined> {
if (!eventTypeId) {
return undefined;
}
const oAuthClientParams = await this.platformBookingsService.getOAuthClientParams(eventTypeId);
if (!oAuthClientParams) {
return undefined;
}
return oAuthClientParams.platformClientId;
}
private async getOAuthClientsParams(clientId: string, isEmbed = false): Promise<OAuthRequestParams> {
const res = { ...DEFAULT_PLATFORM_PARAMS };
@@ -379,7 +423,9 @@ export class BookingsController_2024_04_15 {
): Promise<NextApiRequest & { userId?: number } & OAuthRequestParams> {
const requestId = req.get("X-Request-Id");
const clone = { ...req };
const userId = (await this.getOwnerId(req)) ?? -1;
const userId = clone.body.rescheduleUid
? await this.getOwnerIdRescheduledBooking(req, oAuthClientId)
: await this.getOwnerId(req);
const oAuthParams = oAuthClientId
? await this.getOAuthClientsParams(oAuthClientId, this.transformToBoolean(isEmbed))
: DEFAULT_PLATFORM_PARAMS;
@@ -396,9 +442,27 @@ export class BookingsController_2024_04_15 {
noEmail: !oAuthParams.arePlatformEmailsEnabled,
creationSource: CreationSource.API_V2,
};
if (oAuthClientId) {
await this.setPlatformAttendeesEmails(clone.body, oAuthClientId);
}
return clone as unknown as NextApiRequest & { userId?: number } & OAuthRequestParams;
}
async setPlatformAttendeesEmails(requestBody: any, oAuthClientId: string): Promise<void> {
if (requestBody?.responses?.email) {
requestBody.responses.email = await this.platformBookingsService.getPlatformAttendeeEmail(
requestBody.responses.email,
oAuthClientId
);
}
if (requestBody?.responses?.guests && requestBody?.responses?.guests.length) {
requestBody.responses.guests = await this.platformBookingsService.getPlatformAttendeesEmails(
requestBody.responses.guests,
oAuthClientId
);
}
}
private async createNextApiRecurringBookingRequest(
req: BookingRequest,
oAuthClientId?: string,
@@ -425,6 +489,9 @@ export class BookingsController_2024_04_15 {
noEmail: !oAuthParams.arePlatformEmailsEnabled,
creationSource: CreationSource.API_V2,
});
if (oAuthClientId) {
await this.setPlatformAttendeesEmails(clone.body, oAuthClientId);
}
return clone as unknown as NextApiRequest & { userId?: number } & OAuthRequestParams;
}
@@ -0,0 +1,463 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { CreateBookingInput_2024_04_15 } from "@/ee/bookings/2024-04-15/inputs/create-booking.input";
import {
GetBookingsDataEntry,
GetBookingsOutput_2024_04_15,
} from "@/ee/bookings/2024-04-15/outputs/get-bookings.output";
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
import { Locales } from "@/lib/enums/locales";
import {
CreateManagedUserData,
CreateManagedUserOutput,
} from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/create-managed-user.output";
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
import { CreateManagedUserInput } from "@/modules/users/inputs/create-managed-user.input";
import { UsersModule } from "@/modules/users/users.module";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import { PlatformOAuthClient, Team, User } from "@prisma/client";
import * as request from "supertest";
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_06_14 } from "@calcom/platform-constants";
import {
ApiSuccessResponse,
CreateEventTypeInput_2024_06_14,
EventTypeOutput_2024_06_14,
} from "@calcom/platform-types";
const CLIENT_REDIRECT_URI = "http://localhost:4321";
describe("Managed user bookings 2024-04-15", () => {
let app: INestApplication;
let oAuthClient: PlatformOAuthClient;
let organization: Team;
let userRepositoryFixture: UserRepositoryFixture;
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
let teamRepositoryFixture: TeamRepositoryFixture;
let profilesRepositoryFixture: ProfileRepositoryFixture;
let membershipsRepositoryFixture: MembershipRepositoryFixture;
const platformAdminEmail = `managed-users-bookings-admin-${randomString()}@api.com`;
let platformAdmin: User;
const managedUsersTimeZone = "Europe/Rome";
const firstManagedUserEmail = `managed-user-bookings-2024-04-15-first-user@api.com`;
const secondManagedUserEmail = `managed-user-bookings-2024-04-15-second-user@api.com`;
const thirdManagedUserEmail = `managed-user-bookings-2024-04-15-third-user@api.com`;
let firstManagedUser: CreateManagedUserData;
let secondManagedUser: CreateManagedUserData;
let thirdManagedUser: CreateManagedUserData;
let firstManagedUserEventTypeId: number;
let firstManagedUserBookingsCount = 0;
let secondManagedUserBookingsCount = 0;
let thirdManagedUserBookingsCount = 0;
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
providers: [PrismaExceptionFilter, HttpExceptionFilter],
imports: [AppModule, UsersModule],
}).compile();
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
profilesRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
platformAdmin = await userRepositoryFixture.create({ email: platformAdminEmail });
organization = await teamRepositoryFixture.create({
name: `oauth-client-users-organization-${randomString()}`,
isPlatform: true,
isOrganization: true,
});
oAuthClient = await createOAuthClient(organization.id);
await profilesRepositoryFixture.create({
uid: "asd1qwwqeqw-asddsadasd",
username: platformAdminEmail,
organization: { connect: { id: organization.id } },
user: {
connect: { id: platformAdmin.id },
},
});
await membershipsRepositoryFixture.create({
role: "OWNER",
user: { connect: { id: platformAdmin.id } },
team: { connect: { id: organization.id } },
accepted: true,
});
await app.init();
});
async function createOAuthClient(organizationId: number) {
const data = {
logo: "logo-url",
name: "name",
redirectUris: [CLIENT_REDIRECT_URI],
permissions: 1023,
};
const secret = "secret";
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
return client;
}
it(`should create first managed user`, async () => {
const requestBody: CreateManagedUserInput = {
email: firstManagedUserEmail,
timeZone: managedUsersTimeZone,
weekStart: "Monday",
timeFormat: 24,
locale: Locales.FR,
name: "Alice Smith",
avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png",
};
const response = await request(app.getHttpServer())
.post(`/api/v2/oauth-clients/${oAuthClient.id}/users`)
.set("x-cal-secret-key", oAuthClient.secret)
.send(requestBody)
.expect(201);
const responseBody: CreateManagedUserOutput = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data.user.email).toEqual(
OAuthClientUsersService.getOAuthUserEmail(oAuthClient.id, requestBody.email)
);
expect(responseBody.data.accessToken).toBeDefined();
expect(responseBody.data.refreshToken).toBeDefined();
firstManagedUser = responseBody.data;
});
it("should create an event type for managed user", async () => {
const body: CreateEventTypeInput_2024_06_14 = {
title: "Managed user bookings first managed user event type",
slug: "managed-user-bookings-first-managed-user-event-type",
description: "Managed user bookings first managed user event type description",
lengthInMinutes: 30,
};
return request(app.getHttpServer())
.post("/api/v2/event-types")
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.set("Authorization", `Bearer ${firstManagedUser.accessToken}`)
.send(body)
.expect(201)
.then(async (response) => {
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
const createdEventType = responseBody.data;
expect(createdEventType).toHaveProperty("id");
expect(createdEventType.title).toEqual(body.title);
expect(createdEventType.description).toEqual(body.description);
expect(createdEventType.lengthInMinutes).toEqual(body.lengthInMinutes);
expect(createdEventType.ownerId).toEqual(firstManagedUser.user.id);
firstManagedUserEventTypeId = createdEventType.id;
});
});
it(`should create second managed user`, async () => {
const requestBody: CreateManagedUserInput = {
email: secondManagedUserEmail,
timeZone: managedUsersTimeZone,
weekStart: "Monday",
timeFormat: 24,
locale: Locales.FR,
name: "Bob Smith",
avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png",
};
const response = await request(app.getHttpServer())
.post(`/api/v2/oauth-clients/${oAuthClient.id}/users`)
.set("x-cal-secret-key", oAuthClient.secret)
.send(requestBody)
.expect(201);
const responseBody: CreateManagedUserOutput = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data.user.email).toEqual(
OAuthClientUsersService.getOAuthUserEmail(oAuthClient.id, requestBody.email)
);
expect(responseBody.data.accessToken).toBeDefined();
expect(responseBody.data.refreshToken).toBeDefined();
secondManagedUser = responseBody.data;
});
it(`should create third managed user`, async () => {
const requestBody: CreateManagedUserInput = {
email: thirdManagedUserEmail,
timeZone: managedUsersTimeZone,
weekStart: "Monday",
timeFormat: 24,
locale: Locales.FR,
name: "Charlie Smith",
avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png",
};
const response = await request(app.getHttpServer())
.post(`/api/v2/oauth-clients/${oAuthClient.id}/users`)
.set("x-cal-secret-key", oAuthClient.secret)
.send(requestBody)
.expect(201);
const responseBody: CreateManagedUserOutput = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data.user.email).toEqual(
OAuthClientUsersService.getOAuthUserEmail(oAuthClient.id, requestBody.email)
);
expect(responseBody.data.accessToken).toBeDefined();
expect(responseBody.data.refreshToken).toBeDefined();
thirdManagedUser = responseBody.data;
});
describe("bookings using original emails", () => {
it("managed user should be booked by managed user attendee and booking shows up in both users' bookings", async () => {
const body: CreateBookingInput_2024_04_15 = {
start: "2040-05-21T09:30:00.000Z",
end: "2040-05-21T10:00:00.000Z",
eventTypeId: firstManagedUserEventTypeId,
timeZone: "Europe/London",
language: "en",
metadata: {},
hashedLink: "",
responses: {
name: secondManagedUser.user.name || "unknown-name",
email: secondManagedUserEmail,
location: {
value: "attendeeInPerson",
optionValue: "rome",
},
notes: "test",
guests: [],
},
};
await request(app.getHttpServer()).post("/v2/bookings").send(body).expect(201);
firstManagedUserBookingsCount += 1;
secondManagedUserBookingsCount += 1;
const getFirstManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set("Authorization", `Bearer ${firstManagedUser.accessToken}`)
.expect(200);
const firstManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 =
getFirstManagedUserBookings.body;
const firstManagedUserBookings: GetBookingsDataEntry[] =
firstManagedUserBookingsResponseBody.data.bookings;
expect(firstManagedUserBookings.length).toEqual(firstManagedUserBookingsCount);
const getSecondManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set("Authorization", `Bearer ${secondManagedUser.accessToken}`)
.expect(200);
const secondManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 =
getSecondManagedUserBookings.body;
const secondManagedUserBookings: GetBookingsDataEntry[] =
secondManagedUserBookingsResponseBody.data.bookings;
expect(secondManagedUserBookings.length).toEqual(secondManagedUserBookingsCount);
});
it("managed user should be booked by managed user attendee and managed user as a guest and booking shows up in all three users' bookings", async () => {
const body: CreateBookingInput_2024_04_15 = {
start: "2040-05-21T10:30:00.000Z",
end: "2040-05-21T11:00:00.000Z",
eventTypeId: firstManagedUserEventTypeId,
timeZone: "Europe/London",
language: "en",
metadata: {},
hashedLink: "",
responses: {
name: thirdManagedUser.user.name || "unknown-name",
email: thirdManagedUserEmail,
location: {
value: "attendeeInPerson",
optionValue: "rome",
},
notes: "test",
guests: [secondManagedUserEmail],
},
};
await request(app.getHttpServer()).post("/v2/bookings").send(body).expect(201);
firstManagedUserBookingsCount += 1;
secondManagedUserBookingsCount += 1;
thirdManagedUserBookingsCount += 1;
const getFirstManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set("Authorization", `Bearer ${firstManagedUser.accessToken}`)
.expect(200);
const firstManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 =
getFirstManagedUserBookings.body;
const firstManagedUserBookings: GetBookingsDataEntry[] =
firstManagedUserBookingsResponseBody.data.bookings;
expect(firstManagedUserBookings.length).toEqual(firstManagedUserBookingsCount);
const getSecondManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set("Authorization", `Bearer ${secondManagedUser.accessToken}`)
.expect(200);
const secondManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 =
getSecondManagedUserBookings.body;
const secondManagedUserBookings: GetBookingsDataEntry[] =
secondManagedUserBookingsResponseBody.data.bookings;
expect(secondManagedUserBookings.length).toEqual(secondManagedUserBookingsCount);
const getThirdManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set("Authorization", `Bearer ${thirdManagedUser.accessToken}`)
.expect(200);
const thirdManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 =
getThirdManagedUserBookings.body;
const thirdManagedUserBookings: GetBookingsDataEntry[] =
thirdManagedUserBookingsResponseBody.data.bookings;
expect(thirdManagedUserBookings.length).toEqual(thirdManagedUserBookingsCount);
});
});
describe("bookings using OAuth client emails", () => {
it("managed user should be booked by managed user attendee and booking shows up in both users' bookings", async () => {
const body: CreateBookingInput_2024_04_15 = {
start: "2040-05-21T12:00:00.000Z",
end: "2040-05-21T12:30:00.000Z",
eventTypeId: firstManagedUserEventTypeId,
timeZone: "Europe/London",
language: "en",
metadata: {},
hashedLink: "",
responses: {
name: secondManagedUser.user.name || "unknown-name",
email: secondManagedUser.user.email,
location: {
value: "attendeeInPerson",
optionValue: "rome",
},
notes: "test",
guests: [],
},
};
await request(app.getHttpServer()).post("/v2/bookings").send(body).expect(201);
firstManagedUserBookingsCount += 1;
secondManagedUserBookingsCount += 1;
const getFirstManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set("Authorization", `Bearer ${firstManagedUser.accessToken}`)
.expect(200);
const firstManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 =
getFirstManagedUserBookings.body;
const firstManagedUserBookings: GetBookingsDataEntry[] =
firstManagedUserBookingsResponseBody.data.bookings;
expect(firstManagedUserBookings.length).toEqual(firstManagedUserBookingsCount);
const getSecondManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set("Authorization", `Bearer ${secondManagedUser.accessToken}`)
.expect(200);
const secondManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 =
getSecondManagedUserBookings.body;
const secondManagedUserBookings: GetBookingsDataEntry[] =
secondManagedUserBookingsResponseBody.data.bookings;
expect(secondManagedUserBookings.length).toEqual(secondManagedUserBookingsCount);
});
it("managed user should be booked by managed user attendee and managed user as a guest and booking shows up in all three users' bookings", async () => {
const body: CreateBookingInput_2024_04_15 = {
start: "2040-05-21T13:00:00.000Z",
end: "2040-05-21T13:30:00.000Z",
eventTypeId: firstManagedUserEventTypeId,
timeZone: "Europe/London",
language: "en",
metadata: {},
hashedLink: "",
responses: {
name: thirdManagedUser.user.name || "unknown-name",
email: thirdManagedUser.user.email,
location: {
value: "attendeeInPerson",
optionValue: "rome",
},
notes: "test",
guests: [secondManagedUser.user.email],
},
};
await request(app.getHttpServer()).post("/v2/bookings").send(body).expect(201);
firstManagedUserBookingsCount += 1;
secondManagedUserBookingsCount += 1;
thirdManagedUserBookingsCount += 1;
const getFirstManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set("Authorization", `Bearer ${firstManagedUser.accessToken}`)
.expect(200);
const firstManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 =
getFirstManagedUserBookings.body;
const firstManagedUserBookings: GetBookingsDataEntry[] =
firstManagedUserBookingsResponseBody.data.bookings;
expect(firstManagedUserBookings.length).toEqual(firstManagedUserBookingsCount);
const getSecondManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set("Authorization", `Bearer ${secondManagedUser.accessToken}`)
.expect(200);
const secondManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 =
getSecondManagedUserBookings.body;
const secondManagedUserBookings: GetBookingsDataEntry[] =
secondManagedUserBookingsResponseBody.data.bookings;
expect(secondManagedUserBookings.length).toEqual(secondManagedUserBookingsCount);
const getThirdManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set("Authorization", `Bearer ${thirdManagedUser.accessToken}`)
.expect(200);
const thirdManagedUserBookingsResponseBody: GetBookingsOutput_2024_04_15 =
getThirdManagedUserBookings.body;
const thirdManagedUserBookings: GetBookingsDataEntry[] =
thirdManagedUserBookingsResponseBody.data.bookings;
expect(thirdManagedUserBookings.length).toEqual(thirdManagedUserBookingsCount);
});
});
afterAll(async () => {
await userRepositoryFixture.delete(firstManagedUser.user.id);
await userRepositoryFixture.delete(secondManagedUser.user.id);
await userRepositoryFixture.delete(thirdManagedUser.user.id);
await userRepositoryFixture.delete(platformAdmin.id);
await oauthClientRepositoryFixture.delete(oAuthClient.id);
await teamRepositoryFixture.delete(organization.id);
await app.close();
});
});
@@ -8,9 +8,45 @@ import {
IsOptional,
IsArray,
IsObject,
IsEmail,
ValidateNested,
isEmail,
Validate,
} from "class-validator";
import { ValidationOptions, registerDecorator } from "class-validator";
import { RESCHEDULED_BY_DOCS } from "@calcom/platform-types";
type BookingName = { firstName: string; lastName: string };
function ValidateBookingName(validationOptions?: ValidationOptions) {
return function (target: object, propertyName: string) {
registerDecorator({
name: "validateBookingName",
target: target.constructor,
propertyName: propertyName,
options: validationOptions,
validator: {
validate(value: any) {
if (typeof value === "string") {
return value.trim().length > 0;
}
if (typeof value === "object" && value !== null) {
return (
typeof value.firstName === "string" &&
typeof value.lastName === "string" &&
value.firstName.trim().length > 0 &&
value.lastName.trim().length > 0
);
}
return false;
},
defaultMessage(): string {
return "Name must be either a non-empty string or an object with non-empty firstName and lastName";
},
},
});
};
}
class Location {
@IsString()
@@ -23,18 +59,26 @@ class Location {
}
class Response {
@IsString()
@ApiProperty()
name!: string;
@ApiProperty({
oneOf: [
{ type: "string" },
{ type: "object", properties: { firstName: { type: "string" }, lastName: { type: "string" } } },
],
})
@ValidateBookingName()
name!: string | BookingName;
@IsEmail()
@Validate((value: string) => !value || isEmail(value), {
message: "Invalid response email",
})
@ApiProperty()
email!: string;
@IsOptional()
@IsArray()
@IsString({ each: true })
@ApiProperty({ type: [String] })
guests!: string[];
@ApiPropertyOptional({ type: [String] })
guests?: string[];
@IsOptional()
@ValidateNested()
@@ -72,6 +116,13 @@ export class CreateBookingInput_2024_04_15 {
@ApiPropertyOptional()
rescheduleUid?: string;
@IsOptional()
@ApiPropertyOptional({ description: RESCHEDULED_BY_DOCS })
@Validate((value: string) => !value || isEmail(value), {
message: "Invalid rescheduledBy email format",
})
rescheduledBy?: string;
@IsTimeZone()
@ApiProperty()
timeZone!: string;
@@ -113,8 +164,9 @@ export class CreateBookingInput_2024_04_15 {
@ApiPropertyOptional()
seatReferenceUid?: string;
@Type(() => Response)
@ApiProperty({ type: Response })
@ValidateNested()
@Type(() => Response)
responses!: Response;
@IsString()
@@ -2,7 +2,7 @@ import { CreateBookingInput_2024_04_15 } from "@/ee/bookings/2024-04-15/inputs/c
import { ApiPropertyOptional } from "@nestjs/swagger";
import { IsBoolean, IsString, IsNumber, IsOptional } from "class-validator";
import type { AppsStatus } from "@calcom/platform-libraries";
import type { AppsStatus } from "@calcom/platform-libraries/app-store";
export class CreateRecurringBookingInput_2024_04_15 extends CreateBookingInput_2024_04_15 {
@IsBoolean()
@@ -160,7 +160,7 @@ class User {
email!: string;
}
class GetBookingsDataEntry {
export class GetBookingsDataEntry {
@IsInt()
@ApiProperty()
id!: number;
@@ -3,26 +3,47 @@ import { BookingsController_2024_08_13 } from "@/ee/bookings/2024-08-13/controll
import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service";
import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service";
import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service";
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module";
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
import { AppsRepository } from "@/modules/apps/apps.repository";
import { BillingModule } from "@/modules/billing/billing.module";
import { BookingSeatModule } from "@/modules/booking-seat/booking-seat.module";
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { RedisModule } from "@/modules/redis/redis.module";
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
import { TokensModule } from "@/modules/tokens/tokens.module";
import { TokensRepository } from "@/modules/tokens/tokens.repository";
import { UsersModule } from "@/modules/users/users.module";
import { Module } from "@nestjs/common";
@Module({
imports: [PrismaModule, RedisModule, TokensModule, BillingModule, UsersModule, BookingSeatModule],
imports: [
PrismaModule,
RedisModule,
TokensModule,
BillingModule,
UsersModule,
BookingSeatModule,
SchedulesModule_2024_04_15,
EventTypesModule_2024_04_15,
EventTypesModule_2024_06_14,
],
providers: [
TokensRepository,
OAuthFlowService,
OAuthClientRepository,
OAuthClientUsersService,
BookingsService_2024_08_13,
InputBookingsService_2024_08_13,
OutputBookingsService_2024_08_13,
@@ -30,6 +51,12 @@ import { Module } from "@nestjs/common";
EventTypesRepository_2024_06_14,
BookingSeatRepository,
ApiKeysRepository,
PlatformBookingsService,
CalendarsService,
CredentialsRepository,
AppsRepository,
CalendarsRepository,
SelectedCalendarsRepository,
],
controllers: [BookingsController_2024_08_13],
exports: [InputBookingsService_2024_08_13, OutputBookingsService_2024_08_13, BookingsService_2024_08_13],
@@ -1,7 +1,7 @@
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { Injectable } from "@nestjs/common";
import type { Prisma } from "@prisma/client";
@Injectable()
export class BookingsRepository_2024_08_13 {
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
@@ -98,7 +98,7 @@ export class BookingsRepository_2024_08_13 {
}
async getByUidWithAttendeesAndUserAndEvent(uid: string) {
return this.dbRead.prisma.booking.findUnique({
const booking = await this.dbRead.prisma.booking.findUnique({
where: {
uid,
},
@@ -108,6 +108,15 @@ export class BookingsRepository_2024_08_13 {
eventType: true,
},
});
if (!booking) {
return null;
}
return {
...booking,
responses: booking.responses as Prisma.JsonObject,
metadata: booking.metadata as Prisma.JsonObject | null,
};
}
async getByUidWithAttendeesWithBookingSeatAndUserAndEvent(uid: string) {
@@ -1,4 +1,5 @@
import { BookingUidGuard } from "@/ee/bookings/2024-08-13/guards/booking-uid.guard";
import { CalendarLinksOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/calendar-links.output";
import { CancelBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/cancel-booking.output";
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
import { MarkAbsentBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/mark-absent.output";
@@ -38,11 +39,15 @@ import { Request } from "express";
import { BOOKING_READ, BOOKING_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
import {
CancelBookingInput,
CancelBookingInput_2024_08_13,
CancelBookingInputPipe,
CancelSeatedBookingInput_2024_08_13,
GetBookingOutput_2024_08_13,
GetBookingsOutput_2024_08_13,
RescheduleBookingInput,
RescheduleBookingInput_2024_08_13,
RescheduleBookingInputPipe,
RescheduleSeatedBookingInput_2024_08_13,
} from "@calcom/platform-types";
import {
CreateBookingInputPipe,
@@ -64,9 +69,12 @@ import {
@DocsTags("Bookings")
@ApiHeader({
name: "cal-api-version",
description: `Must be set to \`2024-08-13\``,
description: `Must be set to ${VERSION_2024_08_13}`,
example: VERSION_2024_08_13,
required: true,
schema: {
default: VERSION_2024_08_13,
},
})
export class BookingsController_2024_08_13 {
private readonly logger = new Logger("BookingsController_2024_08_13");
@@ -98,7 +106,7 @@ export class BookingsController_2024_08_13 {
],
},
description:
"Accepts different types of booking input: CreateBookingInput_2024_08_13, CreateInstantBookingInput_2024_08_13, or CreateRecurringBookingInput_2024_08_13",
"Accepts different types of booking input: Create Booking (Option 1), Create Instant Booking (Option 2), or Create Recurring Booking (Option 3)",
})
@ApiExtraModels(
CreateBookingInput_2024_08_13,
@@ -171,9 +179,19 @@ export class BookingsController_2024_08_13 {
@UseGuards(BookingUidGuard)
@ApiOperation({
summary: "Reschedule a booking",
description:
"Reschedule a booking by passing `:bookingUid` of the booking that should be rescheduled and pass request body with a new start time to create a new booking.",
description: "Reschedule a booking or seated booking",
})
@ApiBody({
schema: {
oneOf: [
{ $ref: getSchemaPath(RescheduleBookingInput_2024_08_13) },
{ $ref: getSchemaPath(RescheduleSeatedBookingInput_2024_08_13) },
],
},
description:
"Accepts different types of reschedule booking input: Reschedule Booking (Option 1) or Reschedule Seated Booking (Option 2)",
})
@ApiExtraModels(RescheduleBookingInput_2024_08_13, RescheduleSeatedBookingInput_2024_08_13)
async rescheduleBooking(
@Param("bookingUid") bookingUid: string,
@Body(new RescheduleBookingInputPipe())
@@ -198,6 +216,17 @@ export class BookingsController_2024_08_13 {
For seated bookings to cancel one individual booking provide :bookingUid and :seatUid in the request body. For recurring seated bookings it is not possible to cancel all of them with 1 call
like with non-seated recurring bookings by providing recurring bookind uid - you have to cancel each recurrence booking by its bookingUid + seatUid.`,
})
@ApiBody({
schema: {
oneOf: [
{ $ref: getSchemaPath(CancelBookingInput_2024_08_13) },
{ $ref: getSchemaPath(CancelSeatedBookingInput_2024_08_13) },
],
},
description:
"Accepts different types of cancel booking input: Cancel Booking (Option 1) or Cancel Seated Booking (Option 2)",
})
@ApiExtraModels(CancelBookingInput_2024_08_13, CancelSeatedBookingInput_2024_08_13)
async cancelBooking(
@Req() request: Request,
@Param("bookingUid") bookingUid: string,
@@ -335,4 +364,22 @@ export class BookingsController_2024_08_13 {
data: booking,
};
}
@Get("/:bookingUid/calendar-links")
@UseGuards(ApiAuthGuard, BookingUidGuard)
@Permissions([BOOKING_READ])
@ApiOperation({
summary: "Get 'Add to Calendar' links for a booking",
description:
"Retrieve calendar links for a booking that can be used to add the event to various calendar services. Returns links for Google Calendar, Microsoft Office, Microsoft Outlook, and a downloadable ICS file.",
})
@HttpCode(HttpStatus.OK)
async getCalendarLinks(@Param("bookingUid") bookingUid: string): Promise<CalendarLinksOutput_2024_08_13> {
const calendarLinks = await this.bookingsService.getCalendarLinks(bookingUid);
return {
status: SUCCESS_STATUS,
data: calendarLinks,
};
}
}
@@ -30,7 +30,7 @@ import {
OrganizerRescheduledEmail,
AttendeeCancelledEmail,
OrganizerCancelledEmail,
} from "@calcom/platform-libraries";
} from "@calcom/platform-libraries/emails";
import {
CreateBookingInput_2024_08_13,
BookingOutput_2024_08_13,
@@ -189,7 +189,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
createdBooking = data;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
@@ -238,7 +238,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
rescheduledBooking = data;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
@@ -434,7 +434,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
}
describe("get individual booking", () => {
it("should should get a seated booking with split name responses", async () => {
it("should get a seated booking with split name responses", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings/${bookingWithSplitName.uid}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -451,13 +451,13 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.bookingFieldsResponses.name).toEqual(splitName);
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
});
it("should should get a seated booking with split name responses", async () => {
it("should get a seated booking with split name responses", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings/${seatedBookingWithSplitName.uid}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -476,7 +476,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.attendees[0].bookingFieldsResponses.name).toEqual(splitName);
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
@@ -536,7 +536,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.bookingFieldsResponses["video-url"]).toEqual("http://video-url.com");
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
@@ -0,0 +1,180 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { UsersModule } from "@/modules/users/users.module";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import { User } from "@prisma/client";
import * as request from "supertest";
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
import { Booking, PlatformOAuthClient, Team } from "@calcom/prisma/client";
describe("Bookings Endpoints 2024-08-13", () => {
describe("Calendar Links", () => {
let app: INestApplication;
let organization: Team;
let userRepositoryFixture: UserRepositoryFixture;
let bookingsRepositoryFixture: BookingsRepositoryFixture;
let schedulesService: SchedulesService_2024_04_15;
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
let oAuthClient: PlatformOAuthClient;
let teamRepositoryFixture: TeamRepositoryFixture;
const userEmail = `calendar-links-2024-08-13-user-${randomString()}@api.com`;
let user: User;
let booking: Booking;
beforeAll(async () => {
const moduleRef = await withApiAuth(
userEmail,
Test.createTestingModule({
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
})
)
.overrideGuard(PermissionsGuard)
.useValue({
canActivate: () => true,
})
.compile();
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
organization = await teamRepositoryFixture.create({
name: `confirm-bookings-2024-08-13-organization-${randomString()}`,
});
oAuthClient = await createOAuthClient(organization.id);
user = await userRepositoryFixture.create({
email: userEmail,
platformOAuthClients: {
connect: {
id: oAuthClient.id,
},
},
});
const userSchedule: CreateScheduleInput_2024_04_15 = {
name: `calendar-links-2024-08-13-${randomString()}-schedule`,
timeZone: "Europe/Rome",
isDefault: true,
};
await schedulesService.createUserSchedule(user.id, userSchedule);
const event = await eventTypesRepositoryFixture.create(
{
title: `calendar-links-2024-08-13-${randomString()}-event-type`,
slug: `calendar-links-2024-08-13-${randomString()}-event-type-slug`,
length: 60,
},
user.id
);
booking = await bookingsRepositoryFixture.create({
user: {
connect: {
id: user.id,
},
},
startTime: new Date(Date.UTC(2020, 0, 8, 12, 0, 0)),
endTime: new Date(Date.UTC(2020, 0, 8, 13, 0, 0)),
uid: `calendar-links-2024-08-13-booking-${randomString()}`,
title: "Test Booking",
eventType: {
connect: {
id: event.id,
},
},
location: "integrations:daily",
customInputs: {},
metadata: {},
attendees: {
create: {
email: "johndoe@example.com",
name: "John Doe",
locale: "en",
timeZone: "America/New_York",
},
},
responses: {
create: {
name: "John Doe",
email: "johndoe@example.com",
},
},
});
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
await app.init();
});
async function createOAuthClient(organizationId: number) {
const data = {
logo: "logo-url",
name: "name",
redirectUris: ["http://localhost:5555"],
permissions: 32,
};
const secret = "secret";
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
return client;
}
it("should get calendar links for a booking", async () => {
const response = await request(app.getHttpServer())
.get(`/v2/bookings/${booking.uid}/calendar-links`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200);
expect(response.body.status).toEqual(SUCCESS_STATUS);
expect(response.body.data).toBeDefined();
const googleCalendarLink = response.body.data.find((item: { id: string }) => item.id === 'googleCalendar').link;
const microsoftOfficeLink = response.body.data.find((item: { id: string }) => item.id === 'microsoftOffice').link;
const microsoftOutlookLink = response.body.data.find((item: { id: string }) => item.id === 'microsoftOutlook').link;
const icsLink = response.body.data.find((item: { id: string }) => item.id === 'ics').link;
expect(googleCalendarLink).toMatch(/^https:\/\/calendar\.google\.com\//);
expect(microsoftOfficeLink).toMatch(/^https:\/\/outlook\.office\.com\//);
expect(microsoftOutlookLink).toMatch(/^https:\/\/outlook\.live\.com\//);
expect(icsLink).toMatch(/^data:text\/calendar,/);
});
it("should return 404 for non-existent booking", async () => {
await request(app.getHttpServer())
.get(`/v2/bookings/non-existent-uid/calendar-links`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(404);
});
afterAll(async () => {
await oauthClientRepositoryFixture.delete(oAuthClient.id);
await teamRepositoryFixture.delete(organization.id);
await userRepositoryFixture.deleteByEmail(user.email);
await bookingsRepositoryFixture.deleteAllBookings(user.id, user.email);
await app.close();
});
});
});
@@ -174,7 +174,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(dbBooking?.status).toEqual("ACCEPTED");
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
@@ -235,7 +235,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(dbBooking?.status).toEqual("REJECTED");
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
@@ -34,7 +34,7 @@ import {
AttendeeRequestEmail,
OrganizerRequestEmail,
AttendeeDeclinedEmail,
} from "@calcom/platform-libraries";
} from "@calcom/platform-libraries/emails";
import {
CreateBookingInput_2024_08_13,
BookingOutput_2024_08_13,
@@ -94,6 +94,8 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
let emailsDisabledSetup: EmailSetup;
const authEmail = `confirm-emails-2024-08-13-admin-${randomString()}@api.com`;
let userEmailsEnabled = "";
const userEmailsDisabled = `confirm-emails-2024-08-13-user-${randomString()}@api.com`;
beforeAll(async () => {
const moduleRef = await withApiAuth(
@@ -141,8 +143,12 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
async function setupEnabledEmails() {
const oAuthClientEmailsEnabled = await createOAuthClient(organization.id, true);
userEmailsEnabled = `confirm-emails-2024-08-13-user-${randomString()}+${
oAuthClientEmailsEnabled.id
}@api.com`;
const user = await userRepositoryFixture.create({
email: `confirm-emails-2024-08-13-user-${randomString()}@api.com`,
email: userEmailsEnabled,
platformOAuthClients: {
connect: {
id: oAuthClientEmailsEnabled.id,
@@ -179,7 +185,7 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
const oAuthClientEmailsDisabled = await createOAuthClient(organization.id, false);
const user = await userRepositoryFixture.create({
email: `confirm-emails-2024-08-13-user-${randomString()}@api.com`,
email: userEmailsDisabled,
platformOAuthClients: {
connect: {
id: oAuthClientEmailsDisabled.id,
@@ -265,7 +271,7 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
emailsDisabledSetup.createdBookingUid = responseBody.data.uid;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
@@ -282,9 +288,6 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
expect(responseBody.data).toBeDefined();
expect(AttendeeScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(OrganizerScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
emailsDisabledSetup.rescheduledBookingUid = responseBody.data.uid;
});
});
@@ -322,7 +325,7 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
emailsDisabledSetup.createdBookingUid = responseBody.data.uid;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
@@ -338,10 +341,6 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(AttendeeDeclinedEmail.prototype.getHtml).not.toHaveBeenCalled();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
emailsDisabledSetup.rescheduledBookingUid = responseBody.data.uid;
});
});
});
@@ -351,117 +350,176 @@ describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
jest.clearAllMocks();
});
it("should send an email when creating a booking that requires confirmation", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
eventTypeId: emailsEnabledSetup.eventTypeId,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: "https://meet.google.com/abc-def-ghi",
bookingFieldsResponses: {
customField: "customValue",
},
metadata: {
userId: "100",
},
};
describe("confirming booking that requires confirmation and rescheduling it", () => {
it("should send an email when creating a booking that requires confirmation", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
eventTypeId: emailsEnabledSetup.eventTypeId,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: "https://meet.google.com/abc-def-ghi",
bookingFieldsResponses: {
customField: "customValue",
},
metadata: {
userId: "100",
},
};
return request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201)
.then(async (response) => {
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
if (responseDataIsBooking(responseBody.data)) {
expect(responseBody.data.status).toEqual("pending");
expect(AttendeeRequestEmail.prototype.getHtmlRequestEmail).toHaveBeenCalled();
expect(OrganizerRequestEmail.prototype.getHtmlRequestEmail).toHaveBeenCalled();
emailsEnabledSetup.createdBookingUid = responseBody.data.uid;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
);
}
return request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201)
.then(async (response) => {
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
if (responseDataIsBooking(responseBody.data)) {
expect(responseBody.data.status).toEqual("pending");
expect(AttendeeRequestEmail.prototype.getHtmlRequestEmail).toHaveBeenCalled();
expect(OrganizerRequestEmail.prototype.getHtmlRequestEmail).toHaveBeenCalled();
emailsEnabledSetup.createdBookingUid = responseBody.data.uid;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
);
}
});
});
it("should send an email when confirming a booking", async () => {
return request(app.getHttpServer())
.post(`/v2/bookings/${emailsEnabledSetup.createdBookingUid}/confirm`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: GetBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(AttendeeScheduledEmail.prototype.getHtml).toHaveBeenCalled();
expect(OrganizerScheduledEmail.prototype.getHtml).toHaveBeenCalled();
});
});
describe("rescheduling emails", () => {
it("should send confirmation emails when organizer reschedules a booking that requires confirmation", async () => {
const body: RescheduleBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 14, 30, 0)).toISOString(),
rescheduledBy: userEmailsEnabled,
};
return request(app.getHttpServer())
.post(`/v2/bookings/${emailsEnabledSetup.createdBookingUid}/reschedule`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201)
.then(async (response) => {
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
if (responseDataIsBooking(responseBody.data)) {
expect(responseBody.data.status).toEqual("accepted");
expect(AttendeeRescheduledEmail.prototype.getHtml).toHaveBeenCalled();
expect(OrganizerRescheduledEmail.prototype.getHtml).toHaveBeenCalled();
expect(AttendeeRequestEmail.prototype.getHtmlRequestEmail).not.toHaveBeenCalled();
expect(OrganizerRequestEmail.prototype.getHtmlRequestEmail).not.toHaveBeenCalled();
emailsEnabledSetup.rescheduledBookingUid = responseBody.data.uid;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
);
}
});
});
it("should send requested rescheduling emails when attendee rescheduling a booking that requires confirmation", async () => {
const body: RescheduleBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 9, 0, 0)).toISOString(),
};
return request(app.getHttpServer())
.post(`/v2/bookings/${emailsEnabledSetup.rescheduledBookingUid}/reschedule`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201)
.then(async (response) => {
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
if (responseDataIsBooking(responseBody.data)) {
expect(responseBody.data.status).toEqual("pending");
expect(AttendeeRescheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(OrganizerRescheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
expect(AttendeeRequestEmail.prototype.getHtmlRequestEmail).toHaveBeenCalled();
expect(OrganizerRequestEmail.prototype.getHtmlRequestEmail).toHaveBeenCalled();
emailsEnabledSetup.rescheduledBookingUid = responseBody.data.uid;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
);
}
});
});
});
});
it("should send an email when confirming a booking", async () => {
return request(app.getHttpServer())
.post(`/v2/bookings/${emailsEnabledSetup.createdBookingUid}/confirm`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: GetBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(AttendeeScheduledEmail.prototype.getHtml).toHaveBeenCalled();
expect(OrganizerScheduledEmail.prototype.getHtml).toHaveBeenCalled();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
emailsEnabledSetup.rescheduledBookingUid = responseBody.data.uid;
});
});
describe("declining booking that requires confirmation", () => {
it("should send an email when creating a booking that requires confirmation", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 10, 0, 0)).toISOString(),
eventTypeId: emailsEnabledSetup.eventTypeId,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: "https://meet.google.com/abc-def-ghi",
bookingFieldsResponses: {
customField: "customValue",
},
metadata: {
userId: "100",
},
};
it("should send an email when creating a booking that requires confirmation", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 10, 0, 0)).toISOString(),
eventTypeId: emailsEnabledSetup.eventTypeId,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: "https://meet.google.com/abc-def-ghi",
bookingFieldsResponses: {
customField: "customValue",
},
metadata: {
userId: "100",
},
};
return request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201)
.then(async (response) => {
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
if (responseDataIsBooking(responseBody.data)) {
expect(responseBody.data.status).toEqual("pending");
expect(AttendeeRequestEmail.prototype.getHtmlRequestEmail).toHaveBeenCalled();
expect(OrganizerRequestEmail.prototype.getHtmlRequestEmail).toHaveBeenCalled();
emailsEnabledSetup.createdBookingUid = responseBody.data.uid;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
);
}
});
});
return request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201)
.then(async (response) => {
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
if (responseDataIsBooking(responseBody.data)) {
expect(responseBody.data.status).toEqual("pending");
expect(AttendeeRequestEmail.prototype.getHtmlRequestEmail).toHaveBeenCalled();
expect(OrganizerRequestEmail.prototype.getHtmlRequestEmail).toHaveBeenCalled();
emailsEnabledSetup.createdBookingUid = responseBody.data.uid;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
);
}
});
});
it("should send an email when declining a booking", async () => {
return request(app.getHttpServer())
.post(`/v2/bookings/${emailsEnabledSetup.createdBookingUid}/decline`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: GetBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(AttendeeDeclinedEmail.prototype.getHtml).toHaveBeenCalled();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
emailsEnabledSetup.rescheduledBookingUid = responseBody.data.uid;
});
it("should send an email when declining a booking", async () => {
return request(app.getHttpServer())
.post(`/v2/bookings/${emailsEnabledSetup.createdBookingUid}/decline`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: GetBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(AttendeeDeclinedEmail.prototype.getHtml).toHaveBeenCalled();
});
});
});
});
@@ -36,7 +36,7 @@ import {
AttendeeCancelledEmail,
OrganizerReassignedEmail,
AttendeeUpdatedEmail,
} from "@calcom/platform-libraries";
} from "@calcom/platform-libraries/emails";
import {
CreateBookingInput_2024_08_13,
BookingOutput_2024_08_13,
@@ -570,7 +570,7 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
emailsDisabledSetup.collectiveEventType.createdBookingUid = responseBody.data.uid;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
@@ -610,7 +610,7 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
emailsDisabledSetup.roundRobinEventType.currentHostId = responseBody.data.hosts[0].id;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
@@ -787,7 +787,7 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
emailsEnabledSetup.collectiveEventType.createdBookingUid = responseBody.data.uid;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
@@ -827,7 +827,7 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
emailsEnabledSetup.roundRobinEventType.currentHostId = responseBody.data.hosts[0].id;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
@@ -30,7 +30,7 @@ import {
AttendeeRescheduledEmail,
OrganizerCancelledEmail,
AttendeeCancelledEmail,
} from "@calcom/platform-libraries";
} from "@calcom/platform-libraries/emails";
import {
CreateBookingInput_2024_08_13,
BookingOutput_2024_08_13,
@@ -261,7 +261,7 @@ describe("Bookings Endpoints 2024-08-13 user emails", () => {
emailsDisabledSetup.createdBookingUid = responseBody.data.uid;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
@@ -381,7 +381,7 @@ describe("Bookings Endpoints 2024-08-13 user emails", () => {
emailsEnabledSetup.createdBookingUid = responseBody.data.uid;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
@@ -0,0 +1,489 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
import { Locales } from "@/lib/enums/locales";
import {
CreateManagedUserData,
CreateManagedUserOutput,
} from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/create-managed-user.output";
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
import { CreateManagedUserInput } from "@/modules/users/inputs/create-managed-user.input";
import { UsersModule } from "@/modules/users/users.module";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import { PlatformOAuthClient, Team, User } from "@prisma/client";
import * as request from "supertest";
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
import { SchedulesRepositoryFixture } from "test/fixtures/repository/schedules.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { randomString } from "test/utils/randomString";
import {
CAL_API_VERSION_HEADER,
SUCCESS_STATUS,
VERSION_2024_06_14,
VERSION_2024_08_13,
} from "@calcom/platform-constants";
import {
ApiSuccessResponse,
BookingOutput_2024_08_13,
CreateBookingInput_2024_08_13,
CreateEventTypeInput_2024_06_14,
EventTypeOutput_2024_06_14,
GetBookingsOutput_2024_08_13,
} from "@calcom/platform-types";
const CLIENT_REDIRECT_URI = "http://localhost:4321";
describe("Managed user bookings 2024-08-13", () => {
let app: INestApplication;
let oAuthClient: PlatformOAuthClient;
let organization: Team;
let userRepositoryFixture: UserRepositoryFixture;
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
let teamRepositoryFixture: TeamRepositoryFixture;
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
let schedulesRepositoryFixture: SchedulesRepositoryFixture;
let profilesRepositoryFixture: ProfileRepositoryFixture;
let membershipsRepositoryFixture: MembershipRepositoryFixture;
const platformAdminEmail = `managed-users-bookings-2024-08-13-admin-${randomString()}@api.com`;
let platformAdmin: User;
const managedUsersTimeZone = "Europe/Rome";
const firstManagedUserEmail = `managed-user-bookings-2024-08-13-first-user-${randomString()}@api.com`;
const secondManagedUserEmail = `managed-user-bookings-2024-08-13-second-user-${randomString()}@api.com`;
const thirdManagedUserEmail = `managed-user-bookings-2024-08-13-third-user-${randomString()}@api.com`;
let firstManagedUser: CreateManagedUserData;
let secondManagedUser: CreateManagedUserData;
let thirdManagedUser: CreateManagedUserData;
let firstManagedUserEventTypeId: number;
let firstManagedUserBookingsCount = 0;
let secondManagedUserBookingsCount = 0;
let thirdManagedUserBookingsCount = 0;
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
providers: [PrismaExceptionFilter, HttpExceptionFilter],
imports: [AppModule, UsersModule],
}).compile();
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
schedulesRepositoryFixture = new SchedulesRepositoryFixture(moduleRef);
profilesRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
platformAdmin = await userRepositoryFixture.create({ email: platformAdminEmail });
organization = await teamRepositoryFixture.create({
name: `oauth-client-users-organization-${randomString()}`,
isPlatform: true,
isOrganization: true,
});
oAuthClient = await createOAuthClient(organization.id);
await profilesRepositoryFixture.create({
uid: "asd1qwwqeqw-asddsadasd",
username: platformAdminEmail,
organization: { connect: { id: organization.id } },
user: {
connect: { id: platformAdmin.id },
},
});
await membershipsRepositoryFixture.create({
role: "OWNER",
user: { connect: { id: platformAdmin.id } },
team: { connect: { id: organization.id } },
accepted: true,
});
await app.init();
});
async function createOAuthClient(organizationId: number) {
const data = {
logo: "logo-url",
name: "name",
redirectUris: [CLIENT_REDIRECT_URI],
permissions: 1023,
};
const secret = "secret";
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
return client;
}
it(`should create first managed user`, async () => {
const requestBody: CreateManagedUserInput = {
email: firstManagedUserEmail,
timeZone: managedUsersTimeZone,
weekStart: "Monday",
timeFormat: 24,
locale: Locales.FR,
name: "Alice Smith",
avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png",
};
const response = await request(app.getHttpServer())
.post(`/api/v2/oauth-clients/${oAuthClient.id}/users`)
.set("x-cal-secret-key", oAuthClient.secret)
.send(requestBody)
.expect(201);
const responseBody: CreateManagedUserOutput = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data.user.email).toEqual(
OAuthClientUsersService.getOAuthUserEmail(oAuthClient.id, requestBody.email)
);
expect(responseBody.data.accessToken).toBeDefined();
expect(responseBody.data.refreshToken).toBeDefined();
firstManagedUser = responseBody.data;
});
it("should create an event type for managed user", async () => {
const body: CreateEventTypeInput_2024_06_14 = {
title: "Managed user bookings first managed user event type",
slug: "managed-user-bookings-first-managed-user-event-type",
description: "Managed user bookings first managed user event type description",
lengthInMinutes: 30,
};
return request(app.getHttpServer())
.post("/api/v2/event-types")
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.set("Authorization", `Bearer ${firstManagedUser.accessToken}`)
.send(body)
.expect(201)
.then(async (response) => {
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
const createdEventType = responseBody.data;
expect(createdEventType).toHaveProperty("id");
expect(createdEventType.title).toEqual(body.title);
expect(createdEventType.description).toEqual(body.description);
expect(createdEventType.lengthInMinutes).toEqual(body.lengthInMinutes);
expect(createdEventType.ownerId).toEqual(firstManagedUser.user.id);
firstManagedUserEventTypeId = createdEventType.id;
});
});
it(`should create second managed user`, async () => {
const requestBody: CreateManagedUserInput = {
email: secondManagedUserEmail,
timeZone: managedUsersTimeZone,
weekStart: "Monday",
timeFormat: 24,
locale: Locales.FR,
name: "Bob Smith",
avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png",
};
const response = await request(app.getHttpServer())
.post(`/api/v2/oauth-clients/${oAuthClient.id}/users`)
.set("x-cal-secret-key", oAuthClient.secret)
.send(requestBody)
.expect(201);
const responseBody: CreateManagedUserOutput = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data.user.email).toEqual(
OAuthClientUsersService.getOAuthUserEmail(oAuthClient.id, requestBody.email)
);
expect(responseBody.data.accessToken).toBeDefined();
expect(responseBody.data.refreshToken).toBeDefined();
secondManagedUser = responseBody.data;
});
it(`should create third managed user`, async () => {
const requestBody: CreateManagedUserInput = {
email: thirdManagedUserEmail,
timeZone: managedUsersTimeZone,
weekStart: "Monday",
timeFormat: 24,
locale: Locales.FR,
name: "Charlie Smith",
avatarUrl: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png",
};
const response = await request(app.getHttpServer())
.post(`/api/v2/oauth-clients/${oAuthClient.id}/users`)
.set("x-cal-secret-key", oAuthClient.secret)
.send(requestBody)
.expect(201);
const responseBody: CreateManagedUserOutput = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data.user.email).toEqual(
OAuthClientUsersService.getOAuthUserEmail(oAuthClient.id, requestBody.email)
);
expect(responseBody.data.accessToken).toBeDefined();
expect(responseBody.data.refreshToken).toBeDefined();
thirdManagedUser = responseBody.data;
});
describe("bookings using original emails", () => {
it("managed user should be booked by managed user attendee and booking shows up in both users' bookings", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
eventTypeId: firstManagedUserEventTypeId,
attendee: {
name: secondManagedUser.user.name!,
email: secondManagedUserEmail,
timeZone: secondManagedUser.user.timeZone,
language: secondManagedUser.user.locale,
},
location: "https://meet.google.com/abc-def-ghi",
};
await request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201);
firstManagedUserBookingsCount += 1;
secondManagedUserBookingsCount += 1;
const getFirstManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${firstManagedUser.accessToken}`)
.expect(200);
const firstManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 =
getFirstManagedUserBookings.body;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const firstManagedUserBookings: BookingOutput_2024_08_13[] = firstManagedUserBookingsResponseBody.data;
expect(firstManagedUserBookings.length).toEqual(firstManagedUserBookingsCount);
const getSecondManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${secondManagedUser.accessToken}`)
.expect(200);
const secondManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 =
getSecondManagedUserBookings.body;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const secondManagedUserBookings: BookingOutput_2024_08_13[] =
secondManagedUserBookingsResponseBody.data;
expect(secondManagedUserBookings.length).toEqual(secondManagedUserBookingsCount);
});
it("managed user should be booked by managed user attendee and managed user as a guest and booking shows up in all three users' bookings", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 14, 0, 0)).toISOString(),
eventTypeId: firstManagedUserEventTypeId,
attendee: {
name: thirdManagedUser.user.name!,
email: thirdManagedUserEmail,
timeZone: secondManagedUser.user.timeZone,
language: secondManagedUser.user.locale,
},
guests: [secondManagedUserEmail],
location: "https://meet.google.com/abc-def-ghi",
};
await request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201);
firstManagedUserBookingsCount += 1;
secondManagedUserBookingsCount += 1;
thirdManagedUserBookingsCount += 1;
const getFirstManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${firstManagedUser.accessToken}`)
.expect(200);
const firstManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 =
getFirstManagedUserBookings.body;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const firstManagedUserBookings: BookingOutput_2024_08_13[] = firstManagedUserBookingsResponseBody.data;
expect(firstManagedUserBookings.length).toEqual(firstManagedUserBookingsCount);
const getSecondManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${secondManagedUser.accessToken}`)
.expect(200);
const secondManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 =
getSecondManagedUserBookings.body;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const secondManagedUserBookings: BookingOutput_2024_08_13[] =
secondManagedUserBookingsResponseBody.data;
expect(secondManagedUserBookings.length).toEqual(secondManagedUserBookingsCount);
const getThirdManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${thirdManagedUser.accessToken}`)
.expect(200);
const thirdManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 =
getThirdManagedUserBookings.body;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const thirdManagedUserBookings: BookingOutput_2024_08_13[] = thirdManagedUserBookingsResponseBody.data;
expect(thirdManagedUserBookings.length).toEqual(thirdManagedUserBookingsCount);
});
});
describe("bookings using OAuth client emails", () => {
it("managed user should be booked by managed user attendee and booking shows up in both users' bookings", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 15, 0, 0)).toISOString(),
eventTypeId: firstManagedUserEventTypeId,
attendee: {
name: secondManagedUser.user.name!,
email: secondManagedUser.user.email,
timeZone: secondManagedUser.user.timeZone,
language: secondManagedUser.user.locale,
},
location: "https://meet.google.com/abc-def-ghi",
};
await request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201);
firstManagedUserBookingsCount += 1;
secondManagedUserBookingsCount += 1;
const getFirstManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${firstManagedUser.accessToken}`)
.expect(200);
const firstManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 =
getFirstManagedUserBookings.body;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const firstManagedUserBookings: BookingOutput_2024_08_13[] = firstManagedUserBookingsResponseBody.data;
expect(firstManagedUserBookings.length).toEqual(firstManagedUserBookingsCount);
const getSecondManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${secondManagedUser.accessToken}`)
.expect(200);
const secondManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 =
getSecondManagedUserBookings.body;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const secondManagedUserBookings: BookingOutput_2024_08_13[] =
secondManagedUserBookingsResponseBody.data;
expect(secondManagedUserBookings.length).toEqual(secondManagedUserBookingsCount);
});
it("managed user should be booked by managed user attendee and managed user as a guest and booking shows up in all three users' bookings", async () => {
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 15, 30, 0)).toISOString(),
eventTypeId: firstManagedUserEventTypeId,
attendee: {
name: thirdManagedUser.user.name!,
email: thirdManagedUser.user.email,
timeZone: secondManagedUser.user.timeZone,
language: secondManagedUser.user.locale,
},
guests: [secondManagedUser.user.email],
location: "https://meet.google.com/abc-def-ghi",
};
await request(app.getHttpServer())
.post("/v2/bookings")
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201);
firstManagedUserBookingsCount += 1;
secondManagedUserBookingsCount += 1;
thirdManagedUserBookingsCount += 1;
const getFirstManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${firstManagedUser.accessToken}`)
.expect(200);
const firstManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 =
getFirstManagedUserBookings.body;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const firstManagedUserBookings: BookingOutput_2024_08_13[] = firstManagedUserBookingsResponseBody.data;
expect(firstManagedUserBookings.length).toEqual(firstManagedUserBookingsCount);
const getSecondManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${secondManagedUser.accessToken}`)
.expect(200);
const secondManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 =
getSecondManagedUserBookings.body;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const secondManagedUserBookings: BookingOutput_2024_08_13[] =
secondManagedUserBookingsResponseBody.data;
expect(secondManagedUserBookings.length).toEqual(secondManagedUserBookingsCount);
const getThirdManagedUserBookings = await request(app.getHttpServer())
.get("/v2/bookings")
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.set("Authorization", `Bearer ${thirdManagedUser.accessToken}`)
.expect(200);
const thirdManagedUserBookingsResponseBody: GetBookingsOutput_2024_08_13 =
getThirdManagedUserBookings.body;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const thirdManagedUserBookings: BookingOutput_2024_08_13[] = thirdManagedUserBookingsResponseBody.data;
expect(thirdManagedUserBookings.length).toEqual(thirdManagedUserBookingsCount);
});
});
afterAll(async () => {
await userRepositoryFixture.delete(firstManagedUser.user.id);
await userRepositoryFixture.delete(secondManagedUser.user.id);
await userRepositoryFixture.delete(thirdManagedUser.user.id);
await userRepositoryFixture.delete(platformAdmin.id);
await oauthClientRepositoryFixture.delete(oAuthClient.id);
await teamRepositoryFixture.delete(organization.id);
await app.close();
});
});
@@ -21,7 +21,7 @@ import { randomString } from "test/utils/randomString";
import { withApiAuth } from "test/utils/withApiAuth";
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
import { AttendeeCancelledEmail, OrganizerCancelledEmail } from "@calcom/platform-libraries";
import { AttendeeCancelledEmail, OrganizerCancelledEmail } from "@calcom/platform-libraries/emails";
import {
CreateRecurringBookingInput_2024_08_13,
RecurringBookingOutput_2024_08_13,
@@ -684,7 +684,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should cancel all remaning recurrences", async () => {
it("should cancel all remaining recurrences", async () => {
const recurringBookingUid = recurringBooking[0].recurringBookingUid;
return request(app.getHttpServer())
@@ -40,6 +40,8 @@ import {
import { PlatformOAuthClient, Team } from "@calcom/prisma/client";
describe("Bookings Endpoints 2024-08-13", () => {
const googleMeetUrl = "https://meet.google.com/abc-def-ghi";
describe("Seated bookings", () => {
let app: INestApplication;
let organization: Team;
@@ -330,7 +332,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
timeZone: "Europe/Rome",
language: "it",
},
location: "https://meet.google.com/abc-def-ghi",
location: googleMeetUrl,
bookingFieldsResponses: {
codingLanguage: "TypeScript",
},
@@ -383,6 +385,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
bookingFieldsResponses: {
name: body.attendee.name,
...body.bookingFieldsResponses,
location: {
optionValue: googleMeetUrl,
value: "link",
},
},
metadata: body.metadata,
});
@@ -418,6 +424,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
bookingFieldsResponses: {
name: body.attendee.name,
...body.bookingFieldsResponses,
location: {
optionValue: googleMeetUrl,
value: "link",
},
},
metadata: body.metadata,
});
@@ -797,7 +807,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
timeZone: "Europe/Rome",
language: "it",
},
location: "https://meet.google.com/abc-def-ghi",
location: googleMeetUrl,
bookingFieldsResponses: {
codingLanguage: "TypeScript",
},
@@ -850,6 +860,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
bookingFieldsResponses: {
name: body.attendee.name,
...body.bookingFieldsResponses,
location: {
optionValue: googleMeetUrl,
value: "link",
},
},
metadata: body.metadata,
});
@@ -885,6 +899,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
bookingFieldsResponses: {
name: body.attendee.name,
...body.bookingFieldsResponses,
location: {
optionValue: googleMeetUrl,
value: "link",
},
},
metadata: body.metadata,
});
@@ -910,7 +928,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
timeZone: "Europe/Rome",
language: "it",
},
location: "https://meet.google.com/abc-def-ghi",
location: googleMeetUrl,
bookingFieldsResponses: {
codingLanguage: "TypeScript",
},
@@ -966,6 +984,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
bookingFieldsResponses: {
name: nameAttendeeOne,
...body.bookingFieldsResponses,
location: {
optionValue: googleMeetUrl,
value: "link",
},
},
metadata: createdRecurringSeatedBooking[0].attendees[0].metadata,
});
@@ -982,6 +1004,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
bookingFieldsResponses: {
name: nameAttendeeTwo,
...body.bookingFieldsResponses,
location: {
optionValue: googleMeetUrl,
value: "link",
},
},
metadata: body.metadata,
});
@@ -1017,6 +1043,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
bookingFieldsResponses: {
name: nameAttendeeOne,
...body.bookingFieldsResponses,
location: {
optionValue: googleMeetUrl,
value: "link",
},
},
metadata: createdRecurringSeatedBooking[0].attendees[0].metadata,
});
@@ -1030,6 +1060,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
bookingFieldsResponses: {
name: nameAttendeeTwo,
...body.bookingFieldsResponses,
location: {
optionValue: googleMeetUrl,
value: "link",
},
},
metadata: body.metadata,
});
@@ -1,6 +1,7 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
import { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reschedule-booking.output";
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
@@ -31,6 +32,7 @@ import {
RecurringBookingOutput_2024_08_13,
GetBookingsOutput_2024_08_13,
GetSeatedBookingOutput_2024_08_13,
RescheduleBookingInput_2024_08_13,
} from "@calcom/platform-types";
import { PlatformOAuthClient, Team } from "@calcom/prisma/client";
@@ -66,6 +68,8 @@ describe("Bookings Endpoints 2024-08-13", () => {
const team2EventTypeSlug = `team-bookings-event-type-${randomString()}`;
const phoneOnlyEventTypeSlug = `team-bookings-event-type-${randomString()}`;
let phoneBasedBooking: BookingOutput_2024_08_13;
beforeAll(async () => {
const moduleRef = await withApiAuth(
teamUserEmail,
@@ -401,19 +405,20 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.absentHost).toEqual(false);
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
});
it("should create a phone based booking", async () => {
const phoneNumber = "+919876543210";
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 15, 0, 0)).toISOString(),
eventTypeId: phoneOnlyEventTypeId,
attendee: {
name: "alice",
phoneNumber: "+919876543210",
phoneNumber,
timeZone: "Europe/Madrid",
language: "es",
},
@@ -453,9 +458,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
expect(data.meetingUrl).toEqual(body.meetingUrl);
expect(data.absentHost).toEqual(false);
expect(data.bookingFieldsResponses.attendeePhoneNumber).toEqual(phoneNumber);
phoneBasedBooking = data;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
@@ -515,7 +522,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.absentHost).toEqual(false);
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
@@ -523,7 +530,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
describe("get team bookings", () => {
it("should should get bookings by teamId", async () => {
it("should get bookings by teamId", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?teamId=${team1.id}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -542,7 +549,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should should get bookings by teamId", async () => {
it("should get bookings by teamId", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?teamId=${team2.id}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -598,7 +605,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should should get bookings by teamIds", async () => {
it("should get bookings by teamIds", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?teamIds=${team1.id},${team2.id}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -619,6 +626,59 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
describe("reschedule", () => {
it("should reschedule phone based booking", async () => {
const body: RescheduleBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2035, 0, 8, 14, 0, 0)).toISOString(),
reschedulingReason: "Flying to mars that day",
};
return request(app.getHttpServer())
.post(`/v2/bookings/${phoneBasedBooking.uid}/reschedule`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201)
.then(async (response) => {
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseDataIsBooking(responseBody.data)).toBe(true);
if (responseDataIsBooking(responseBody.data)) {
const data: BookingOutput_2024_08_13 = responseBody.data;
expect(data.id).toBeDefined();
expect(data.uid).toBeDefined();
expect(data.hosts.length).toEqual(1);
expect(data.hosts[0].id).toEqual(teamUser.id);
expect(data.status).toEqual("accepted");
expect(data.start).toEqual(body.start);
expect(data.end).toEqual(new Date(Date.UTC(2035, 0, 8, 14, 15, 0)).toISOString());
expect(data.duration).toEqual(15);
expect(data.eventTypeId).toEqual(phoneOnlyEventTypeId);
expect(data.attendees.length).toEqual(1);
expect(data.attendees[0]).toEqual({
name: phoneBasedBooking.attendees[0].name,
email: phoneBasedBooking.attendees[0].email,
phoneNumber: phoneBasedBooking.attendees[0].phoneNumber,
timeZone: phoneBasedBooking.attendees[0].timeZone,
language: phoneBasedBooking.attendees[0].language,
absent: false,
});
expect(data.meetingUrl).toEqual(phoneBasedBooking.meetingUrl);
expect(data.absentHost).toEqual(false);
expect(data.bookingFieldsResponses.attendeePhoneNumber).toEqual(
phoneBasedBooking.bookingFieldsResponses.attendeePhoneNumber
);
phoneBasedBooking = data;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
});
});
async function createOAuthClient(organizationId: number) {
const data = {
logo: "logo-url",
@@ -4,6 +4,7 @@ import { CancelBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
import { MarkAbsentBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/mark-absent.output";
import { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reschedule-booking.output";
import { CreateEventTypeOutput_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/outputs/create-event-type.output";
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
@@ -27,10 +28,12 @@ import { withApiAuth } from "test/utils/withApiAuth";
import {
CAL_API_VERSION_HEADER,
SUCCESS_STATUS,
VERSION_2024_06_14,
VERSION_2024_08_13,
X_CAL_CLIENT_ID,
} from "@calcom/platform-constants";
import {
CreateEventTypeInput_2024_06_14,
GetBookingOutput_2024_08_13,
GetBookingsOutput_2024_08_13,
GetSeatedBookingOutput_2024_08_13,
@@ -95,6 +98,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
organization = await teamRepositoryFixture.create({
name: `user-bookings-organization-${randomString()}`,
isOrganization: true,
});
oAuthClient = await createOAuthClient(organization.id);
@@ -146,7 +150,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
startTime: new Date(Date.UTC(2020, 0, 8, 13, 0, 0)),
endTime: new Date(Date.UTC(2020, 0, 8, 14, 0, 0)),
title: "peer coding lets goo",
uid: "booking-in-the-past",
uid: `booking-in-the-past-${randomString()}`,
eventType: {
connect: {
id: eventTypeId,
@@ -326,6 +330,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
it("should create a booking", async () => {
const googleMeetUrl = "https://meet.google.com/abc-def-ghi";
const body: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
eventTypeId,
@@ -335,7 +340,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
timeZone: "Europe/Rome",
language: "it",
},
location: "https://meet.google.com/abc-def-ghi",
location: googleMeetUrl,
bookingFieldsResponses: {
customField: "customValue",
},
@@ -389,6 +394,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
email: body.attendee.email,
...body.bookingFieldsResponses,
guests: body.guests,
location: {
optionValue: googleMeetUrl,
value: "link",
},
});
expect(data.guests).toEqual(body.guests);
@@ -407,7 +416,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
createdBooking = data;
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
@@ -518,7 +527,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
describe("get individual booking", () => {
it("should should get a booking", async () => {
it("should get a booking", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings/${createdBooking.uid}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -547,13 +556,13 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.createdAt).toEqual(createdBooking.createdAt);
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
});
it("should should get a booking with rating", async () => {
it("should get a booking with rating", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings/${bookingInThePast.uid}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -577,7 +586,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should should get 1 recurrence of a recurring booking", async () => {
it("should get 1 recurrence of a recurring booking", async () => {
const recurrenceUid = createdRecurringBooking[0].uid;
return request(app.getHttpServer())
.get(`/v2/bookings/${recurrenceUid}`)
@@ -607,13 +616,13 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.absentHost).toEqual(createdRecurringBooking[0].absentHost);
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
});
it("should should get all recurrences of the recurring bookings", async () => {
it("should get all recurrences of the recurring bookings", async () => {
const recurringBookingUid = createdRecurringBooking[0].recurringBookingUid;
return request(app.getHttpServer())
.get(`/v2/bookings/${recurringBookingUid}`)
@@ -682,7 +691,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
describe("get bookings", () => {
it("should should get all bookings", async () => {
it("should get all bookings", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -700,7 +709,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should should take bookings", async () => {
it("should take bookings", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?take=3`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -718,7 +727,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should should skip bookings", async () => {
it("should skip bookings", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?skip=2`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -736,7 +745,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should should get upcoming bookings", async () => {
it("should get upcoming bookings", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?status=upcoming`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -754,7 +763,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should should get past bookings", async () => {
it("should get past bookings", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?status=past`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -772,7 +781,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should should get upcoming and past bookings", async () => {
it("should get upcoming and past bookings", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?status=upcoming,past`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -790,7 +799,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should should get recurring booking recurrences", async () => {
it("should get recurring booking recurrences", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?status=recurring`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -808,7 +817,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should should get bookings by attendee email", async () => {
it("should get bookings by attendee email", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?attendeeEmail=mr_proper@gmail.com`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -826,7 +835,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should should get bookings by attendee name", async () => {
it("should get bookings by attendee name", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?attendeeName=Mr Proper Recurring`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -844,7 +853,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should should get bookings by eventTypeId", async () => {
it("should get bookings by eventTypeId", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?eventTypeId=${eventTypeId}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -862,7 +871,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should should get bookings by eventTypeIds", async () => {
it("should get bookings by eventTypeIds", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?eventTypeIds=${eventTypeId},${recurringEventTypeId}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -880,7 +889,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should should get bookings by after specified start time", async () => {
it("should get bookings by after specified start time", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?afterStart=${createdRecurringBooking[1].start}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -898,7 +907,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should should get bookings by before specified end time", async () => {
it("should get bookings by before specified end time", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?beforeEnd=${createdRecurringBooking[0].end}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -916,7 +925,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should should get bookings after specified update time", async () => {
it("should get bookings after specified update time", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?afterUpdatedAt=${createdRecurringBooking[1].updatedAt}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -934,7 +943,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should should get bookings before specified update time", async () => {
it("should get bookings before specified update time", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?beforeUpdatedAt=${createdRecurringBooking[0].updatedAt}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -952,7 +961,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should should get bookings in update range", async () => {
it("should get bookings in update range", async () => {
return request(app.getHttpServer())
.get(
`/v2/bookings?afterUpdatedAt=${createdRecurringBooking[0].updatedAt}&beforeUpdatedAt=${createdRecurringBooking[2].updatedAt}`
@@ -972,7 +981,65 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should should sort bookings by start in descending order", async () => {
describe("createdAt filter", () => {
it("should get bookings after specified createdAt time", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?afterCreatedAt=${createdRecurringBooking[1].createdAt}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
const data: (
| BookingOutput_2024_08_13
| RecurringBookingOutput_2024_08_13
| GetSeatedBookingOutput_2024_08_13
)[] = responseBody.data;
expect(data.length).toEqual(2);
});
});
it("should get bookings before specified createdAt time", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?beforeCreatedAt=${createdRecurringBooking[0].createdAt}`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
const data: (
| BookingOutput_2024_08_13
| RecurringBookingOutput_2024_08_13
| GetSeatedBookingOutput_2024_08_13
)[] = responseBody.data;
expect(data.length).toEqual(3);
});
});
it("should get bookings in createdAt range", async () => {
return request(app.getHttpServer())
.get(
`/v2/bookings?afterCreatedAt=${createdRecurringBooking[0].createdAt}&beforeCreatedAt=${createdRecurringBooking[1].createdAt}`
)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(200)
.then(async (response) => {
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
const data: (
| BookingOutput_2024_08_13
| RecurringBookingOutput_2024_08_13
| GetSeatedBookingOutput_2024_08_13
)[] = responseBody.data;
expect(data.length).toEqual(2);
});
});
});
it("should sort bookings by start in descending order", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?eventTypeId=${eventTypeId}&sortStart=desc`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -992,7 +1059,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should should sort bookings by start in ascending order", async () => {
it("should sort bookings by start in ascending order", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?eventTypeId=${eventTypeId}&sortStart=asc`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -1012,7 +1079,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should should sort bookings by end in descending order", async () => {
it("should sort bookings by end in descending order", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?eventTypeId=${eventTypeId}&sortEnd=desc`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -1032,7 +1099,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should should sort bookings by end in ascending order", async () => {
it("should sort bookings by end in ascending order", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?eventTypeId=${eventTypeId}&sortEnd=asc`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -1052,7 +1119,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should should sort bookings by created in descending order", async () => {
it("should sort bookings by created in descending order", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?eventTypeId=${eventTypeId}&sortCreated=desc`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -1072,7 +1139,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should should sort bookings by created in ascending order", async () => {
it("should sort bookings by created in ascending order", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?eventTypeId=${eventTypeId}&sortCreated=asc`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -1092,7 +1159,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should should sort bookings by updated in descending order", async () => {
it("should sort bookings by updated in descending order", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?eventTypeId=${eventTypeId}&sortUpdatedAt=desc`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -1112,7 +1179,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
});
it("should should sort bookings by updated in ascending order", async () => {
it("should sort bookings by updated in ascending order", async () => {
return request(app.getHttpServer())
.get(`/v2/bookings?eventTypeId=${eventTypeId}&sortUpdatedAt=asc`)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
@@ -1134,7 +1201,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
});
describe("reschedule bookings", () => {
it("should should reschedule normal booking", async () => {
it("should reschedule normal booking", async () => {
const body: RescheduleBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2035, 0, 8, 14, 0, 0)).toISOString(),
reschedulingReason: "Flying to mars that day",
@@ -1480,7 +1547,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.cancelledByEmail).toEqual(cancelledByEmail);
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
@@ -1530,13 +1597,447 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.rescheduledByEmail).toEqual(rescheduledByEmail);
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
});
});
describe("booking location", () => {
const address = "123 Main St";
const link = "https://cal.com/join/123456";
const phone = "+37121999999";
let eventTypeWithAllLocationsId: number;
it("can create event type with all locations except google meet", async () => {
const eventTypeBody: CreateEventTypeInput_2024_06_14 = {
title: "book using any location",
slug: "book-using-any-location",
lengthInMinutes: 15,
locations: [
{
type: "integration",
integration: "cal-video",
},
{
type: "address",
address,
public: true,
},
{
type: "link",
link,
public: true,
},
{
type: "phone",
phone,
public: true,
},
{
type: "attendeeAddress",
},
{
type: "attendeePhone",
},
{
type: "attendeeDefined",
},
],
};
const eventTypeResponse = await request(app.getHttpServer())
.post("/api/v2/event-types")
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.send(eventTypeBody)
.expect(201);
const eventTypeResponseBody: CreateEventTypeOutput_2024_06_14 = eventTypeResponse.body;
const createdEventType = eventTypeResponseBody.data;
expect(createdEventType).toHaveProperty("id");
expect(createdEventType.locations).toHaveLength(7);
expect(createdEventType.locations).toEqual(createdEventType.locations);
eventTypeWithAllLocationsId = createdEventType.id;
});
it("can book with cal video location", async () => {
const bookingBody: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2040, 0, 9, 13, 0, 0)).toISOString(),
eventTypeId: eventTypeWithAllLocationsId,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: {
type: "integration",
integration: "cal-video",
},
};
const bookingResponse = await request(app.getHttpServer())
.post("/v2/bookings")
.send(bookingBody)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201);
const bookingResponseBody: CreateBookingOutput_2024_08_13 = bookingResponse.body;
const createdBooking = bookingResponseBody.data;
expect(createdBooking).toHaveProperty("id");
if (responseDataIsBooking(createdBooking)) {
expect(
createdBooking.location?.startsWith("http") || createdBooking.location === "integrations:daily"
).toEqual(true);
await bookingsRepositoryFixture.deleteById(createdBooking.id);
} else {
throw new Error("Unexpected response data type");
}
});
it("can book with address location", async () => {
const bookingBody: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2040, 0, 9, 13, 0, 0)).toISOString(),
eventTypeId: eventTypeWithAllLocationsId,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: {
type: "address",
},
};
const bookingResponse = await request(app.getHttpServer())
.post("/v2/bookings")
.send(bookingBody)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201);
const bookingResponseBody: CreateBookingOutput_2024_08_13 = bookingResponse.body;
const createdBooking = bookingResponseBody.data;
expect(createdBooking).toHaveProperty("id");
if (responseDataIsBooking(createdBooking)) {
expect(createdBooking.location).toEqual(address);
await bookingsRepositoryFixture.deleteById(createdBooking.id);
} else {
throw new Error("Unexpected response data type");
}
});
it("can book with link location", async () => {
const bookingBody: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2040, 0, 9, 13, 0, 0)).toISOString(),
eventTypeId: eventTypeWithAllLocationsId,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: {
type: "link",
},
};
const bookingResponse = await request(app.getHttpServer())
.post("/v2/bookings")
.send(bookingBody)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201);
const bookingResponseBody: CreateBookingOutput_2024_08_13 = bookingResponse.body;
const createdBooking = bookingResponseBody.data;
expect(createdBooking).toHaveProperty("id");
if (responseDataIsBooking(createdBooking)) {
expect(createdBooking.location).toEqual(link);
await bookingsRepositoryFixture.deleteById(createdBooking.id);
} else {
throw new Error("Unexpected response data type");
}
});
it("can book with link location", async () => {
const bookingBody: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2040, 0, 9, 13, 0, 0)).toISOString(),
eventTypeId: eventTypeWithAllLocationsId,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: {
type: "phone",
},
};
const bookingResponse = await request(app.getHttpServer())
.post("/v2/bookings")
.send(bookingBody)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201);
const bookingResponseBody: CreateBookingOutput_2024_08_13 = bookingResponse.body;
const createdBooking = bookingResponseBody.data;
expect(createdBooking).toHaveProperty("id");
if (responseDataIsBooking(createdBooking)) {
expect(createdBooking.location).toEqual(phone);
await bookingsRepositoryFixture.deleteById(createdBooking.id);
} else {
throw new Error("Unexpected response data type");
}
});
it("can book with attendeeAddress location", async () => {
const attendeeAddress = "123 Example St, City, Valhalla";
const bookingBody: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2040, 0, 9, 13, 0, 0)).toISOString(),
eventTypeId: eventTypeWithAllLocationsId,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: {
type: "attendeeAddress",
address: attendeeAddress,
},
};
const bookingResponse = await request(app.getHttpServer())
.post("/v2/bookings")
.send(bookingBody)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201);
const bookingResponseBody: CreateBookingOutput_2024_08_13 = bookingResponse.body;
const createdBooking = bookingResponseBody.data;
expect(createdBooking).toHaveProperty("id");
if (responseDataIsBooking(createdBooking)) {
expect(createdBooking.location).toEqual(attendeeAddress);
await bookingsRepositoryFixture.deleteById(createdBooking.id);
} else {
throw new Error("Unexpected response data type");
}
});
xit("can book with attendeeAddress location", async () => {
const attendeePhone = "+37120993151";
const bookingBody: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2040, 0, 9, 13, 0, 0)).toISOString(),
eventTypeId: eventTypeWithAllLocationsId,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: {
type: "attendeePhone",
phone: attendeePhone,
},
};
const bookingResponse = await request(app.getHttpServer())
.post("/v2/bookings")
.send(bookingBody)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201);
const bookingResponseBody: CreateBookingOutput_2024_08_13 = bookingResponse.body;
const createdBooking = bookingResponseBody.data;
expect(createdBooking).toHaveProperty("id");
if (responseDataIsBooking(createdBooking)) {
expect(createdBooking.location).toEqual(attendeePhone);
await bookingsRepositoryFixture.deleteById(createdBooking.id);
} else {
throw new Error("Unexpected response data type");
}
});
it("can book with attendeeDefined location", async () => {
const attendeeDefinedLocation = "namek 100";
const bookingBody: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2040, 0, 9, 13, 0, 0)).toISOString(),
eventTypeId: eventTypeWithAllLocationsId,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: {
type: "attendeeDefined",
location: attendeeDefinedLocation,
},
};
const bookingResponse = await request(app.getHttpServer())
.post("/v2/bookings")
.send(bookingBody)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201);
const bookingResponseBody: CreateBookingOutput_2024_08_13 = bookingResponse.body;
const createdBooking = bookingResponseBody.data;
expect(createdBooking).toHaveProperty("id");
if (responseDataIsBooking(createdBooking)) {
expect(createdBooking.location).toEqual(attendeeDefinedLocation);
await bookingsRepositoryFixture.deleteById(createdBooking.id);
} else {
throw new Error("Unexpected response data type");
}
});
it("can't book with not location that is not in event type", async () => {
const bookingBody: CreateBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2040, 0, 9, 13, 0, 0)).toISOString(),
eventTypeId: eventTypeWithAllLocationsId,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: {
type: "integration",
integration: "google-meet",
},
};
await request(app.getHttpServer())
.post("/v2/bookings")
.send(bookingBody)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(400);
});
it("can't book with invalid location type", async () => {
const bookingBody = {
start: new Date(Date.UTC(2040, 0, 9, 13, 0, 0)).toISOString(),
eventTypeId: eventTypeWithAllLocationsId,
attendee: {
name: "Mr Proper",
email: "mr_proper@gmail.com",
timeZone: "Europe/Rome",
language: "it",
},
location: {
type: "blablabala",
},
};
await request(app.getHttpServer())
.post("/v2/bookings")
.send(bookingBody)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(400);
});
});
describe("rescheduling booking with invalid booking responses", () => {
it("should reschedule", async () => {
const email = `user-bookings-attendee-${randomString(10)}@gmail.com`;
const username = `user-bookings-attendee-${randomString(10)}`;
const locale = "it";
const timeZone = "Europe/Rome";
const attendee = await userRepositoryFixture.create({
email,
username,
locale,
timeZone,
});
const booking = await bookingsRepositoryFixture.create({
uid: `booking-uid-${randomString(10)}`,
title: "booking title",
startTime: "2060-09-05T11:00:00.000Z",
endTime: "2060-09-05T12:00:00.000Z",
eventType: {
connect: {
id: eventTypeId,
},
},
status: "ACCEPTED",
metadata: {},
responses: "null",
user: {
connect: {
id: user.id,
},
},
attendees: {
create: {
email: email,
name: username,
timeZone,
locale,
},
},
});
const body: RescheduleBookingInput_2024_08_13 = {
start: new Date(Date.UTC(2060, 0, 9, 14, 0, 0)).toISOString(),
reschedulingReason: "Flying to mars that day",
};
return request(app.getHttpServer())
.post(`/v2/bookings/${booking.uid}/reschedule`)
.send(body)
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
.expect(201)
.then(async (response) => {
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const data: BookingOutput_2024_08_13 = responseBody.data;
expect(data.reschedulingReason).toEqual(body.reschedulingReason);
expect(data.start).toEqual(body.start);
expect(data.end).toEqual(new Date(Date.UTC(2060, 0, 9, 15, 0, 0)).toISOString());
expect(data.rescheduledFromUid).toEqual(booking.uid);
expect(data.id).toBeDefined();
expect(data.uid).toBeDefined();
expect(data.hosts[0].id).toEqual(user.id);
expect(data.hosts[0].username).toEqual(user.username);
expect(data.hosts[0].email).toEqual(user.email);
expect(data.status).toEqual("accepted");
expect(data.eventTypeId).toEqual(booking.eventTypeId);
expect(data.attendees.length).toEqual(1);
expect(data.attendees[0]).toEqual({
name: attendee.username,
email: attendee.email,
timeZone: attendee.timeZone,
language: attendee.locale,
absent: false,
});
expect(data.bookingFieldsResponses).toEqual({
name: attendee.username,
email: attendee.email,
rescheduledReason: body.reschedulingReason,
guests: [],
});
await bookingsRepositoryFixture.deleteById(data.id);
await userRepositoryFixture.delete(attendee.id);
});
});
});
function responseDataIsRecurranceBooking(data: any): data is RecurringBookingOutput_2024_08_13 {
return (
!Array.isArray(data) &&
@@ -194,7 +194,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.duration).toEqual(variableLengthEventType.length);
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
@@ -239,7 +239,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
expect(data.duration).toEqual(lengthInMinutes);
} else {
throw new Error(
"Invalid response data - expected booking but received array of possibily recurring bookings"
"Invalid response data - expected booking but received array of possibly recurring bookings"
);
}
});
@@ -0,0 +1,33 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsEnum, IsString } from "class-validator";
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
export class CalendarLink {
@IsString()
@ApiProperty({
description: "The label of the calendar link",
})
label!: string;
@IsString()
@ApiProperty({
description: "The link to the calendar",
})
link!: string;
}
export class CalendarLinksOutput_2024_08_13 {
@ApiProperty({
description: "The status of the request, always 'success' for successful responses",
example: SUCCESS_STATUS,
})
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
@ApiProperty({
description: "Calendar links for the booking",
type: [CalendarLink],
})
data!: CalendarLink[];
}
@@ -1,6 +1,8 @@
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
import { CalendarLink } from "@/ee/bookings/2024-08-13/outputs/calendar-links.output";
import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service";
import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service";
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
import { BillingService } from "@/modules/billing/services/billing.service";
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
@@ -14,6 +16,7 @@ import { z } from "zod";
import {
handleNewRecurringBooking,
getTranslation,
getAllUserBookings,
handleInstantMeeting,
handleCancelBooking,
@@ -21,6 +24,7 @@ import {
roundRobinManualReassignment,
handleMarkNoShow,
confirmBookingHandler,
getCalendarLinks,
} from "@calcom/platform-libraries";
import { handleNewBooking } from "@calcom/platform-libraries";
import {
@@ -67,7 +71,8 @@ export class BookingsService_2024_08_13 {
private readonly prismaReadService: PrismaReadService,
private readonly billingService: BillingService,
private readonly usersService: UsersService,
private readonly usersRepository: UsersRepository
private readonly usersRepository: UsersRepository,
private readonly platformBookingsService: PlatformBookingsService
) {}
async createBooking(request: Request, body: CreateBookingInput) {
@@ -224,8 +229,7 @@ export class BookingsService_2024_08_13 {
const fetchedBookings: { bookings: { id: number }[] } = await getAllUserBookings({
bookingListingByStatus: queryParams.status || [],
skip: queryParams.skip ?? 0,
// note(Lauris): we substract -1 because getAllUSerBookings child function adds +1 for some reason
take: queryParams.take ? queryParams.take - 1 : 100,
take: queryParams.take ?? 100,
filters: this.inputService.transformGetBookingsFilters(queryParams),
ctx: {
user,
@@ -368,7 +372,7 @@ export class BookingsService_2024_08_13 {
const bodyTransformed = this.inputService.transformInputMarkAbsentBooking(body);
const bookingBefore = await this.bookingsRepository.getByUid(bookingUid);
const platformClientParams = bookingBefore?.eventTypeId
? await this.inputService.getOAuthClientParams(bookingBefore.eventTypeId)
? await this.platformBookingsService.getOAuthClientParams(bookingBefore.eventTypeId)
: undefined;
await handleMarkNoShow({
@@ -432,7 +436,7 @@ export class BookingsService_2024_08_13 {
}
const platformClientParams = booking.eventTypeId
? await this.inputService.getOAuthClientParams(booking.eventTypeId)
? await this.platformBookingsService.getOAuthClientParams(booking.eventTypeId)
: undefined;
const emailsEnabled = platformClientParams ? platformClientParams.arePlatformEmailsEnabled : true;
@@ -471,7 +475,7 @@ export class BookingsService_2024_08_13 {
}
const platformClientParams = booking.eventTypeId
? await this.inputService.getOAuthClientParams(booking.eventTypeId)
? await this.platformBookingsService.getOAuthClientParams(booking.eventTypeId)
: undefined;
const emailsEnabled = platformClientParams ? platformClientParams.arePlatformEmailsEnabled : true;
@@ -498,7 +502,7 @@ export class BookingsService_2024_08_13 {
}
const platformClientParams = booking.eventTypeId
? await this.inputService.getOAuthClientParams(booking.eventTypeId)
? await this.platformBookingsService.getOAuthClientParams(booking.eventTypeId)
: undefined;
const emailsEnabled = platformClientParams ? platformClientParams.arePlatformEmailsEnabled : true;
@@ -526,7 +530,7 @@ export class BookingsService_2024_08_13 {
}
const platformClientParams = booking.eventTypeId
? await this.inputService.getOAuthClientParams(booking.eventTypeId)
? await this.platformBookingsService.getOAuthClientParams(booking.eventTypeId)
: undefined;
const emailsEnabled = platformClientParams ? platformClientParams.arePlatformEmailsEnabled : true;
@@ -547,4 +551,34 @@ export class BookingsService_2024_08_13 {
return this.getBooking(bookingUid);
}
async getCalendarLinks(bookingUid: string): Promise<CalendarLink[]> {
const booking = await this.bookingsRepository.getByUidWithAttendeesAndUserAndEvent(bookingUid);
if (!booking) {
throw new NotFoundException(`Booking with uid ${bookingUid} not found`);
}
if (!booking.eventTypeId) {
throw new BadRequestException(`Booking with uid ${bookingUid} has no event type`);
}
const eventType = await this.eventTypesRepository.getEventTypeByIdIncludeUsersAndTeam(
booking.eventTypeId
);
if (!eventType) {
throw new BadRequestException(`Booking with uid ${bookingUid} has no event type`);
}
// TODO: Maybe we should get locale from query params?
return getCalendarLinks({
booking,
eventType: {
...eventType,
// TODO: Support dynamic event bookings later. It would require a slug input it seems
isDynamic: false,
},
// It can be made customizable through the API endpoint later.
t: await getTranslation("en", "common"),
});
}
}
@@ -3,23 +3,32 @@ import {
bookingResponsesSchema,
seatedBookingDataSchema,
} from "@/ee/bookings/2024-08-13/services/output.service";
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
import { OutputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/output-event-types.service";
import { hashAPIKey, isApiKey, stripApiKey } from "@/lib/api-key";
import { defaultBookingResponses } from "@/lib/safe-parse/default-responses-booking";
import { safeParse } from "@/lib/safe-parse/safe-parse";
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
import { UsersRepository } from "@/modules/users/users.repository";
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
import { Logger } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { CreationSource } from "@prisma/client";
import { isURL, isPhoneNumber } from "class-validator";
import { Request } from "express";
import { DateTime } from "luxon";
import { NextApiRequest } from "next/types";
import { v4 as uuidv4 } from "uuid";
import { z } from "zod";
import { EventTypeMetaDataSchema } from "@calcom/platform-libraries";
import {
EventTypeMetaDataSchema,
apiToInternalintegrationsMapping,
} from "@calcom/platform-libraries/event-types";
import {
CancelBookingInput,
CancelBookingInput_2024_08_13,
@@ -33,7 +42,8 @@ import {
RescheduleBookingInput_2024_08_13,
RescheduleSeatedBookingInput_2024_08_13,
} from "@calcom/platform-types";
import { EventType, PlatformOAuthClient } from "@calcom/prisma/client";
import { BookingInputLocation_2024_08_13 } from "@calcom/platform-types/bookings/2024-08-13/inputs/location.input";
import { EventType } from "@calcom/prisma/client";
type BookingRequest = NextApiRequest & { userId: number | undefined } & OAuthRequestParams;
@@ -70,71 +80,48 @@ export class InputBookingsService_2024_08_13 {
constructor(
private readonly oAuthFlowService: OAuthFlowService,
private readonly oAuthClientRepository: OAuthClientRepository,
private readonly eventTypesRepository: EventTypesRepository_2024_06_14,
private readonly bookingsRepository: BookingsRepository_2024_08_13,
private readonly config: ConfigService,
private readonly apiKeyRepository: ApiKeysRepository,
private readonly bookingSeatRepository: BookingSeatRepository
private readonly bookingSeatRepository: BookingSeatRepository,
private readonly outputEventTypesService: OutputEventTypesService_2024_06_14,
private readonly platformBookingsService: PlatformBookingsService,
private readonly usersRepository: UsersRepository
) {}
async createBookingRequest(
request: Request,
body: CreateBookingInput_2024_08_13 | CreateInstantBookingInput_2024_08_13
): Promise<BookingRequest> {
const bodyTransformed = await this.transformInputCreateBooking(body);
const oAuthClientParams = await this.getOAuthClientParams(body.eventTypeId);
const oAuthClientParams = await this.platformBookingsService.getOAuthClientParams(body.eventTypeId);
const bodyTransformed = await this.transformInputCreateBooking(body, oAuthClientParams?.platformClientId);
const newRequest = { ...request };
const userId = (await this.createBookingRequestOwnerId(request)) ?? undefined;
const location = request.body.location || request.body.meetingUrl;
this.logger.log(`createBookingRequest_2024_04_15`, {
this.logger.log(`createBookingRequest_2024_08_13`, {
requestId: request.get("X-Request-Id"),
ownerId: userId,
location,
oAuthClientParams,
});
if (oAuthClientParams) {
Object.assign(newRequest, { userId, ...oAuthClientParams, platformBookingLocation: location });
Object.assign(newRequest, { userId, ...oAuthClientParams });
newRequest.body = {
...bodyTransformed,
noEmail: !oAuthClientParams.arePlatformEmailsEnabled,
creationSource: CreationSource.API_V2,
};
} else {
Object.assign(newRequest, { userId, platformBookingLocation: location });
Object.assign(newRequest, { userId });
newRequest.body = { ...bodyTransformed, noEmail: false, creationSource: CreationSource.API_V2 };
}
return newRequest as unknown as BookingRequest;
}
async getOAuthClientParams(eventTypeId: number) {
const eventType = await this.eventTypesRepository.getEventTypeById(eventTypeId);
let oAuthClient: PlatformOAuthClient | null = null;
if (eventType?.userId) {
oAuthClient = await this.oAuthClientRepository.getByUserId(eventType.userId);
} else if (eventType?.teamId) {
oAuthClient = await this.oAuthClientRepository.getByTeamId(eventType.teamId);
}
if (oAuthClient) {
return {
platformClientId: oAuthClient.id,
platformCancelUrl: oAuthClient.bookingCancelRedirectUri,
platformRescheduleUrl: oAuthClient.bookingRescheduleRedirectUri,
platformBookingUrl: oAuthClient.bookingRedirectUri,
arePlatformEmailsEnabled: oAuthClient.areEmailsEnabled,
};
}
return undefined;
}
async transformInputCreateBooking(inputBooking: CreateBookingInput_2024_08_13) {
async transformInputCreateBooking(inputBooking: CreateBookingInput_2024_08_13, platformClientId?: string) {
const eventType = await this.eventTypesRepository.getEventTypeByIdWithOwnerAndTeam(
inputBooking.eventTypeId
);
@@ -151,7 +138,21 @@ export class InputBookingsService_2024_08_13 {
);
const endTime = startTime.plus({ minutes: lengthInMinutes });
const guests = inputBooking.guests;
const guests =
inputBooking.guests && platformClientId
? await this.platformBookingsService.getPlatformAttendeesEmails(inputBooking.guests, platformClientId)
: inputBooking.guests;
const attendeeEmail =
inputBooking.attendee.email && platformClientId
? await this.platformBookingsService.getPlatformAttendeeEmail(
inputBooking.attendee.email,
platformClientId
)
: inputBooking.attendee.email;
const inputLocation = inputBooking.location || inputBooking.meetingUrl;
this.isBookingLocationWithEventTypeLocations(inputLocation, eventType);
const location = inputLocation ? this.transformLocation(inputLocation) : undefined;
return {
start: startTime.toISO(),
@@ -163,20 +164,14 @@ export class InputBookingsService_2024_08_13 {
hasHashedBookingLink: false,
guests,
// note(Lauris): responses with name and email are required by the handleNewBooking
responses: inputBooking.bookingFieldsResponses
? {
...inputBooking.bookingFieldsResponses,
name: inputBooking.attendee.name,
email: inputBooking.attendee.email ?? "",
attendeePhoneNumber: inputBooking.attendee.phoneNumber,
guests,
}
: {
name: inputBooking.attendee.name,
email: inputBooking.attendee.email ?? "",
attendeePhoneNumber: inputBooking.attendee.phoneNumber,
guests,
},
responses: {
...(inputBooking.bookingFieldsResponses || {}),
name: inputBooking.attendee.name,
email: attendeeEmail ?? "",
attendeePhoneNumber: inputBooking.attendee.phoneNumber,
guests,
location,
},
};
}
@@ -203,24 +198,24 @@ export class InputBookingsService_2024_08_13 {
request: Request,
body: CreateRecurringBookingInput_2024_08_13
): Promise<BookingRequest> {
const oAuthClientParams = await this.platformBookingsService.getOAuthClientParams(body.eventTypeId);
// note(Lauris): update to this.transformInputCreate when rescheduling is implemented
const bodyTransformed = await this.transformInputCreateRecurringBooking(body);
const oAuthClientParams = await this.getOAuthClientParams(body.eventTypeId);
const bodyTransformed = await this.transformInputCreateRecurringBooking(
body,
oAuthClientParams?.platformClientId
);
const newRequest = { ...request };
const userId = (await this.createBookingRequestOwnerId(request)) ?? undefined;
const location = request.body.location || request.body.meetingUrl;
if (oAuthClientParams) {
Object.assign(newRequest, {
userId,
...oAuthClientParams,
platformBookingLocation: location,
noEmail: !oAuthClientParams.arePlatformEmailsEnabled,
});
} else {
Object.assign(newRequest, { userId, platformBookingLocation: location });
Object.assign(newRequest, { userId });
}
newRequest.body = bodyTransformed.map((event) => ({
@@ -231,7 +226,148 @@ export class InputBookingsService_2024_08_13 {
return newRequest as unknown as BookingRequest;
}
async transformInputCreateRecurringBooking(inputBooking: CreateRecurringBookingInput_2024_08_13) {
transformLocation(location: string | BookingInputLocation_2024_08_13): {
value: string;
optionValue: string;
} {
if (typeof location === "string") {
// note(Lauris): this is for backwards compatibility because before switching to booking location objects
// we only received a string. If someone is complaining that their location is not displaying as a URL
// or whatever check that they are not providing a string for bookign location but one of the input objects.
if (isURL(location, { require_protocol: false }) || location.startsWith("www.")) {
return {
value: "link",
optionValue: location,
};
}
if (isPhoneNumber(location)) {
return {
value: "phone",
optionValue: location,
};
}
return {
value: "somewhereElse",
optionValue: location,
};
}
if (location.type === "integration") {
const integration = apiToInternalintegrationsMapping[location.integration];
if (!integration) {
throw new BadRequestException(`Invalid integration: ${location.integration}`);
}
return {
value: integration,
optionValue: "",
};
}
if (location.type === "address") {
return {
value: "inPerson",
optionValue: "",
};
}
if (location.type === "attendeeAddress") {
return {
value: "attendeeInPerson",
optionValue: location.address,
};
}
if (location.type === "link") {
return {
value: "link",
optionValue: "",
};
}
if (location.type === "phone") {
return {
value: "userPhone",
optionValue: "",
};
}
if (location.type === "organizersDefaultApp") {
return {
value: "conferencing",
optionValue: "",
};
}
if (location.type === "attendeePhone") {
return {
value: "phone",
optionValue: location.phone,
};
}
if (location.type === "attendeeDefined") {
return {
value: "somewhereElse",
optionValue: location.location,
};
}
throw new BadRequestException(
`Booking location with type ${(location as BookingInputLocation_2024_08_13).type} not valid.`
);
}
private isBookingLocationWithEventTypeLocations(
inputBookingLocation: undefined | string | BookingInputLocation_2024_08_13,
dbEventType: EventType
) {
if (!inputBookingLocation || typeof inputBookingLocation === "string") {
// note(Lauris): for backwards compatibility because we had string locations before so let them pass.
return true;
}
const eventTypeLocations = this.outputEventTypesService.transformLocations(dbEventType.locations);
const allowedLocationTypes = eventTypeLocations.map((location) => location.type);
const isAllowed = allowedLocationTypes.includes(inputBookingLocation.type);
if (!isAllowed) {
throw new BadRequestException(
`Booking location with type ${inputBookingLocation.type} not valid for event type with id=${
dbEventType.id
}. The event type has following location types: ${allowedLocationTypes.join(
", "
)}, and only these types are allowed for booking location.`
);
}
if (inputBookingLocation.type === "integration" && "integration" in inputBookingLocation) {
const allowedIntegrations = eventTypeLocations
.filter((location) => location.type === "integration")
.map((location) => location.integration);
const isAllowedIntegration = allowedIntegrations.includes(inputBookingLocation.integration);
if (!isAllowedIntegration) {
throw new BadRequestException(
`Booking location with integration ${
inputBookingLocation.integration
} not valid for event type with id=${
dbEventType.id
}. The event type has following integrations: ${allowedIntegrations.join(
", "
)}, and only these integrations are allowed for booking location.`
);
}
}
return true;
}
async transformInputCreateRecurringBooking(
inputBooking: CreateRecurringBookingInput_2024_08_13,
platformClientId?: string
) {
const eventType = await this.eventTypesRepository.getEventTypeByIdWithOwnerAndTeam(
inputBooking.eventTypeId
);
@@ -261,7 +397,21 @@ export class InputBookingsService_2024_08_13 {
inputBooking.attendee.timeZone
);
const guests = inputBooking.guests;
const guests =
inputBooking.guests && platformClientId
? await this.platformBookingsService.getPlatformAttendeesEmails(inputBooking.guests, platformClientId)
: inputBooking.guests;
const attendeeEmail =
inputBooking.attendee.email && platformClientId
? await this.platformBookingsService.getPlatformAttendeeEmail(
inputBooking.attendee.email,
platformClientId
)
: inputBooking.attendee.email;
const inputLocation = inputBooking.location || inputBooking.meetingUrl;
this.isBookingLocationWithEventTypeLocations(inputLocation, eventType);
const location = inputLocation ? this.transformLocation(inputLocation) : undefined;
for (let i = 0; i < repeatsTimes; i++) {
const endTime = startTime.plus({ minutes: eventType.length });
@@ -277,14 +427,14 @@ export class InputBookingsService_2024_08_13 {
hasHashedBookingLink: false,
guests,
// note(Lauris): responses with name and email are required by the handleNewBooking
responses: inputBooking.bookingFieldsResponses
? {
...inputBooking.bookingFieldsResponses,
name: inputBooking.attendee.name,
email: inputBooking.attendee.email,
guests,
}
: { name: inputBooking.attendee.name, email: inputBooking.attendee.email, guests },
responses: {
...(inputBooking.bookingFieldsResponses || {}),
name: inputBooking.attendee.name,
email: attendeeEmail,
attendeePhoneNumber: inputBooking.attendee.phoneNumber,
guests,
location,
},
schedulingType: eventType.schedulingType,
});
@@ -315,10 +465,29 @@ export class InputBookingsService_2024_08_13 {
? await this.transformInputRescheduleSeatedBooking(bookingUid, body)
: await this.transformInputRescheduleBooking(bookingUid, body);
const oAuthClientParams = await this.getOAuthClientParams(bodyTransformed.eventTypeId);
const oAuthClientParams = await this.platformBookingsService.getOAuthClientParams(
bodyTransformed.eventTypeId
);
const newRequest = { ...request };
const userId = (await this.createBookingRequestOwnerId(request)) ?? undefined;
let userId: number | undefined = undefined;
if (
oAuthClientParams &&
request.body.rescheduledBy &&
!request.body.rescheduledBy.includes(oAuthClientParams.platformClientId)
) {
request.body.rescheduledBy = OAuthClientUsersService.getOAuthUserEmail(
oAuthClientParams.platformClientId,
request.body.rescheduledBy
);
}
if (request.body.rescheduledBy) {
if (request.body.rescheduledBy !== bodyTransformed.responses.email) {
userId = (await this.usersRepository.findByEmail(request.body.rescheduledBy))?.id;
}
}
const location = await this.getRescheduleBookingLocation(bookingUid);
if (oAuthClientParams) {
@@ -401,8 +570,19 @@ export class InputBookingsService_2024_08_13 {
throw new NotFoundException(`Event type with id=${booking.eventTypeId} not found`);
}
const bookingResponses = bookingResponsesSchema.parse(booking.responses);
const attendee = booking.attendees.find((attendee) => attendee.email === bookingResponses.email);
const bookingResponses = safeParse(
bookingResponsesSchema,
booking.responses,
defaultBookingResponses,
false
);
const bookingResponsesMissing =
bookingResponses.name === defaultBookingResponses.name &&
bookingResponses.email === defaultBookingResponses.email;
const attendee = bookingResponsesMissing
? booking.attendees[0]
: booking.attendees.find((attendee) => attendee.email === bookingResponses.email);
if (!attendee) {
throw new NotFoundException(
@@ -410,6 +590,12 @@ export class InputBookingsService_2024_08_13 {
);
}
if (bookingResponsesMissing) {
bookingResponses.name = attendee.name;
bookingResponses.email = attendee.email;
bookingResponses.attendeePhoneNumber = attendee.phoneNumber || undefined;
}
const startTime = DateTime.fromISO(inputBooking.start, { zone: "utc" }).setZone(attendee.timeZone);
const endTime = startTime.plus({ minutes: eventType.length });
@@ -446,7 +632,8 @@ export class InputBookingsService_2024_08_13 {
return keyData?.userId;
} else {
// Access Token
return this.oAuthFlowService.getOwnerId(bearerToken);
const ownerId = await this.oAuthFlowService.getOwnerId(bearerToken);
return ownerId;
}
}
} catch (err) {
@@ -465,6 +652,8 @@ export class InputBookingsService_2024_08_13 {
queryParams.eventTypeIds || (queryParams.eventTypeId ? [queryParams.eventTypeId] : undefined),
afterUpdatedDate: queryParams.afterUpdatedAt,
beforeUpdatedDate: queryParams.beforeUpdatedAt,
afterCreatedDate: queryParams.afterCreatedAt,
beforeCreatedDate: queryParams.beforeCreatedAt,
};
}
@@ -501,7 +690,7 @@ export class InputBookingsService_2024_08_13 {
}
const oAuthClientParams = booking.eventTypeId
? await this.getOAuthClientParams(booking.eventTypeId)
? await this.platformBookingsService.getOAuthClientParams(booking.eventTypeId)
: undefined;
const newRequest = { ...request };
@@ -27,6 +27,7 @@ import { Booking, BookingSeat } from "@calcom/prisma/client";
export const bookingResponsesSchema = z
.object({
email: z.string(),
attendeePhoneNumber: z.string().optional(),
name: z.union([
z.string(),
z.object({
@@ -45,6 +46,7 @@ export const seatedBookingDataSchema = z
responses: z
.object({
email: z.string(),
attendeePhoneNumber: z.string().optional(),
name: z.union([
z.string(),
z.object({
@@ -0,0 +1,62 @@
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
import { UsersRepository } from "@/modules/users/users.repository";
import { Injectable } from "@nestjs/common";
import { PlatformOAuthClient } from "@calcom/prisma/client";
@Injectable()
export class PlatformBookingsService {
constructor(
private readonly usersRepository: UsersRepository,
private readonly eventTypesRepository: EventTypesRepository_2024_06_14,
private readonly oAuthClientRepository: OAuthClientRepository
) {}
async getPlatformAttendeesEmails(guestsEmails: string[], platformClientId: string) {
return Promise.all(
guestsEmails.map((guestEmail) => this.getPlatformAttendeeEmail(guestEmail, platformClientId))
);
}
async getPlatformAttendeeEmail(attendeeEmail: string, platformClientId: string) {
if (!attendeeEmail.includes(platformClientId)) {
// note(Lauris): we need to do this when managed user is booking another managed user and the managed user doing the booking entered their email without oAuth client id
// or if one of the guests added are managed users without oAuth client id in their email.
const oAuthUserEmail = OAuthClientUsersService.getOAuthUserEmail(platformClientId, attendeeEmail);
const oAuthUser = await this.usersRepository.findByEmail(oAuthUserEmail);
if (oAuthUser) {
return oAuthUserEmail;
}
}
return attendeeEmail;
}
async getOAuthClientParams(eventTypeId: number) {
const eventType = await this.eventTypesRepository.getEventTypeById(eventTypeId);
let oAuthClient: PlatformOAuthClient | null = null;
if (eventType?.userId) {
oAuthClient = await this.oAuthClientRepository.getByUserId(eventType.userId);
} else if (eventType?.teamId) {
oAuthClient = await this.oAuthClientRepository.getByTeamId(eventType.teamId);
}
// Last resort check the hosts of the event-type
if (!oAuthClient && eventType?.teamId) {
oAuthClient = await this.oAuthClientRepository.getByEventTypeHosts(eventTypeId);
}
if (oAuthClient) {
return {
platformClientId: oAuthClient.id,
platformCancelUrl: oAuthClient.bookingCancelRedirectUri,
platformRescheduleUrl: oAuthClient.bookingRescheduleRedirectUri,
platformBookingUrl: oAuthClient.bookingRedirectUri,
arePlatformEmailsEnabled: oAuthClient.areEmailsEnabled,
};
}
return undefined;
}
}
@@ -31,7 +31,7 @@ import {
} from "@calcom/platform-constants";
import { OFFICE_365_CALENDAR_ID, OFFICE_365_CALENDAR_TYPE } from "@calcom/platform-constants";
import { ICS_CALENDAR } from "@calcom/platform-constants/apps";
import { IcsFeedCalendarService } from "@calcom/platform-libraries";
import { IcsFeedCalendarService } from "@calcom/platform-libraries/app-store";
const CLIENT_REDIRECT_URI = "http://localhost:5555";
@@ -131,7 +131,7 @@ class Primary {
@IsString()
@IsOptional()
@ApiPropertyOptional({ type: String, nullable: true })
domainWideDelegationCredentialId?: string | null;
delegationCredentialId?: string | null;
}
export class Calendar {
@@ -174,7 +174,7 @@ export class Calendar {
@IsString()
@IsOptional()
@ApiPropertyOptional({ type: String, nullable: true })
domainWideDelegationCredentialId?: string | null;
delegationCredentialId?: string | null;
}
export class ConnectedCalendar {
@@ -190,7 +190,7 @@ export class ConnectedCalendar {
@IsString()
@IsOptional()
@ApiPropertyOptional({ type: String, nullable: true })
domainWideDelegationCredentialId?: string | null;
delegationCredentialId?: string | null;
@ValidateNested()
@IsObject()
@@ -208,7 +208,7 @@ export class ConnectedCalendar {
class DestinationCalendar {
@IsInt()
@ApiProperty()
id!: number;
id!: number | string;
@IsString()
@ApiProperty()
@@ -238,7 +238,7 @@ class DestinationCalendar {
@IsString()
@IsOptional()
@ApiPropertyOptional({ type: String, nullable: true })
domainWideDelegationCredentialId?: string | null;
delegationCredentialId?: string | null;
@IsString()
@IsOptional()
@@ -0,0 +1,33 @@
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
import { Process, Processor } from "@nestjs/bull";
import { Logger } from "@nestjs/common";
import { Job } from "bull";
export const DEFAULT_CALENDARS_JOB = "default_calendars_job";
export const CALENDARS_QUEUE = "calendars";
export type DefaultCalendarsJobDataType = {
userId: number;
};
@Processor(CALENDARS_QUEUE)
export class CalendarsProcessor {
private readonly logger = new Logger(CalendarsProcessor.name);
constructor(public readonly calendarsService: CalendarsService) {}
@Process(DEFAULT_CALENDARS_JOB)
async handleEnsureDefaultCalendars(job: Job<DefaultCalendarsJobDataType>) {
const { userId } = job.data;
try {
// getCalendars calls getConnectedDestinationCalendarsAndEnsureDefaultsInDb from platform libraries
// which gets the calendars from third party providers and ensure default calendars are set in DB
await this.calendarsService.getCalendars(userId);
} catch (err) {
this.logger.error(`Failed to load default calendars of user with id: ${userId}`, {
userId,
err,
});
}
return;
}
}
@@ -5,7 +5,8 @@ import { BadRequestException, UnauthorizedException } from "@nestjs/common";
import { Injectable } from "@nestjs/common";
import { SUCCESS_STATUS, APPLE_CALENDAR_TYPE, APPLE_CALENDAR_ID } from "@calcom/platform-constants";
import { symmetricEncrypt, CalendarService, symmetricDecrypt } from "@calcom/platform-libraries";
import { symmetricEncrypt, symmetricDecrypt } from "@calcom/platform-libraries";
import { CalendarService } from "@calcom/platform-libraries/app-store";
import { Credential } from "@calcom/prisma/client";
@Injectable()
@@ -39,7 +39,7 @@ export class CalendarsService {
private readonly selectedCalendarsRepository: SelectedCalendarsRepository
) {}
private buildNonDwdCredentials<TCredential>(credentials: TCredential[]) {
private buildNonDelegationCredentials<TCredential>(credentials: TCredential[]) {
return credentials
.map((credential) => ({
...credential,
@@ -83,11 +83,12 @@ export class CalendarsService {
);
try {
const calendarBusyTimes = await getBusyCalendarTimes(
this.buildNonDwdCredentials(credentials),
this.buildNonDelegationCredentials(credentials),
dateFrom,
dateTo,
composedSelectedCalendars
);
// @ts-expect-error Element implicitly has any type
const calendarBusyTimesConverted = calendarBusyTimes.map((busyTime) => {
const busyTimeStart = DateTime.fromJSDate(new Date(busyTime.start)).setZone(timezone);
const busyTimeEnd = DateTime.fromJSDate(new Date(busyTime.end)).setZone(timezone);
@@ -6,7 +6,8 @@ import { BadRequestException, UnauthorizedException, Logger } from "@nestjs/comm
import { Injectable } from "@nestjs/common";
import { SUCCESS_STATUS, ICS_CALENDAR_TYPE, ICS_CALENDAR } from "@calcom/platform-constants";
import { symmetricEncrypt, IcsFeedCalendarService } from "@calcom/platform-libraries";
import { symmetricEncrypt } from "@calcom/platform-libraries";
import { IcsFeedCalendarService } from "@calcom/platform-libraries/app-store";
@Injectable()
export class IcsFeedService implements ICSFeedCalendarApp {
@@ -33,10 +33,10 @@ import {
} from "@calcom/platform-constants";
import {
EventTypesByViewer,
EventTypesPublic,
eventTypeBookingFields,
eventTypeLocations,
} from "@calcom/platform-libraries";
EventTypesPublic,
} from "@calcom/platform-libraries/event-types";
import { ApiSuccessResponse } from "@calcom/platform-types";
describe("Event types Endpoints", () => {
@@ -5,7 +5,7 @@ import { UsersService } from "@/modules/users/services/users.service";
import { UserWithProfile } from "@/modules/users/users.repository";
import { Injectable } from "@nestjs/common";
import { getEventTypeById } from "@calcom/platform-libraries";
import { getEventTypeById } from "@calcom/platform-libraries/event-types";
import type { PrismaClient } from "@calcom/prisma";
@Injectable()
@@ -1,6 +1,8 @@
import { DEFAULT_EVENT_TYPES } from "@/ee/event-types/event-types_2024_04_15/constants/constants";
import { EventTypesRepository_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.repository";
import { CreateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/create-event-type.input";
import { Editable } from "@/ee/event-types/event-types_2024_04_15/inputs/enums/editable";
import { BaseField } from "@/ee/event-types/event-types_2024_04_15/inputs/enums/field-type";
import { UpdateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/update-event-type.input";
import { EventTypeOutput } from "@/ee/event-types/event-types_2024_04_15/outputs/event-type.output";
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
@@ -16,7 +18,7 @@ import {
EventTypesPublic,
getEventTypesPublic,
systemBeforeFieldEmail,
} from "@calcom/platform-libraries";
} from "@calcom/platform-libraries/event-types";
import { EventType } from "@calcom/prisma/client";
@Injectable()
@@ -68,6 +70,7 @@ export class EventTypesService_2024_04_15 {
organization: { isOrgAdmin },
profile: { id: profileId },
metadata: user.metadata,
email: user.email,
};
}
@@ -134,7 +137,7 @@ export class EventTypesService_2024_04_15 {
!bookingFields.find((field) => field.type === "email") &&
!bookingFields.find((field) => field.type === "phone")
) {
bookingFields.push(systemBeforeFieldEmail);
bookingFields.push({ ...systemBeforeFieldEmail, type: BaseField.email, editable: Editable.system });
}
await updateEventType({
@@ -167,7 +170,15 @@ export class EventTypesService_2024_04_15 {
async getUserToUpdateEvent(user: UserWithProfile) {
const profileId = this.usersService.getUserMainProfile(user)?.id || null;
const selectedCalendars = await this.selectedCalendarsRepository.getUserSelectedCalendars(user.id);
return { ...user, profile: { id: profileId }, selectedCalendars };
const eventTypeSelectedCalendars =
await this.selectedCalendarsRepository.getUserEventTypeSelectedCalendar(user.id);
return {
...user,
locale: user.locale ?? "en",
profile: { id: profileId },
userLevelSelectedCalendars: selectedCalendars,
allSelectedCalendars: [...eventTypeSelectedCalendars, ...selectedCalendars],
};
}
async deleteEventType(eventTypeId: number, userId: number) {
@@ -116,6 +116,7 @@ describe("Event types Endpoints", () => {
slug: "email",
required: true,
disableOnPrefill: false,
hidden: false,
};
const defaultResponseBookingFieldLocation = {
@@ -336,6 +337,46 @@ describe("Event types Endpoints", () => {
.expect(404);
});
it("should not be able to create phone-only event type", async () => {
const body: CreateEventTypeInput_2024_06_14 = {
title: "Phone coding consultation",
slug: "phone-coding-consultation",
description: "Our team will review your codebase.",
lengthInMinutes: 60,
locations: [
{
type: "integration",
integration: "cal-video",
},
],
bookingFields: [
{
type: "email",
required: false,
label: "Email",
hidden: true,
},
{
type: "phone",
slug: "attendeePhoneNumber",
required: true,
label: "Phone number",
hidden: false,
},
],
};
const response = await request(app.getHttpServer())
.post("/api/v2/event-types")
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
.send(body)
.expect(400);
expect(response.body.error.message).toBe(
"checkIsEmailUserAccessible - Email booking field must be required and visible"
);
});
it("should create an event type", async () => {
const nameBookingField: NameDefaultFieldInput_2024_06_14 = {
type: "name",
@@ -1191,7 +1232,14 @@ describe("Event types Endpoints", () => {
const expectedReturnSystemFields = [
{ isDefault: true, required: true, slug: "name", type: "name", disableOnPrefill: false },
{ isDefault: true, required: true, slug: "email", type: "email", disableOnPrefill: false },
{
isDefault: true,
required: true,
slug: "email",
type: "email",
disableOnPrefill: false,
hidden: false,
},
{
isDefault: true,
type: "radioInput",
@@ -1644,6 +1692,7 @@ describe("Event types Endpoints", () => {
slug: "email",
required: true,
disableOnPrefill: false,
hidden: false,
},
{
isDefault: true,
@@ -1845,7 +1894,6 @@ describe("Event types Endpoints", () => {
await teamRepositoryFixture.delete(organization.id);
try {
await eventTypesRepositoryFixture.delete(legacyEventTypeId1);
await eventTypesRepositoryFixture.delete(legacyEventTypeId2);
} catch (e) {
// Event type might have been deleted by the test
}
@@ -29,7 +29,12 @@ import {
} from "@nestjs/common";
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
import { EVENT_TYPE_READ, EVENT_TYPE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
import {
EVENT_TYPE_READ,
EVENT_TYPE_WRITE,
SUCCESS_STATUS,
VERSION_2024_06_14,
} from "@calcom/platform-constants";
import {
UpdateEventTypeInput_2024_06_14,
GetEventTypesQuery_2024_06_14,
@@ -45,8 +50,12 @@ import {
@DocsTags("Event Types")
@ApiHeader({
name: "cal-api-version",
description: `Must be set to \`2024-06-14\``,
description: `Must be set to ${VERSION_2024_06_14}`,
example: VERSION_2024_06_14,
required: true,
schema: {
default: VERSION_2024_06_14,
},
})
export class EventTypesController_2024_06_14 {
constructor(
@@ -1,8 +1,7 @@
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { UsersService } from "@/modules/users/services/users.service";
import { Injectable } from "@nestjs/common";
import type { Prisma } from "@prisma/client";
import { InputEventTransformed_2024_06_14 } from "@calcom/platform-types";
@Injectable()
@@ -76,6 +75,22 @@ export class EventTypesRepository_2024_06_14 {
});
}
async getEventTypeByIdIncludeUsersAndTeam(eventTypeId: number) {
const eventType = await this.dbRead.prisma.eventType.findUnique({
where: { id: eventTypeId },
include: { users: true, team: true },
});
if (!eventType) {
return null;
}
return {
...eventType,
recurringEvent: eventType.recurringEvent as Prisma.JsonObject | null | undefined,
};
}
async getEventTypeByIdWithOwnerAndTeam(eventTypeId: number) {
return this.dbRead.prisma.eventType.findUnique({
where: { id: eventTypeId },
@@ -8,9 +8,15 @@ import { UsersService } from "@/modules/users/services/users.service";
import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository";
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
import { createEventType, updateEventType } from "@calcom/platform-libraries";
import { getEventTypesPublic, EventTypesPublic } from "@calcom/platform-libraries";
import { dynamicEvent } from "@calcom/platform-libraries";
import {
createEventType,
updateEventType,
getEventTypesPublic,
EventTypesPublic,
SystemField,
CustomField,
} from "@calcom/platform-libraries/event-types";
import { GetEventTypesQuery_2024_06_14, InputEventTransformed_2024_06_14 } from "@calcom/platform-types";
import { EventType } from "@calcom/prisma/client";
@@ -27,6 +33,9 @@ export class EventTypesService_2024_06_14 {
) {}
async createUserEventType(user: UserWithProfile, body: InputEventTransformed_2024_06_14) {
if (body.bookingFields) {
this.checkHasUserAccessibleEmailBookingField(body.bookingFields);
}
await this.checkCanCreateEventType(user.id, body);
const eventTypeUser = await this.getUserToCreateEvent(user);
const { destinationCalendar: _destinationCalendar, ...rest } = body;
@@ -74,6 +83,16 @@ export class EventTypesService_2024_06_14 {
await this.checkUserOwnsSchedule(userId, body.scheduleId);
}
checkHasUserAccessibleEmailBookingField(bookingFields: (SystemField | CustomField)[]) {
const emailField = bookingFields.find((field) => field.type === "email" && field.name === "email");
const isEmailFieldRequiredAndVisible = emailField?.required && !emailField?.hidden;
if (!isEmailFieldRequiredAndVisible) {
throw new BadRequestException(
"checkIsEmailUserAccessible - Email booking field must be required and visible"
);
}
}
async getEventTypeByUsernameAndSlug(
username: string,
eventTypeSlug: string,
@@ -112,8 +131,11 @@ export class EventTypesService_2024_06_14 {
: false;
const profileId = this.usersService.getUserMainProfile(user)?.id || null;
const selectedCalendars = await this.selectedCalendarsRepository.getUserSelectedCalendars(user.id);
const eventTypeSelectedCalendars =
await this.selectedCalendarsRepository.getUserEventTypeSelectedCalendar(user.id);
return {
id: user.id,
locale: user.locale ?? "en",
role: user.role,
username: user.username,
organizationId: user.organizationId,
@@ -121,6 +143,9 @@ export class EventTypesService_2024_06_14 {
profile: { id: profileId },
metadata: user.metadata,
selectedCalendars,
email: user.email,
userLevelSelectedCalendars: selectedCalendars,
allSelectedCalendars: [...eventTypeSelectedCalendars, ...selectedCalendars],
};
}
@@ -204,7 +229,14 @@ export class EventTypesService_2024_06_14 {
return defaultEventTypes;
}
async updateEventType(eventTypeId: number, body: InputEventTransformed_2024_06_14, user: UserWithProfile) {
async updateEventType(
eventTypeId: number,
body: Partial<InputEventTransformed_2024_06_14>,
user: UserWithProfile
) {
if (body.bookingFields) {
this.checkHasUserAccessibleEmailBookingField(body.bookingFields);
}
await this.checkCanUpdateEventType(user.id, eventTypeId, body.scheduleId);
const eventTypeUser = await this.getUserToUpdateEvent(user);
@@ -230,7 +262,7 @@ export class EventTypesService_2024_06_14 {
};
}
async checkCanUpdateEventType(userId: number, eventTypeId: number, scheduleId: number | undefined) {
async checkCanUpdateEventType(userId: number, eventTypeId: number, scheduleId: number | undefined | null) {
const existingEventType = await this.getUserEventType(userId, eventTypeId);
if (!existingEventType) {
throw new NotFoundException(`Event type with id ${eventTypeId} not found`);
@@ -242,7 +274,15 @@ export class EventTypesService_2024_06_14 {
async getUserToUpdateEvent(user: UserWithProfile) {
const profileId = this.usersService.getUserMainProfile(user)?.id || null;
const selectedCalendars = await this.selectedCalendarsRepository.getUserSelectedCalendars(user.id);
return { ...user, profile: { id: profileId }, selectedCalendars };
const eventTypeSelectedCalendars =
await this.selectedCalendarsRepository.getUserEventTypeSelectedCalendar(user.id);
return {
...user,
locale: user.locale ?? "en",
profile: { id: profileId },
userLevelSelectedCalendars: selectedCalendars,
allSelectedCalendars: [...eventTypeSelectedCalendars, ...selectedCalendars],
};
}
async deleteEventType(eventTypeId: number, userId: number) {
@@ -25,15 +25,17 @@ import {
transformSeatsApiToInternal,
SystemField,
CustomField,
} from "@calcom/platform-libraries";
InternalLocation,
InternalLocationSchema,
} from "@calcom/platform-libraries/event-types";
import {
CreateEventTypeInput_2024_06_14,
DestinationCalendar_2024_06_14,
InputEventTransformed_2024_06_14,
OutputUnknownLocation_2024_06_14,
UpdateEventTypeInput_2024_06_14,
} from "@calcom/platform-types";
import { OutputEventTypesService_2024_06_14 } from "./output-event-types.service";
import { BookerLayouts } from "@calcom/prisma/zod-utils";
interface ValidationContext {
eventTypeId?: number;
@@ -47,7 +49,6 @@ interface ValidationContext {
export class InputEventTypesService_2024_06_14 {
constructor(
private readonly eventTypesRepository: EventTypesRepository_2024_06_14,
private readonly outputEventTypesService: OutputEventTypesService_2024_06_14,
private readonly calendarsService: CalendarsService
) {}
@@ -58,7 +59,7 @@ export class InputEventTypesService_2024_06_14 {
const transformedBody = this.transformInputCreateEventType(inputEventType);
await this.validateEventTypeInputs({
seatsPerTimeSlot: transformedBody.seatsPerTimeSlot,
seatsPerTimeSlot: transformedBody?.seatsPerTimeSlot || null,
locations: transformedBody.locations,
requiresConfirmation: transformedBody.requiresConfirmation,
eventName: transformedBody.eventName,
@@ -124,12 +125,11 @@ export class InputEventTypesService_2024_06_14 {
} = inputEventType;
const confirmationPolicyTransformed = this.transformInputConfirmationPolicy(confirmationPolicy);
const hasMultipleLocations = (locations || defaultLocations).length > 1;
const eventType = {
...rest,
length: lengthInMinutes,
locations: this.transformInputLocations(locations || defaultLocations),
bookingFields: this.transformInputBookingFields(bookingFields, hasMultipleLocations),
bookingFields: this.transformInputBookingFields(bookingFields),
bookingLimits: bookingLimitsCount ? this.transformInputIntervalLimits(bookingLimitsCount) : undefined,
durationLimits: bookingLimitsDuration
? this.transformInputIntervalLimits(bookingLimitsDuration)
@@ -178,15 +178,12 @@ export class InputEventTypesService_2024_06_14 {
: {};
const confirmationPolicyTransformed = this.transformInputConfirmationPolicy(confirmationPolicy);
const hasMultipleLocations = !!(locations && locations?.length > 1);
const eventType = {
...rest,
length: lengthInMinutes,
locations: locations ? this.transformInputLocations(locations) : undefined,
bookingFields: bookingFields
? this.transformInputBookingFields(bookingFields, hasMultipleLocations)
: undefined,
bookingFields: bookingFields ? this.transformInputBookingFields(bookingFields) : undefined,
bookingLimits: bookingLimitsCount ? this.transformInputIntervalLimits(bookingLimitsCount) : undefined,
durationLimits: bookingLimitsDuration
? this.transformInputIntervalLimits(bookingLimitsDuration)
@@ -216,10 +213,7 @@ export class InputEventTypesService_2024_06_14 {
return transformLocationsApiToInternal(inputLocations);
}
transformInputBookingFields(
inputBookingFields: CreateEventTypeInput_2024_06_14["bookingFields"],
hasMultipleLocations: boolean
) {
transformInputBookingFields(inputBookingFields: CreateEventTypeInput_2024_06_14["bookingFields"]) {
const internalFields: (SystemField | CustomField)[] = inputBookingFields
? transformBookingFieldsApiToInternal(inputBookingFields)
: [];
@@ -248,7 +242,28 @@ export class InputEventTypesService_2024_06_14 {
systemCustomRescheduleReasonField || systemAfterFieldRescheduleReason,
];
return [...defaultFieldsBefore, ...userCustomFields, ...defaultFieldsAfter];
const bookingFields = [...defaultFieldsBefore, ...userCustomFields, ...defaultFieldsAfter];
if (!this.hasEmailOrPhoneOnlySetup(bookingFields)) {
throw new BadRequestException(
"Booking fields validation failed: visible and required email or visible and required attendee phone field is needed."
);
}
return bookingFields;
}
hasEmailOrPhoneOnlySetup(bookingFields: (SystemField | CustomField)[]) {
const emailField = bookingFields.find((field) => field.type === "email" && field.name === "email");
const attendeePhoneNumberField = bookingFields.find(
(field) => field.type === "phone" && field.name === "attendeePhoneNumber"
);
const isEmailFieldRequiredAndVisible = emailField?.required && !emailField?.hidden;
const isAttendeePhoneNumberFieldRequiredAndVisible =
attendeePhoneNumberField?.required && !attendeePhoneNumberField?.hidden;
return isEmailFieldRequiredAndVisible || isAttendeePhoneNumberFieldRequiredAndVisible;
}
isUserCustomField(field: SystemField | CustomField): field is CustomField {
@@ -272,7 +287,12 @@ export class InputEventTypesService_2024_06_14 {
}
transformInputBookerLayouts(inputBookerLayouts: CreateEventTypeInput_2024_06_14["bookerLayouts"]) {
return transformBookerLayoutsApiToInternal(inputBookerLayouts);
const layouts = transformBookerLayoutsApiToInternal(inputBookerLayouts);
if (!layouts) return undefined;
return {
defaultLayout: layouts.defaultLayout as unknown as BookerLayouts,
enabledLayouts: layouts.enabledLayouts as unknown as BookerLayouts[],
};
}
transformInputConfirmationPolicy(
@@ -303,23 +323,22 @@ export class InputEventTypesService_2024_06_14 {
eventName,
}: ValidationContext) {
let seatsPerTimeSlotDb: number | null = null;
let locationsDb: ReturnType<typeof this.transformInputLocations> = [];
let locationsDb: ReturnType<typeof this.transformLocations> = [];
let requiresConfirmationDb = false;
if (eventTypeId != null) {
const eventTypeDb = await this.eventTypesRepository.getEventTypeWithSeats(eventTypeId);
seatsPerTimeSlotDb = eventTypeDb?.seatsPerTimeSlot ?? null;
locationsDb = this.outputEventTypesService.transformLocations(eventTypeDb?.locations) ?? [];
locationsDb = this.transformLocations(eventTypeDb?.locations) ?? [];
requiresConfirmationDb = eventTypeDb?.requiresConfirmation ?? false;
}
const seatsPerTimeSlotFinal = seatsPerTimeSlot !== undefined ? seatsPerTimeSlot : seatsPerTimeSlotDb;
const seatsEnabledFinal = seatsPerTimeSlotFinal != null && seatsPerTimeSlotFinal > 0;
const seatsPerTimeSlotFinal = !!seatsPerTimeSlot ? seatsPerTimeSlot : seatsPerTimeSlotDb;
const seatsEnabledFinal = !!seatsPerTimeSlotFinal && seatsPerTimeSlotFinal > 0;
const locationsFinal = locations !== undefined ? locations : locationsDb;
const requiresConfirmationFinal =
requiresConfirmation !== undefined ? requiresConfirmation : requiresConfirmationDb;
this.validateSeatsSingleLocationRule(seatsEnabledFinal, locationsFinal);
this.validateSeatsRequiresConfirmationFalseRule(seatsEnabledFinal, requiresConfirmationFinal);
this.validateMultipleLocationsSeatsDisabledRule(locationsFinal, seatsEnabledFinal);
@@ -331,7 +350,7 @@ export class InputEventTypesService_2024_06_14 {
}
validateSeatsSingleLocationRule(
seatsEnabled: boolean,
locations: ReturnType<typeof this.transformInputLocations>
locations: ReturnType<typeof this.transformLocations>
) {
if (seatsEnabled && locations.length > 1) {
throw new BadRequestException(
@@ -339,6 +358,22 @@ export class InputEventTypesService_2024_06_14 {
);
}
}
transformLocations(locations: any) {
if (!locations) return [];
const knownLocations: InternalLocation[] = [];
const unknownLocations: OutputUnknownLocation_2024_06_14[] = [];
for (const location of locations) {
const result = InternalLocationSchema.safeParse(location);
if (result.success) {
knownLocations.push(result.data);
} else {
unknownLocations.push({ type: "unknown", location: JSON.stringify(location) });
}
}
return [...knownLocations];
}
validateSeatsRequiresConfirmationFalseRule(seatsEnabled: boolean, requiresConfirmation: boolean) {
if (seatsEnabled && requiresConfirmation) {
@@ -349,7 +384,7 @@ export class InputEventTypesService_2024_06_14 {
}
validateMultipleLocationsSeatsDisabledRule(
locations: ReturnType<typeof this.transformInputLocations>,
locations: ReturnType<typeof this.transformLocations>,
seatsEnabled: boolean
) {
if (locations.length > 1 && seatsEnabled) {
@@ -2,15 +2,18 @@ import { Injectable } from "@nestjs/common";
import type { EventType, User, Schedule, DestinationCalendar } from "@prisma/client";
import {
EventTypeMetaDataSchema,
userMetadata,
parseBookingLimit,
parseRecurringEvent,
getBookingFieldsWithSystemFields,
} from "@calcom/platform-libraries";
import {
EventTypeMetaDataSchema,
transformLocationsInternalToApi,
transformBookingFieldsInternalToApi,
parseRecurringEvent,
InternalLocationSchema,
SystemField,
CustomField,
parseBookingLimit,
transformIntervalLimitsInternalToApi,
transformFutureBookingLimitsInternalToApi,
transformRecurrenceInternalToApi,
@@ -21,8 +24,7 @@ import {
transformSeatsInternalToApi,
InternalLocation,
BookingFieldSchema,
getBookingFieldsWithSystemFields,
} from "@calcom/platform-libraries";
} from "@calcom/platform-libraries/event-types";
import {
TransformFutureBookingsLimitSchema_2024_06_14,
BookerLayoutsTransformedSchema,
@@ -198,13 +200,13 @@ export class OutputEventTypesService_2024_06_14 {
};
}
transformLocations(locations: any) {
if (!locations) return [];
transformLocations(locationDb: any) {
if (!locationDb) return [];
const knownLocations: InternalLocation[] = [];
const unknownLocations: OutputUnknownLocation_2024_06_14[] = [];
for (const location of locations) {
for (const location of locationDb) {
const result = InternalLocationSchema.safeParse(location);
if (result.success) {
knownLocations.push(result.data);
@@ -315,6 +317,7 @@ export class OutputEventTypesService_2024_06_14 {
transformEventTypeColor(eventTypeColor: any) {
if (!eventTypeColor) return undefined;
const parsedeventTypeColor = parseEventTypeColor(eventTypeColor);
if (!parsedeventTypeColor) return undefined;
return transformEventTypeColorsInternalToApi(parsedeventTypeColor);
}
@@ -39,8 +39,12 @@ import {
@DocsTags("Schedules")
@ApiHeader({
name: "cal-api-version",
description: `Must be set to \`2024-06-11\``,
description: `Must be set to ${VERSION_2024_06_11}`,
example: VERSION_2024_06_11,
required: true,
schema: {
default: VERSION_2024_06_11,
},
})
@ApiHeader({
name: "Authorization",
@@ -1,6 +1,9 @@
import { Injectable } from "@nestjs/common";
import { transformApiScheduleOverrides, transformApiScheduleAvailability } from "@calcom/platform-libraries";
import {
transformApiScheduleOverrides,
transformApiScheduleAvailability,
} from "@calcom/platform-libraries/schedules";
import { CreateScheduleInput_2024_06_11, ScheduleAvailabilityInput_2024_06_11 } from "@calcom/platform-types";
import { ScheduleOverrideInput_2024_06_11 } from "@calcom/platform-types";
@@ -0,0 +1,57 @@
import { filterReqHeaders } from "@/lib/filterReqHeaders";
import { ArgumentsHost, Catch, ExceptionFilter, Logger } from "@nestjs/common";
import { Request } from "express";
import { ERROR_STATUS } from "@calcom/platform-constants";
import {
CalendarAppError,
CalendarAppDelegationCredentialInvalidGrantError,
CalendarAppDelegationCredentialError,
CalendarAppDelegationCredentialConfigurationError,
CalendarAppDelegationCredentialClientIdNotAuthorizedError,
CalendarAppDelegationCredentialNotSetupError,
} from "@calcom/platform-libraries/app-store";
import { Response } from "@calcom/platform-types";
type CalendarError =
| CalendarAppError
| CalendarAppDelegationCredentialInvalidGrantError
| CalendarAppDelegationCredentialError
| CalendarAppDelegationCredentialConfigurationError
| CalendarAppDelegationCredentialNotSetupError
| CalendarAppDelegationCredentialClientIdNotAuthorizedError;
@Catch(
CalendarAppError,
CalendarAppDelegationCredentialInvalidGrantError,
CalendarAppDelegationCredentialError,
CalendarAppDelegationCredentialConfigurationError,
CalendarAppDelegationCredentialClientIdNotAuthorizedError,
CalendarAppDelegationCredentialNotSetupError
)
export class CalendarServiceExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger("CalendarServiceExceptionFilter");
catch(exception: CalendarError, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const requestId = request.headers["X-Request-Id"];
this.logger.error(`Calendar Service Exception Filter: ${exception?.message}`, {
exception,
body: request.body,
headers: filterReqHeaders(request.headers),
url: request.url,
method: request.method,
requestId,
});
response.status(400).json({
status: ERROR_STATUS,
timestamp: new Date().toISOString(),
path: request.url,
error: { code: exception.name, message: exception.message },
});
}
}
@@ -1,3 +1,4 @@
import { filterReqHeaders } from "@/lib/filterReqHeaders";
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, Logger } from "@nestjs/common";
import { Request } from "express";
@@ -18,7 +19,7 @@ export class HttpExceptionFilter implements ExceptionFilter<HttpException> {
this.logger.error(`Http Exception Filter: ${exception?.message}`, {
exception,
body: request.body,
headers: request.headers,
headers: filterReqHeaders(request.headers),
url: request.url,
method: request.method,
requestId,

Some files were not shown because too many files have changed in this diff Show More